# IthoPy
Python3 library to build and parse I2C bus messages for Itho devices. Meant for inclusion in your own Python code, not for direct consumption through the CLI (though it's on the list!).
NOTE: This library can't communicate directly with your device. For that you'll need to use something like [itho-esp](https://github.com/rustyx/itho-esp) to send/receive these messages to a ESP32 using MQTT.
## Tested devices
- Itho HRU ECO BAL LE (firmware: v12, hardware: v3)
## TODO
- [x] Implement message builder
- [x] Tests for message builder
- [x] Implement message parser
- [x] Tests for message parser
- [x] Support for more commands and devices:
- [x] Support for parsing device format and status queries
- [ ] Support for building device type and serial queries
- [ ] Allow user to select device model
- [ ] Load parameters from csv file
- [ ] Load data labels from csv file
- [ ] CLI for quick message parsing and building
## Building I2C Queries
NOTE: The most up to date examples can be found in the test suite.
```py
from ithopy.hru_device import HruDevice
hru = HruDevice(HruDevice.ESP32_ADDR, HruDevice.HRU_ADDR)
msg = hru.set_supply_fan_rpm(0)
print(str(msg))
# => "82 80 A4 10 06 13 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2D 00 04"
print(msg.bytes_list())
# => ['82', '80', 'A4', '10', '06', '13', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '2D', '00', '04']
print(msg.build().data)
# => [130, 128, 164, 16, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 4]
```
## Parsing I2C Responses
### Device Status Format
```python
import json
from ithopy.devices import HruMessageParser
parser = HruMessageParser()
message = self.parser.parse(
[0x80, 0x82, 0xA4, 0x00, 0x01, 0x16, 0x91, 0x11, 0x10, 0x90, 0x10,
0x90, 0x92, 0x92, 0x00, 0x92, 0x92, 0x00, 0x00, 0x91, 0x00, 0x10,
0x10, 0x00, 0x90, 0x00, 0x00, 0x10, 0xC8])
print(json.dumps(message.inspect()))
```
Outputs:
```json
{"dest": 128, "src": 130, "msg_class": "status_format", "msg_type": "report_response", "payload": {
"data_formats": [
{"idx": 0, "divider": 10, "bytes": 2, "signed": true, "offset": 0, "label": "Requested fanspeed (%)"},
{"idx": 1, "divider": 10, "bytes": 2, "signed": false, "offset": 2, "label": "Balance (%)"},
{"idx": 2, "divider": 1, "bytes": 2, "signed": false, "offset": 4, "label": "supply fan (rpm)"},
{"idx": 3, "divider": 1, "bytes": 2, "signed": true, "offset": 6, "label": "supply fan actual (rpm)"},
{"idx": 4, "divider": 1, "bytes": 2, "signed": false, "offset": 8, "label": "exhaust fan (rpm)"},
{"idx": 5, "divider": 1, "bytes": 2, "signed": true, "offset": 10, "label": "exhaust fan actual (rpm)"},
{"idx": 6, "divider": 100, "bytes": 2, "signed": true, "offset": 12, "label": "supply temp (\u00b0C)"},
{"idx": 7, "divider": 100, "bytes": 2, "signed": true, "offset": 14, "label": "exhaust temp (\u00b0C)"},
{"idx": 8, "divider": 1, "bytes": 1, "signed": false, "offset": 16, "label": "Status"},
{"idx": 9, "divider": 100, "bytes": 2, "signed": true, "offset": 17, "label": "Room temp (\u00b0C)"},
{"idx": 10, "divider": 100, "bytes": 2, "signed": true, "offset": 19, "label": "Outdoor temp (\u00b0C)"},
{"idx": 11, "divider": 1, "bytes": 1, "signed": false, "offset": 21, "label": "Valve position (pulse)"},
{"idx": 12, "divider": 1, "bytes": 1, "signed": false, "offset": 22, "label": "Bypass position (pulse)"},
{"idx": 13, "divider": 10, "bytes": 2, "signed": true, "offset": 23, "label": "Summercounter"},
{"idx": 14, "divider": 1, "bytes": 1, "signed": false, "offset": 25, "label": "Summerday"},
{"idx": 15, "divider": 1, "bytes": 2, "signed": false, "offset": 26, "label": "Frost timer (sec)"},
{"idx": 16, "divider": 1, "bytes": 2, "signed": false, "offset": 28, "label": "Boiler timer (min)"},
{"idx": 17, "divider": 1, "bytes": 1, "signed": false, "offset": 30, "label": "Frost block"},
{"idx": 18, "divider": 1, "bytes": 2, "signed": true, "offset": 31, "label": "Current position"},
{"idx": 19, "divider": 1, "bytes": 1, "signed": false, "offset": 33, "label": "VKKswitch"},
{"idx": 20, "divider": 1, "bytes": 1, "signed": false, "offset": 34, "label": "GHEswitch"},
{"idx": 21, "divider": 1, "bytes": 2, "signed": false, "offset": 35, "label": "Filter counter"}]}}
```
### Device Status Information
```python
import json
from ithopy.devices import HruMessageParser
parser = HruMessageParser()
message = self.parser.parse(
[0x80, 0x82, 0xA4, 0x00, 0x01, 0x16, 0x91, 0x11, 0x10, 0x90, 0x10,
0x90, 0x92, 0x92, 0x00, 0x92, 0x92, 0x00, 0x00, 0x91, 0x00, 0x10,
0x10, 0x00, 0x90, 0x00, 0x00, 0x10, 0xC8])
print(json.dumps(message.inspect()))
```
Outputs:
```json
{
"dest": 128,
"src": 130,
"msg_class": "device_status",
"msg_type": "report_response",
"payload": {
"0": {
"label": "Requested fanspeed (%)",
"value": 0.0
},
"1": {
"label": "Balance (%)",
"value": 92.4
},
"2": {
"label": "supply fan (rpm)",
"value": 926.0
},
"3": {
"label": "supply fan actual (rpm)",
"value": 920.0
},
"4": {
"label": "exhaust fan (rpm)",
"value": 1003.0
},
"5": {
"label": "exhaust fan actual (rpm)",
"value": 1003.0
},
"6": {
"label": "supply temp (\u00b0C)",
"value": 23.13
},
"7": {
"label": "exhaust temp (\u00b0C)",
"value": 24.42
},
"8": {
"label": "Status",
"value": 0.0
},
"9": {
"label": "Room temp (\u00b0C)",
"value": 23.13
},
"10": {
"label": "Outdoor temp (\u00b0C)",
"value": 24.42
},
"11": {
"label": "Valve position (pulse)",
"value": 0.0
},
"12": {
"label": "Bypass position (pulse)",
"value": 0.0
},
"13": {
"label": "Summercounter",
"value": 300.0
},
"14": {
"label": "Summerday",
"value": 1.0
},
"15": {
"label": "Frost timer (sec)",
"value": 0.0
},
"16": {
"label": "Boiler timer (min)",
"value": 177.0
},
"17": {
"label": "Frost block",
"value": 121.0
},
"18": {
"label": "Current position",
"value": 0.0
},
"19": {
"label": "VKKswitch",
"value": 0.0
},
"20": {
"label": "GHEswitch",
"value": 0.0
},
"21": {
"label": "Filter counter",
"value": 4245.0
}
}
}
```
## I2C packet format
`[Byte index] description`
```
[0] destination address
[1] reply address
[2..3] message class
[4] message type
[5] payload length
[n] payload
[n+1] checksum
```
Payload is parsed depending on message type.
## Unit Tests
Run all unit tests:
```sh
bin/test
```
Raw data
{
"_id": null,
"home_page": "https://github.com/philipkocanda/ithopy",
"name": "ithopy",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.11",
"maintainer_email": null,
"keywords": "itho, i2c",
"author": "Philip Kocanda",
"author_email": "philip@kocanda.nl",
"download_url": "https://files.pythonhosted.org/packages/7d/26/da866bae768587638d57d96b4648ecb76ed342d4b86e7e6e881b958bd8ef/ithopy-0.2.3.tar.gz",
"platform": null,
"description": "# IthoPy\n\nPython3 library to build and parse I2C bus messages for Itho devices. Meant for inclusion in your own Python code, not for direct consumption through the CLI (though it's on the list!).\n\nNOTE: This library can't communicate directly with your device. For that you'll need to use something like [itho-esp](https://github.com/rustyx/itho-esp) to send/receive these messages to a ESP32 using MQTT.\n\n## Tested devices\n\n- Itho HRU ECO BAL LE (firmware: v12, hardware: v3)\n\n## TODO\n\n- [x] Implement message builder\n- [x] Tests for message builder\n- [x] Implement message parser\n- [x] Tests for message parser\n- [x] Support for more commands and devices:\n - [x] Support for parsing device format and status queries\n - [ ] Support for building device type and serial queries\n - [ ] Allow user to select device model\n - [ ] Load parameters from csv file\n - [ ] Load data labels from csv file\n- [ ] CLI for quick message parsing and building\n\n## Building I2C Queries\n\nNOTE: The most up to date examples can be found in the test suite.\n\n```py\nfrom ithopy.hru_device import HruDevice\n\nhru = HruDevice(HruDevice.ESP32_ADDR, HruDevice.HRU_ADDR)\nmsg = hru.set_supply_fan_rpm(0)\n\nprint(str(msg))\n# => \"82 80 A4 10 06 13 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2D 00 04\"\n\nprint(msg.bytes_list())\n# => ['82', '80', 'A4', '10', '06', '13', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '00', '2D', '00', '04']\n\nprint(msg.build().data)\n# => [130, 128, 164, 16, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 0, 4]\n```\n\n## Parsing I2C Responses\n\n### Device Status Format\n\n```python\nimport json\nfrom ithopy.devices import HruMessageParser\n\nparser = HruMessageParser()\n\nmessage = self.parser.parse(\n [0x80, 0x82, 0xA4, 0x00, 0x01, 0x16, 0x91, 0x11, 0x10, 0x90, 0x10,\n 0x90, 0x92, 0x92, 0x00, 0x92, 0x92, 0x00, 0x00, 0x91, 0x00, 0x10,\n 0x10, 0x00, 0x90, 0x00, 0x00, 0x10, 0xC8])\n\nprint(json.dumps(message.inspect()))\n```\n\nOutputs:\n\n```json\n{\"dest\": 128, \"src\": 130, \"msg_class\": \"status_format\", \"msg_type\": \"report_response\", \"payload\": {\n\"data_formats\": [\n {\"idx\": 0, \"divider\": 10, \"bytes\": 2, \"signed\": true, \"offset\": 0, \"label\": \"Requested fanspeed (%)\"},\n {\"idx\": 1, \"divider\": 10, \"bytes\": 2, \"signed\": false, \"offset\": 2, \"label\": \"Balance (%)\"},\n {\"idx\": 2, \"divider\": 1, \"bytes\": 2, \"signed\": false, \"offset\": 4, \"label\": \"supply fan (rpm)\"},\n {\"idx\": 3, \"divider\": 1, \"bytes\": 2, \"signed\": true, \"offset\": 6, \"label\": \"supply fan actual (rpm)\"},\n {\"idx\": 4, \"divider\": 1, \"bytes\": 2, \"signed\": false, \"offset\": 8, \"label\": \"exhaust fan (rpm)\"},\n {\"idx\": 5, \"divider\": 1, \"bytes\": 2, \"signed\": true, \"offset\": 10, \"label\": \"exhaust fan actual (rpm)\"},\n {\"idx\": 6, \"divider\": 100, \"bytes\": 2, \"signed\": true, \"offset\": 12, \"label\": \"supply temp (\\u00b0C)\"},\n {\"idx\": 7, \"divider\": 100, \"bytes\": 2, \"signed\": true, \"offset\": 14, \"label\": \"exhaust temp (\\u00b0C)\"},\n {\"idx\": 8, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 16, \"label\": \"Status\"},\n {\"idx\": 9, \"divider\": 100, \"bytes\": 2, \"signed\": true, \"offset\": 17, \"label\": \"Room temp (\\u00b0C)\"},\n {\"idx\": 10, \"divider\": 100, \"bytes\": 2, \"signed\": true, \"offset\": 19, \"label\": \"Outdoor temp (\\u00b0C)\"},\n {\"idx\": 11, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 21, \"label\": \"Valve position (pulse)\"},\n {\"idx\": 12, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 22, \"label\": \"Bypass position (pulse)\"},\n {\"idx\": 13, \"divider\": 10, \"bytes\": 2, \"signed\": true, \"offset\": 23, \"label\": \"Summercounter\"},\n {\"idx\": 14, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 25, \"label\": \"Summerday\"},\n {\"idx\": 15, \"divider\": 1, \"bytes\": 2, \"signed\": false, \"offset\": 26, \"label\": \"Frost timer (sec)\"},\n {\"idx\": 16, \"divider\": 1, \"bytes\": 2, \"signed\": false, \"offset\": 28, \"label\": \"Boiler timer (min)\"},\n {\"idx\": 17, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 30, \"label\": \"Frost block\"},\n {\"idx\": 18, \"divider\": 1, \"bytes\": 2, \"signed\": true, \"offset\": 31, \"label\": \"Current position\"},\n {\"idx\": 19, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 33, \"label\": \"VKKswitch\"},\n {\"idx\": 20, \"divider\": 1, \"bytes\": 1, \"signed\": false, \"offset\": 34, \"label\": \"GHEswitch\"},\n {\"idx\": 21, \"divider\": 1, \"bytes\": 2, \"signed\": false, \"offset\": 35, \"label\": \"Filter counter\"}]}}\n```\n\n### Device Status Information\n\n```python\nimport json\nfrom ithopy.devices import HruMessageParser\n\nparser = HruMessageParser()\n\nmessage = self.parser.parse(\n [0x80, 0x82, 0xA4, 0x00, 0x01, 0x16, 0x91, 0x11, 0x10, 0x90, 0x10,\n 0x90, 0x92, 0x92, 0x00, 0x92, 0x92, 0x00, 0x00, 0x91, 0x00, 0x10,\n 0x10, 0x00, 0x90, 0x00, 0x00, 0x10, 0xC8])\n\nprint(json.dumps(message.inspect()))\n```\n\nOutputs:\n\n```json\n{\n \"dest\": 128,\n \"src\": 130,\n \"msg_class\": \"device_status\",\n \"msg_type\": \"report_response\",\n \"payload\": {\n \"0\": {\n \"label\": \"Requested fanspeed (%)\",\n \"value\": 0.0\n },\n \"1\": {\n \"label\": \"Balance (%)\",\n \"value\": 92.4\n },\n \"2\": {\n \"label\": \"supply fan (rpm)\",\n \"value\": 926.0\n },\n \"3\": {\n \"label\": \"supply fan actual (rpm)\",\n \"value\": 920.0\n },\n \"4\": {\n \"label\": \"exhaust fan (rpm)\",\n \"value\": 1003.0\n },\n \"5\": {\n \"label\": \"exhaust fan actual (rpm)\",\n \"value\": 1003.0\n },\n \"6\": {\n \"label\": \"supply temp (\\u00b0C)\",\n \"value\": 23.13\n },\n \"7\": {\n \"label\": \"exhaust temp (\\u00b0C)\",\n \"value\": 24.42\n },\n \"8\": {\n \"label\": \"Status\",\n \"value\": 0.0\n },\n \"9\": {\n \"label\": \"Room temp (\\u00b0C)\",\n \"value\": 23.13\n },\n \"10\": {\n \"label\": \"Outdoor temp (\\u00b0C)\",\n \"value\": 24.42\n },\n \"11\": {\n \"label\": \"Valve position (pulse)\",\n \"value\": 0.0\n },\n \"12\": {\n \"label\": \"Bypass position (pulse)\",\n \"value\": 0.0\n },\n \"13\": {\n \"label\": \"Summercounter\",\n \"value\": 300.0\n },\n \"14\": {\n \"label\": \"Summerday\",\n \"value\": 1.0\n },\n \"15\": {\n \"label\": \"Frost timer (sec)\",\n \"value\": 0.0\n },\n \"16\": {\n \"label\": \"Boiler timer (min)\",\n \"value\": 177.0\n },\n \"17\": {\n \"label\": \"Frost block\",\n \"value\": 121.0\n },\n \"18\": {\n \"label\": \"Current position\",\n \"value\": 0.0\n },\n \"19\": {\n \"label\": \"VKKswitch\",\n \"value\": 0.0\n },\n \"20\": {\n \"label\": \"GHEswitch\",\n \"value\": 0.0\n },\n \"21\": {\n \"label\": \"Filter counter\",\n \"value\": 4245.0\n }\n }\n}\n```\n\n## I2C packet format\n\n`[Byte index] description`\n\n```\n[0] destination address\n[1] reply address\n[2..3] message class\n[4] message type\n[5] payload length\n[n] payload\n[n+1] checksum\n```\n\nPayload is parsed depending on message type.\n\n## Unit Tests\n\nRun all unit tests:\n\n```sh\nbin/test\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "IthoPy is a library to communicate with I2C Itho devices",
"version": "0.2.3",
"project_urls": {
"Download": "https://github.com/philipkocanda/ithopy/archive/refs/tags/v0.2.3.zip",
"Homepage": "https://github.com/philipkocanda/ithopy"
},
"split_keywords": [
"itho",
" i2c"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "c9026fcdd14be4ac44c6e6c1ecc02d8dbeb906cccc37c79988bc496ec8369230",
"md5": "4480e6f5551aa5ad9f5d65299e83aad6",
"sha256": "0ae1fce32042e92e1e037ac18338fb7579179291eb8d09c3e4d6f937128efe4c"
},
"downloads": -1,
"filename": "ithopy-0.2.3-py3-none-any.whl",
"has_sig": false,
"md5_digest": "4480e6f5551aa5ad9f5d65299e83aad6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.11",
"size": 13692,
"upload_time": "2024-10-25T18:10:22",
"upload_time_iso_8601": "2024-10-25T18:10:22.019187Z",
"url": "https://files.pythonhosted.org/packages/c9/02/6fcdd14be4ac44c6e6c1ecc02d8dbeb906cccc37c79988bc496ec8369230/ithopy-0.2.3-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "7d26da866bae768587638d57d96b4648ecb76ed342d4b86e7e6e881b958bd8ef",
"md5": "9e888ee0763c0414d688482ad4191a81",
"sha256": "2fd73c0574e825294119b7f90a5bdfb25a5aabc65211d6e4d868f2cd0c2fa361"
},
"downloads": -1,
"filename": "ithopy-0.2.3.tar.gz",
"has_sig": false,
"md5_digest": "9e888ee0763c0414d688482ad4191a81",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.11",
"size": 11769,
"upload_time": "2024-10-25T18:10:22",
"upload_time_iso_8601": "2024-10-25T18:10:22.976371Z",
"url": "https://files.pythonhosted.org/packages/7d/26/da866bae768587638d57d96b4648ecb76ed342d4b86e7e6e881b958bd8ef/ithopy-0.2.3.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-25 18:10:22",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "philipkocanda",
"github_project": "ithopy",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "ithopy"
}