pax_global_header00006660000000000000000000000064143424216760014523gustar00rootroot0000000000000052 comment=254570084d8b1c78798cbf8e6f7424b6a92f846d michaelarnauts-comfoconnect-2545700/000077500000000000000000000000001434242167600173665ustar00rootroot00000000000000michaelarnauts-comfoconnect-2545700/.github/000077500000000000000000000000001434242167600207265ustar00rootroot00000000000000michaelarnauts-comfoconnect-2545700/.github/FUNDING.yml000066400000000000000000000001141434242167600225370ustar00rootroot00000000000000github: michaelarnauts custom: https://www.buymeacoffee.com/michaelarnauts michaelarnauts-comfoconnect-2545700/.gitignore000066400000000000000000000001451434242167600213560ustar00rootroot00000000000000# IDE .idea/ # Temporary files *.pyc *.pyo # Build-related files /*.egg-info /dist/ /build/ /venv/ michaelarnauts-comfoconnect-2545700/LICENSE000066400000000000000000000021521434242167600203730ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Michaël Arnauts https://github.com/michaelarnauts/comfoconnect Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. michaelarnauts-comfoconnect-2545700/MANIFEST.in000066400000000000000000000001421434242167600211210ustar00rootroot00000000000000include README.rst include LICENSE include requirements.txt recursive-include pycomfoconnect *.py michaelarnauts-comfoconnect-2545700/PROTOCOL-PDO.md000066400000000000000000000132041434242167600215710ustar00rootroot00000000000000Overview of known pdids: | pdid | type | description | |------|------|--------------------------------------------------------------------------------------------------------| | 16 | 1 | Away indicator (`01` = low, medium, high fan speed, `07` = away) | | 49 | 1 | Operating mode (`01` = limited manual, `05` = unlimited manual, `ff` = auto) | | 56 | 1 | Operating mode (`01` = unlimited manual, `ff` = auto) | | 65 | 1 | Fans: Fan speed setting (`00` (away), `01`, `02` or `03`) | | 66 | 1 | Bypass activation mode (`00` = auto, `01` = activated, `02` = deactivated) | | 67 | 1 | Temperature Profile (`00` = normal, `01` = cold, `02` = warm) | | 81 | 3 | General: Countdown until next fan speed change (`52020000` = 00000252 -> 594 seconds) | | 117 | 1 | Fans: Exhaust fan duty (`1c` = 28%) | | 118 | 1 | Fans: Supply fan duty (`1d` = 29%) | | 119 | 2 | Fans: Exhaust fan flow (`6e00` = 110 m³/h) | | 120 | 2 | Fans: Supply fan flow (`6900` = 105 m³/h) | | 121 | 2 | Fans: Exhaust fan speed (`2d04` = 1069 rpm) | | 122 | 2 | Fans: Supply fan speed (`5904` = 1113 rpm) | | 128 | 2 | Power Consumption: Current Ventilation (`0f00` = 15 W) | | 129 | 2 | Power Consumption: Total year-to-date (`1700` = 23 kWh) | | 130 | 2 | Power Consumption: Total from start (`1700` = 23 kWh) | | 144 | 2 | Preheater Power Consumption: Total year-to-date (`1700` = 23 kWh) | | 145 | 2 | Preheater Power Consumption: Total from start (`1700` = 23 kWh) | | 146 | 2 | Preheater Power Consumption: Current Ventilation (`0f00` = 15 W) | | 192 | 2 | Days left before filters must be replaced (`8200` = 130 days) | | 209 | 6 | Current RMOT (`7500` = 117 -> 11.7 °C) | | 212 | 1 | Temperature profile target (`ee00` = 23.8 °C)| | 213 | 2 | Avoided Heating: Avoided actual: (`b901` = 441 -> 4.41 W) | | 214 | 2 | Avoided Heating: Avoided year-to-date: (`dd01` = 477 kWh) | | 215 | 2 | Avoided Heating: Avoided total: (`dd01` = 477 kWh) | | 216 | 2 | Avoided Cooling: Avoided actual: (`b901` = 441 -> 4.41 W) | | 217 | 2 | Avoided Cooling: Avoided year-to-date: (`dd01` = 477 kWh) | | 218 | 2 | Avoided Cooling: Avoided total: (`dd01` = 477 kWh) | | 221 | 6 | Temperature & Humidity: Supply Air (`aa00` = 170 -> 17.0 °C) PostHeaterTempAfter | | 227 | 1 | Bypass state (`64` = 100%) | | 274 | 6 | Temperature & Humidity: Extract Air (`ab00` = 171 -> 17.1 °C) | | 275 | 6 | Temperature & Humidity: Exhaust Air (`5600` = 86 -> 8.6 °C) | | 276 | 6 | Temperature & Humidity: Outdoor Air (`3c00` = 60 -> 6.0 °C) | | 277 | 6 | Temperature & Humidity: Preheated Outdoor Air (`3c00` = 60 -> 6.0 °C) | | 278 | 6 | PostHeaterTempBefore | | 290 | 1 | Temperature & Humidity: Extract Air (`31` = 49%) | | 291 | 1 | Temperature & Humidity: Exhaust Air (`57` = 87%) | | 292 | 1 | Temperature & Humidity: Outdoor Air (`43` = 67%) | | 293 | 1 | Temperature & Humidity: Preheated Outdoor Air (`43` = 67%) | | 294 | 1 | Temperature & Humidity: Supply Air (`23` = 35%) | | 785 | 0 | ComfoCoolCompressor State | Unknown/uncertain messages: | pdid | type | description | |------|------|--------------------------------------------------------------------------------------------------------| | 33 | 1 | *Unknown* (`01`) | | 37 | 1 | *Unknown* (`00`) | | 53 | 1 | *Unknown* (`ff`) | | 70 | 1 | *Unknown* (`00`) | | 71 | 1 | *Unknown* (`00`) | | 82 | 3 | *Unknown* (`ffffffff`) | | 85 | 3 | *Unknown* (`ffffffff`) | | 86 | 3 | *Unknown* (`ffffffff`) | | 87 | 3 | *Unknown* (`ffffffff`) | | 176 | 1 | *Unknown* (`00`) | | 208 | 1 | *Unknown* (`00`), Unit of temperature | | 210 | 0 | *Unknown* (`00` = false) | | 211 | 0 | *Unknown* (`00` = false) | | 219 | 2 | *Unknown* (`0000`) | | 224 | 1 | *Unknown* (`03` = 3) | | 225 | 1 | *Unknown* (`01`) | | 226 | 2 | *Unknown* (`6400` = 100) | | 228 | 1 | *Unknown* (`00`) FrostProtectionUnbalance | | 321 | 2 | *Unknown* (`0700` = 7) | | 325 | 2 | *Unknown* (`0100` = 1) | | 337 | 3 | *Unknown* (`26000000` = 2409368) | | 338 | 3 | *Unknown* (`00000000`) | | 341 | 3 | *Unknown* (`00000000`) | | 369 | 1 | *Unknown* (`00`) | | 370 | 1 | *Unknown* (`00`) | | 371 | 1 | *Unknown* (`00`) | | 372 | 1 | *Unknown* (`00`) | | 384 | 6 | *Unknown* (`0000`) | | 386 | 0 | *Unknown* (`00` = false) | | 400 | 6 | *Unknown* (`0000`) | | 401 | 1 | *Unknown* (`00`) | | 402 | 0 | *Unknown* (`00` = false) PostHeaterPresent? | | 416 | 6 | *Unknown* (`70fe` = -400) Outdoor air temperature | | 417 | 6 | *Unknown* (`6400` = 100) GHE Ground temperature | | 418 | 1 | *Unknown* (`00`) GHE State | | 419 | 0 | *Unknown* (`00` = false) GHE Present| Overview of the types: | type | description | remark | |------|-------------|-------------------------------------------------------------------------------------------------| | 0 | CN_BOOL | `00` (false), `01` (true) | | 1 | CN_UINT8 | `00` (0) until `ff` (255) | | 2 | CN_UINT16 | `3412` = 1234 | | 3 | CN_UINT32 | `7856 3412` = 12345678 | | 5 | CN_INT8 | | | 6 | CN_INT16 | `3412` = 1234 | | 8 | CN_INT64 | | | 9 | CN_STRING | | | 10 | CN_TIME | | | 11 | CN_VERSION | | michaelarnauts-comfoconnect-2545700/PROTOCOL-RMI.md000066400000000000000000000402051434242167600215770ustar00rootroot00000000000000# ComfoControl CAN/RMI Protocol This document tries to outline the protocol as used by the newer "Q"-Models. Please note that some stuff might be version dependent, especially ranges. My findings are based on ~R1.6.2 Two basic assumptions: - Your airflow unit is m³/h - Your temperature is measured in °C If your ventilation is set to something else you need to try it out # Quick Overview of the network: Speed is 100kb/s, no CAN-FD but extended ID's are used (one exception: firmware uploading) Each device has an unique node-id smaller than 0x3F or 64. If a device detects that another device is writing using "his" id, the device will change its id Nodes send a periodic message to 0x10000000 + Node-ID with DLC 0 or 4 All PDO's (= sensors, regular data to be transferred, PUSH-Model) are sent with the following ID: `PDO-ID << 14 + 0x40 + Node-ID` Firmware-Updates are sent using 11-bit IDs or 1F800000 RMI-Commands are sent and received using extended-IDs: ``` 1F000000 + SrcAddr << 0 6 bits source Node-Id + DstAddr << 6 6 bits destination Node-Id + AnotherCounter <<12 2 bits we dont know what this is, set it to 0, everything else wont work + MultiMsg <<14 1 bit if this is a message composed of multiple CAN-frames + ErrorOccured <<15 1 bit When Response: If an error occured + IsRequest <<16 1 bit If the message is a request + SeqNr <<17 2 bits, request counter (should be the same for each frame in a multimsg), copied over to the response ``` Some Examples: ``` 1F015057: 11111 0000 0001 0101 00 0001 010111 multi-msg request with SeqNr = 0 1F011074: 11111 0000 0001 0001 00 0001 110100 single-msg request with SeqNr = 0 1F071074: 11111 0000 0111 0001 00 0001 110100 single-msg request with SeqNr = 3 1F005D01: 11111 0000 0000 0101 11 0100 000001 no-error multi-msg response, seqnr = 0 1F001D01: 11111 0000 0000 0001 11 0100 000001 no-error single-msg response, seqnr = 0 1F009D01: 11111 0000 0000 1001 11 0100 000001 error, seqnr = 0 ``` # Encoding an RMI command/message into CAN-Messages There are two ways of messaging them: 1. If the message is less or equal than 8 bytes, it will be sent unfragmented. CAN-Data = RMI-Data MultiMsg = 0 Example: Request: `01 1D 01 10 0A` (Get (`0x01`) from Unit `1D 01` (`TEMPHUMCONTROL 01`) exact value (`10`) variable `0A` (`Target Temperature Warm`)) Response: `E6 00` little-endian encoded, `0x00E6 = 230 = 23.0°C` ``` can1 1F011074 [5] 01 1D 01 10 0A can1 1F001D01 [2] E6 00 ``` 2. If the message is longer than 8 bytes, it will be fragmented into 7-byte blocks Each block is prepended with the index (starting at 0) of the block. If the block is the last to come, 0x80 is added to the first byte. Example: CMI-Request: `80 03 01`, answer `00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00` ``` can1 1F011074 [3] 80 03 01 # I send can1 1F005D01 [8] 00 00 00 00 00 00 00 00 # Answer can1 1F005D01 [8] 01 00 00 00 00 00 00 00 can1 1F005D01 [8] 02 00 00 00 00 00 00 00 can1 1F005D01 [8] 03 00 00 00 00 00 00 00 can1 1F005D01 [5] 84 00 00 00 00 ``` # Enums in the network If an error occures the reason maybe one of the following: | Number | Description | |-----------|-----------------------------------------------------------------------------------------------------------| | 11 | Unknown Command | | 12 | Unknown Unit | | 13 | Unknown SubUnit | | 15 | Type can not have a range | | 30 | Value given not in Range | | 32 | Property not gettable or settable | | 40 | Internal error | | 41 | Internal error, maybe your command is wrong | Please note that this list is not complete. For types please check [PROTOCOL-PDO.md](PROTOCOL-PDO.md) # Units The Ventilation is seperated into multiple Units, and sometimes even SubUnits. Here is a list of some existing units: | ID | Amount of SubUnits | Name | Responsible for | |-----------|-----------------------|-------------------|-----------------------------------------------------------------------------------------------| | 0x01 | 1 | NODE | Represents the general node with attributes like serial nr, etc. | | 0x02 | 1 | COMFOBUS | Unit responsible for comfobus-communication. Probably stores the ID's of connected devices. | | 0x03 | 1 | ERROR | Stores errors, allows errors to be reset | | 0x15 | 10 | SCHEDULE | Responsible for managing Timers, the schedule, etc. Check here for level, bypass etc. | | 0x16 | 2 | VALVE | ??? Bypass PreHeater and Extract | | 0x17 | 2 | FAN | Represents the two fans (supply, exhaust) | | 0x18 | 1 | POWERSENSOR | Counts the actual wattage of ventilation and accumulates to year and since factory reset | | 0x19 | 1 | PREHEATER | Represents the optional preheater | | 0x1A | 1 | HMI | Represents the Display + Buttons | | 0x1B | 1 | RFCOMMUNICATION | wireless-communication with attached devices | | 0x1C | 1 | FILTER | Counts the days since last filter change | | 0x1D | 1 | TEMPHUMCONTROL | Controls the target temperature, if its cooling or heating period and some settings | | 0x1E | 1 | VENTILATIONCONFIG | Responsible for managing various configuration options of the ventilation | | 0x20 | 1 | NODECONFIGURATION | Manages also some options | | 0x21 | 6 | TEMPERATURESENSOR | Represents the 6 temperature sensors in the ventilation | | 0x22 | 6 | HUMIDITYSENSOR | Represents the 6 humidity sensors | | 0x23 | 2 | PRESSURESENSOR | Represents both pressure sensors | | 0x24 | 1 | PERIPHERALS | Stores the ID of the ComfoCool attached, can reset peripheral errors here | | 0x25 | 4 | ANALOGINPUT | Provides data and functionality for the analog inputs, also the scaling for the voltages | | 0x26 | 1 | COOKERHOOD | "Dummy" unit, probably represents the ComfoHood if attached | | 0x27 | 1 | POSTHEATER | Represents the optional post heater attached (temperature sens, config) | | 0x28 | 1 | COMFOFOND | "Dummy" unit, represents the optional comfofond | # General commands There are three commands which always exist on a given Unit: - `0x01`: Read single property: This command reads a single property identified by the Unit, SubUnit and Property Id. Each writable property may have a definitionrange & recommended step size which are also accesible The syntax is: `01 UnitId SubUnitId GetType PropertyId` The answer is depended on the type of the property and the GetType. If the value, range and step (everything) is requested, the answer is: Value, lower boundry of range, upper boundry of range, step size. Example: Get maintainer password: `01 20 01 10 03`, answer: `34 32 31 30 00` (4210\0)\ Unit is `NODECONFIGURATION`, first SubUnit, PropertyId `03` - `0x02`: Read multiple property: This command allows to read multiple propertys within one request, given that the caller knows the type of each. As this command can be replaced by multiple calls to 0x01, we omit it here. - `0x03`: Set a single property: This commands sets one property to the given value. The value needs to be in the range as identified within 0x01, otherwise an error of 30 is returned. The step size, however, is not checked. Syntax: `03 UnitId SubUnitId PropertyId Value` Example: `03 1D 01 04 00`\ Unit is `TEMPHUMCONTROL`, first SubUnit, PropertyId: `04`\ Sets the Property "Sensor ventilation: Temperature passive" to off All other commands are >= 0x80 and dependent on the SubUnit. One example for a custom, unit dependent command is 0x85 from Unit `SCHEDULE` (`15`). It disables a given Timer Entry for a timer, in the example it allows the bypass to be returning to its automatic position:\ `85 15 02 01` Please do not try to run command 0x80, 0x82 on NodeConfiguration (0x20), they will probably break your configuration, and even worse would be calling ANY >= 0x80 command on 0x01. \ It can probably completely brick your ventilation. (it enters factory mode or tries to perform an update) # Some interesting properties: | Unit | PropId | Access | Format | Description | |-----------|-----------|---------|-----------|-------------------------------------------------------------| | 0x1E | 0x03 | rw | UINT16 | Ventilation speed in "Away" Level | | 0x1E | 0x04 | rw | UINT16 | Ventilation speed in "Low" Level | | 0x1E | 0x05 | rw | UINT16 | Ventilation speed in "Medium" Level | | 0x1E | 0x06 | rw | UINT16 | Ventilation speed in "High" Level | | 0x1E | 0x18 | rw | INT16 | Disbalance in percent | | 0x01 | 0x04 | ro | STRING | Serial number | | 0x01 | 0x08 | ro | STRING | Typenbezeichnung | | 0x01 | 0x0B | ro | STRING | Article number | | 0x01 | 0x0D | ro | STRING | Country (Manufacturing or Current?) | | 0x01 | 0x14 | ro | STRING | "ComfoAirQ" | | 0x1D | 0x02 | rw | INT16 | RMOT for heating period | | 0x1D | 0x03 | rw | INT16 | RMOT for cooling period | | 0x1D | 0x04 | rw | UINT8 | Passive temperature control (off, autoonly, on) | | 0x1D | 0x05 | rw | UINT8 | unknown (off, autoonly, on) | | 0x1D | 0x06 | rw | UINT8 | Humidity comfort control (off, autoonly, on) | | 0x1D | 0x07 | rw | UINT8 | Humidity protection (off, autoonly, on) | | 0x1D | 0x08 | rw | UINT8 | unknown (off, autoonly, on) | | 0x1D | 0x0A | rw | INT16 | Target temperature for profile: Heating | | 0x1D | 0x0B | rw | INT16 | Target temperature for profile: Normal | | 0x1D | 0x0C | rw | INT16 | Target temperature for profile: Cooling | # Basic list of commonly-used commands: This is a list of known commands: | Command | Description | |------------------------------------|---------------------------------------------------------------------------------| | `8415 0101 0000 0000 0100 0000 00` | Switch to fan speed away | | `8415 0101 0000 0000 0100 0000 01` | Switch to fan speed 1 | | `8415 0101 0000 0000 0100 0000 02` | Switch to fan speed 2 | | `8415 0101 0000 0000 0100 0000 03` | Switch to fan speed 3 | | `8415 0106 0000 0000 5802 0000 03` | Boost mode: start for 10m (= 600 seconds = `0x0258`) | | `8515 0106` | Boost mode: end | | `8515 0801` | Switch to auto mode | | `8415 0801 0000 0000 0100 0000 01` | Switch to manual mode | | `8415 0601 00000000 100e0000 01` | Set ventilation mode: supply only for 1 hour | | `8515 0601` | Set ventilation mode: balance mode | | `8415 0301 00000000 ffffffff 00` | Set temperature profile: normal | | `8415 0301 00000000 ffffffff 01` | Set temperature profile: cool | | `8415 0301 00000000 ffffffff 02` | Set temperature profile: warm | | `8415 0201 00000000 100e0000 01` | Set bypass: activated for 1 hour | | `8415 0201 00000000 100e0000 02` | Set bypass: deactivated for 1 hour | | `8515 0201` | Set bypass: auto | | `031d 0104 00` | Set sensor ventilation: temperature passive: off | | `031d 0104 01` | Set sensor ventilation: temperature passive: auto only | | `031d 0104 02` | Set sensor ventilation: temperature passive: on | | `031d 0106 00` | Set sensor ventilation: humidity comfort: off | | `031d 0106 01` | Set sensor ventilation: humidity comfort: auto only | | `031d 0106 02` | Set sensor ventilation: humidity comfort: on | | `031d 0107 00` | Set sensor ventilation: humidity protection: off | | `031d 0107 01` | Set sensor ventilation: humidity protection: auto | | `031d 0107 02` | Set sensor ventilation: humidity protection: on | michaelarnauts-comfoconnect-2545700/PROTOCOL.md000066400000000000000000000331451434242167600211570ustar00rootroot00000000000000# ComfoControl Protocol This document tries to explain the ComfoControl Protocol used by Zehnder ventilation units. You need a *ComfoConnect LAN C* device to interface with the unit. **Warning: This documentation is incomplete. If you have more information, don't hesitate to contribute by opening an issue or making a PR.** ## General packet structure The messages are send in a TCP-connection to port 56747, and are prepended with a header that resembles the "Length-prefix protocol buffers" format. This file [zehnder.proto](protobuf/zehnder.proto) contains a Protocol Buffers definition of the protocol. ## Manually decoding a packet The `protoc` utility can be used to manually decode a message based on the hex representation of the payload. You need to strip the headers and extract the `cmd` and `msg` before passing it to `protoc`. See the sections below to find out what to strip. Example: ```markdown 0000004 ea886190220044d68a07d85a2e3866fce 0000000000251010800170b3d54264b4 0004 08022002 0a10a886190220044d68a07d85a2e3866fce10001a126950686f6e652076616e2044657374696e79 length_ src______________________________ dst_____________________________ cmd# cmd_____ msg_____________________________________________________________________________ ``` Command: ```sh $ echo "08022002" | xxd -r -p | protoc --decode=GatewayOperation zehnder.proto ``` ```javascript type: RegisterAppRequestType reference: 2 ``` Message: ```sh $ echo "0a10a886190220044d68a07d85a2e3866fce10001a126950686f6e652076616e2044657374696e79" | xxd -r -p | protoc --decode=RegisterAppRequest zehnder.proto ``` ```javascript uuid: "\250\206\031\002 \004Mh\240}\205\242\343\206o\316" pin: 0 devicename: "iPhone van Destiny" ``` ## Device discovery (`DiscoveryOperation`) The bridge can be discovered by sending an UDP packet containing `0x0a00` to your network's broadcast address on port 56747. It will respond with the following connection information, also in a UDP packet. The last 6 bytes of the identifier seems to be the bridge's MAC address. This packet contains no header and can be fully passed to Protocol Buffers. Raw response data: ``` 12230a0d3139322e3136382e312e32313312100000000000251010800170b3d54264b41801 ``` ```javascript searchGatewayResponse { ipaddress: "192.168.1.213" uuid: "\000\000\000\000\000%\020\020\200\001p\263\325Bd\264" version: 1 } ``` You now have the bridge's `ipaddress` and `uuid`. You need these for further communication. ## Bridge communication (`GatewayOperation`) Further communication with the bridge happens by opening a TCP connection to the bridge on port 56747. Every message will start with the `length` of the message, excluding this `length`-field itself. Note that only `op` and `msg` is valid Protocol Buffers data. The other fields should not be parsed. ### Header The header then consist of a `src`, `dst` and `op_length`, followed by the `op` and the `msg`. The `src` seems to be generated by the client, the `dst` has to be obtained from the discovery packet (`uuid`). The `op_length` fields indicates the length of the `op` field, the rest of the data contains the `msg`. | Field | Data | Remark | |-----------------------|--------------------------------------|-------------------------------------------------------| | length (32 bit) | `0x0000004f` | Length of the whole message excluding this field | | src (12 bytes) | `0xaf154804169043898d2da77148f886be` | | | dst (12 bytes) | `0x0000000000251010800170b3d54264b4` | | | op_length (16 bit | `0x0004` | Length of the `op` message | | op (variable length) | `0x08342002` | Message with type `GatewayOperation` | | msg (variable length) | `...` | Message with type that is stated in `op.type` | ### Commands A message consists of a command block (`GatewayOperation`) and a message block (variable type). The command block contains a `type` and `reference` field. The `type` field indicates the type of the message block. The `reference` field will contain a incremental number that can be used to link a request with a reply. This is a list of the commands. | Request | Confirm | Notification / Response | Description | |-------------------------------|-------------------------------|-------------------------|----------------------------| | NoOperation | | | | | SetAddressRequestType | SetAddressConfirmType | | | | RegisterAppRequestType | RegisterAppConfirmType | | Adds a device in the registration list | | StartSessionRequestType | StartSessionConfirmType | | Start as session with the bridge | | CloseSessionRequestType | CloseSessionConfirmType | | Terminate your session | | ListRegisteredAppsRequestType | ListRegisteredAppsConfirmType | | Returns a list of registered apps on the bridge | | DeregisterAppRequestType | DeregisterAppConfirmType | | Remove a UUID from the registration list | | ChangePinRequestType | ChangePinConfirmType | | Change the PIN code | | GetRemoteAccessIdRequestType | GetRemoteAccessIdConfirmType | | | | SetRemoteAccessIdRequestType | SetRemoteAccessIdConfirmType | | | | GetSupportIdRequestType | GetSupportIdConfirmType | | | | GetWebIdRequestType | GetWebIdConfirmType | | | | SetWebIdRequestType | SetWebIdConfirmType | | | | SetPushIdRequestType | SetPushIdConfirmType | | | | DebugRequestType | DebugConfirmType | | | | UpgradeRequestType | UpgradeConfirmType | | | | SetDeviceSettingsRequestType | SetDeviceSettingsConfirmType | | | | VersionRequestType | VersionConfirmType | | | | | | GatewayNotificationType | | | | | KeepAliveType | You should send these to keep the connection open. | | FactoryResetType | | | | | CnTimeRequestType | CnTimeConfirmType | | Returns the seconds since 2000-01-01 00:00:00. | | CnNodeRequestType | | CnNodeNotificationType | | | CnRmiRequestType | CnRmiResponseType | | | | CnRmiAsyncRequestType | CnRmiAsyncConfirmType | CnRmiAsyncResponseType | | | CnRpdoRequestType | CnRpdoConfirmType | CnRpdoNotificationType | | | | | CnAlarmNotificationType | | | CnFupReadRegisterRequestType | CnFupReadRegisterConfirmType | | | | CnFupProgramBeginRequestType | CnFupProgramBeginConfirmType | | | | CnFupProgramRequestType | CnFupProgramConfirmType | | | | CnFupProgramEndRequestType | CnFupProgramEndConfirmType | | | | CnFupReadRequestType | CnFupReadConfirmType | | | | CnFupResetRequestType | CnFupResetConfirmType | | | #### RegisterApp (`RegisterAppRequestType` and `RegisterAppConfirmType`) Before you can login, you need to register your device, by sending a `type: RegisterAppRequestType`. ```javascript type: RegisterAppRequestType reference: 15 uuid: "\251\226\031\002 \004Mh\240}\205\242\343\206o\312" pin: 0 devicename: "Computer" ``` The bridge will respond with a `type: RegisterAppConfirmType`. In case of success, it will respond like this: ```javascript type: RegisterAppConfirmType reference: 15 ``` In case of a failure (invalid PIN), it will respond with a `result: NOT_ALLOWED`. ```javascript type: RegisterAppConfirmType result: NOT_ALLOWED reference: 15 ``` #### StartSession (`StartSessionRequestType` and `StartSessionConfirmType`) The client logs in by sending a `type: StartSessionRequestType`. Only one client can be logged in at the same time. ```javascript type: StartSessionRequestType reference: 16 ``` In case of a success, it will respond with a `type: StartSessionRequestType` with `result: OK`. ```javascript type: StartSessionConfirmType result: OK reference: 16 ``` If another client is already logged in, the bridge will respond with a `result` of `OTHER_SESSION`. The name of the other device will be in `devicename`. ```javascript type: StartSessionConfirmType result: OTHER_SESSION reference: 16 devicename: "Google Nexus 5X" ``` You can force the takeover of this session by specifying a `takeover: 1`. ```javascript type: StartSessionConfirmType result: OK reference: 17 takeover: 1 ``` Next, we see a few notifications. They seem to be messages to let the client know what nodes are available. We only send messages to `nodeId: 1`. ```javascript type: CnNodeNotificationType nodeId: 1 productId: 1 zoneId: 1 mode: NODE_NORMAL ``` ```javascript type: CnNodeNotificationType nodeId: 48 productId: 5 zoneId: 255 mode: NODE_NORMAL ``` These are the known `productId`s. | productId | type | description | |-----------|----------------|-----------------------------------------------------------------------------------------| | 1 | ComfoAirQ | The ComfoAirQ ventilation unit. | | 2 | ComfoSense | ComfoSense C | | 3 | ComfoSwitch | ComfoSwitch C | | 4 | OptionBox | | | 5 | ZehnderGateway | ComfoConnect LAN C | | 6 | ComfoCool | ComfoCool Q600 | | 7 | KNXGateway | ComfoConnect KNX C | | 8 | Service Tool | | | 9 | PT Tool | Production test tool | | 10 | DVT Tool | Design verification test tool | #### CloseSession (`CloseSessionRequestType`) The client logs out by sending a `type: CloseSessionRequestType`. The bridge doesn't seem to send a response on this. ```javascript type: CloseSessionRequestType ``` When the session is closed by the bridge (because another client connects with `takeover: 1`), the bridge will also send a `type: CloseSessionRequestType` message to the client. #### CnRpdoRequest (`CnRpdoRequestType`, `CnRpdoConfirmType` and `CnRpdoNotificationType`) To receive status updates, you need to register to a `pdid` by sending a `type: CnRpdoRequestType`. You also need to specify a `pdid`, `zone`, `type` and `timeout`. The zone always seems to be `1`. The `type` seems to depend on the `pdid`. ```javascript type: CnRpdoRequestType reference: 104 pdid: 176 zone: 1 type: 1 timeout: 4294967295 ``` The bridge will reply with a `type: CnRpdoConfirmType`. ```javascript type: CnRpdoConfirmType result: OK reference: 104 ``` Next, when an update is available, the bridge will send a `type: CnRpdoNotificationType`. ```javascript type: CnRpdoNotificationType pdid: 176 data: "\000" ``` For known PDOs, check [PROTOCOL-PDO.md](PROTOCOL-PDO.md) #### CnRmiRequest (`CnRmiRequestType` and `CnRmiResponseType`) You can execute a function on the device by invoking a `type: CnRmiRequestType`. You need to specify the `nodeId` and a `message`. This can make a configuration change, or request data. ```javascript type: CnRmiRequestType reference: 122 nodeId: 1 message: "\001\001\001\020\010" ``` The bridge will respond with a `type: CnRmiResponseType`. ```javascript type: CnRmiResponseType reference: 122 message: "ComfoAir Q450 B R RF ST Quality\000" ``` For an overview about the known RMI Requests, check [PROTOCOL-RMI.md](PROTOCOL-RMI.md)michaelarnauts-comfoconnect-2545700/README.rst000066400000000000000000000003621434242167600210560ustar00rootroot00000000000000pycomfoconnect ============== A Python library to interface with the Zehnder ComfoConnect LAN C bridge that is connected to the Zehnder ComfoAir Q350/450/600 ventilation units. Installation ------------ :: pip3 install pycomfoconnect michaelarnauts-comfoconnect-2545700/example/000077500000000000000000000000001434242167600210215ustar00rootroot00000000000000michaelarnauts-comfoconnect-2545700/example/example.py000077500000000000000000000203111434242167600230260ustar00rootroot00000000000000#!/usr/bin/python3 import argparse from time import sleep from pycomfoconnect import * parser = argparse.ArgumentParser() parser.add_argument('--ip', help='ip address of the bridge') args = parser.parse_args() ## Configuration ####################################################################################################### pin = 0 local_name = 'Computer' local_uuid = bytes.fromhex('00000000000000000000000000000005') def bridge_discovery(): ## Bridge discovery ################################################################################################ # Method 1: Use discovery to initialise Bridge # bridges = Bridge.discover(timeout=1) # if bridges: # bridge = bridges[0] # else: # bridge = None # Method 2: Use direct discovery to initialise Bridge bridges = Bridge.discover(args.ip) if bridges: bridge = bridges[0] else: bridge = None # Method 3: Setup bridge manually # bridge = Bridge(args.ip, bytes.fromhex('0000000000251010800170b3d54264b4')) if bridge is None: print("No bridges found!") exit(1) print("Bridge found: %s (%s)" % (bridge.uuid.hex(), bridge.host)) bridge.debug = True return bridge def callback_sensor(var, value): ## Callback sensors ################################################################################################ print("%s = %s" % (var, value)) def main(): # Discover the bridge bridge = bridge_discovery() ## Setup a Comfoconnect session ################################################################################### comfoconnect = ComfoConnect(bridge, local_uuid, local_name, pin) comfoconnect.callback_sensor = callback_sensor try: # Connect to the bridge # comfoconnect.connect(False) # Don't disconnect existing clients. comfoconnect.connect(True) # Disconnect existing clients. except Exception as e: print('ERROR: %s' % e) exit(1) ## Register sensors ################################################################################################ # comfoconnect.register_sensor(SENSOR_FAN_NEXT_CHANGE) # General: Countdown until next fan speed change # comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) # Fans: Fan speed setting # comfoconnect.register_sensor(SENSOR_FAN_SUPPLY_DUTY) # Fans: Supply fan duty # comfoconnect.register_sensor(SENSOR_FAN_EXHAUST_DUTY) # Fans: Exhaust fan duty # comfoconnect.register_sensor(SENSOR_FAN_SUPPLY_FLOW) # Fans: Supply fan flow # comfoconnect.register_sensor(SENSOR_FAN_EXHAUST_FLOW) # Fans: Exhaust fan flow # comfoconnect.register_sensor(SENSOR_FAN_SUPPLY_SPEED) # Fans: Supply fan speed # comfoconnect.register_sensor(SENSOR_FAN_EXHAUST_SPEED) # Fans: Exhaust fan speed # comfoconnect.register_sensor(SENSOR_POWER_CURRENT) # Power Consumption: Current Ventilation # comfoconnect.register_sensor(SENSOR_POWER_TOTAL_YEAR) # Power Consumption: Total year-to-date # comfoconnect.register_sensor(SENSOR_POWER_TOTAL) # Power Consumption: Total from start # comfoconnect.register_sensor(SENSOR_DAYS_TO_REPLACE_FILTER) # Days left before filters must be replaced # comfoconnect.register_sensor(SENSOR_AVOIDED_HEATING_CURRENT) # Avoided Heating: Avoided actual # comfoconnect.register_sensor(SENSOR_AVOIDED_HEATING_TOTAL_YEAR) # Avoided Heating: Avoided year-to-date # comfoconnect.register_sensor(SENSOR_AVOIDED_HEATING_TOTAL) # Avoided Heating: Avoided total comfoconnect.register_sensor(SENSOR_TEMPERATURE_SUPPLY) # Temperature & Humidity: Supply Air (temperature) comfoconnect.register_sensor(SENSOR_TEMPERATURE_EXTRACT) # Temperature & Humidity: Extract Air (temperature) comfoconnect.register_sensor(SENSOR_TEMPERATURE_EXHAUST) # Temperature & Humidity: Exhaust Air (temperature) comfoconnect.register_sensor(SENSOR_TEMPERATURE_OUTDOOR) # Temperature & Humidity: Outdoor Air (temperature) comfoconnect.register_sensor(SENSOR_HUMIDITY_SUPPLY) # Temperature & Humidity: Supply Air (temperature) comfoconnect.register_sensor(SENSOR_HUMIDITY_EXTRACT) # Temperature & Humidity: Extract Air (temperature) comfoconnect.register_sensor(SENSOR_HUMIDITY_EXHAUST) # Temperature & Humidity: Exhaust Air (temperature) comfoconnect.register_sensor(SENSOR_HUMIDITY_OUTDOOR) # Temperature & Humidity: Outdoor Air (temperature) comfoconnect.register_sensor(SENSOR_BYPASS_STATE) # Bypass state comfoconnect.register_sensor(SENSOR_OPERATING_MODE) # Operating mode comfoconnect.register_sensor(SENSOR_OPERATING_MODE_BIS) # Operating mode (bis) # comfoconnect.register_sensor(16) # 1 # comfoconnect.register_sensor(33) # 1 # comfoconnect.register_sensor(37) # 0 # comfoconnect.register_sensor(53) # -1 # comfoconnect.register_sensor(66) # 0 # comfoconnect.register_sensor(67) # 0 # comfoconnect.register_sensor(70) # 0 # comfoconnect.register_sensor(71) # 0 # comfoconnect.register_sensor(82) # ffffffff # comfoconnect.register_sensor(85) # ffffffff # comfoconnect.register_sensor(86) # ffffffff # comfoconnect.register_sensor(87) # ffffffff # comfoconnect.register_sensor(144) # 0 # comfoconnect.register_sensor(145) # 0 # comfoconnect.register_sensor(146) # 0 # comfoconnect.register_sensor(176) # 0 # comfoconnect.register_sensor(208) # 0 # comfoconnect.register_sensor(210) # 0 # comfoconnect.register_sensor(211) # 0 # comfoconnect.register_sensor(212) # 228 # comfoconnect.register_sensor(216) # 0 # comfoconnect.register_sensor(217) # 28 # comfoconnect.register_sensor(218) # 28 # comfoconnect.register_sensor(219) # 0 # comfoconnect.register_sensor(224) # 3 # comfoconnect.register_sensor(225) # 1 # comfoconnect.register_sensor(226) # 100 # comfoconnect.register_sensor(228) # 0 # comfoconnect.register_sensor(321) # 15 # comfoconnect.register_sensor(325) # 1 # comfoconnect.register_sensor(337) # # comfoconnect.register_sensor(338) # 00000000 # comfoconnect.register_sensor(341) # 00000000 # comfoconnect.register_sensor(369) # 0 # comfoconnect.register_sensor(370) # 0 # comfoconnect.register_sensor(371) # 0 # comfoconnect.register_sensor(372) # 0 # comfoconnect.register_sensor(384) # 0 # comfoconnect.register_sensor(386) # 0 # comfoconnect.register_sensor(400) # 0 # comfoconnect.register_sensor(401) # 0 # comfoconnect.register_sensor(402) # 0 # comfoconnect.register_sensor(416) # -400 # comfoconnect.register_sensor(417) # 100 # comfoconnect.register_sensor(418) # 0 # comfoconnect.register_sensor(419) # 0 ## Execute functions ############################################################################################### # ListRegisteredApps # for app in comfoconnect.cmd_list_registered_apps(): # print('%s: %s' % (app['uuid'].hex(), app['devicename'])) # DeregisterApp # comfoconnect.cmd_deregister_app(bytes.fromhex('00000000000000000000000000000001')) # VersionRequest version = comfoconnect.cmd_version_request() print(version) # TimeRequest # timeinfo = comfoconnect.cmd_time_request() # print(timeinfo) ## Executing functions ############################################################################################# # comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) # Go to away mode # comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW) # Set fan speed to 1 # comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM) # Set fan speed to 2 # comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH) # Set fan speed to 3 ## Example interaction ############################################################################################# try: print('Waiting... Stop with CTRL+C') while True: # Callback messages will arrive in the callback method. sleep(1) if not comfoconnect.is_connected(): print('We are not connected anymore...') except KeyboardInterrupt: pass ## Closing the session ############################################################################################# print('Disconnecting...') comfoconnect.disconnect() if __name__ == "__main__": main() michaelarnauts-comfoconnect-2545700/example/manually_decode.py000077500000000000000000000040311434242167600245210ustar00rootroot00000000000000#!/usr/bin/python3 import struct import sys import pycomfoconnect from pycomfoconnect import zehnder_pb2 """ Pipe the messages to this programm to decode them. If they are stored in HEX, you should pass them to xxd -r -p first. You can combine both input and output streams. Usage: $ cat captures/raw_*.txt | xxd -r -p | ./manually_decode.py """ pdids = {} rmis = {} while True: # Read packet from file msg_len_buf = sys.stdin.buffer.read(4) if not msg_len_buf: break msg_len = struct.unpack('>L', msg_len_buf)[0] msg_buf = sys.stdin.buffer.read(msg_len) # Decode packet message = pycomfoconnect.Message.decode(msg_len_buf + msg_buf) if message.cmd.type == zehnder_pb2.GatewayOperation.CnRmiRequestType: if not message.cmd.reference in rmis: rmis[message.cmd.reference] = {} rmis[message.cmd.reference]['tx'] = message.msg.message.hex() if message.cmd.type == zehnder_pb2.GatewayOperation.CnRmiResponseType: if not message.cmd.reference in rmis: rmis[message.cmd.reference] = {} rmis[message.cmd.reference]['rx'] = message.msg.message.hex() if message.cmd.type == zehnder_pb2.GatewayOperation.CnRpdoRequestType: if not message.msg.pdid in pdids: pdids[message.msg.pdid] = {} try: pdids[message.msg.pdid]['tx'].append(message.msg.type) except KeyError: pdids[message.msg.pdid]['tx'] = [message.msg.type] if message.cmd.type == zehnder_pb2.GatewayOperation.CnRpdoConfirmType: pass if message.cmd.type == zehnder_pb2.GatewayOperation.CnRpdoNotificationType: if not message.msg.pdid in pdids: pdids[message.msg.pdid] = {} try: pdids[message.msg.pdid]['rx'].append(message.msg.data.hex()) except KeyError: pdids[message.msg.pdid]['rx'] = [message.msg.data.hex()] print("CnRpdoRequestType") for pdid in pdids: print(pdid, pdids[pdid]) # # # print("CnRmiRequestType") # # for rmi in rmis: # # print(rmi, rmis[rmi]) # michaelarnauts-comfoconnect-2545700/protobuf/000077500000000000000000000000001434242167600212265ustar00rootroot00000000000000michaelarnauts-comfoconnect-2545700/protobuf/zehnder.proto000066400000000000000000000173531434242167600237630ustar00rootroot00000000000000syntax = "proto2"; message DiscoveryOperation { optional SearchGatewayRequest searchGatewayRequest = 1; optional SearchGatewayResponse searchGatewayResponse = 2; } message SearchGatewayRequest { } message SearchGatewayResponse { required string ipaddress = 1; required bytes uuid = 2; required uint32 version = 3; } message GatewayOperation { enum OperationType { NoOperation = 0; SetAddressRequestType = 1; RegisterAppRequestType = 2; StartSessionRequestType = 3; CloseSessionRequestType = 4; ListRegisteredAppsRequestType = 5; DeregisterAppRequestType = 6; ChangePinRequestType = 7; GetRemoteAccessIdRequestType = 8; SetRemoteAccessIdRequestType = 9; GetSupportIdRequestType = 10; SetSupportIdRequestType = 11; GetWebIdRequestType = 12; SetWebIdRequestType = 13; SetPushIdRequestType = 14; DebugRequestType = 15; UpgradeRequestType = 16; SetDeviceSettingsRequestType = 17; VersionRequestType = 18; SetAddressConfirmType = 51; RegisterAppConfirmType = 52; StartSessionConfirmType = 53; CloseSessionConfirmType = 54; ListRegisteredAppsConfirmType = 55; DeregisterAppConfirmType = 56; ChangePinConfirmType = 57; GetRemoteAccessIdConfirmType = 58; SetRemoteAccessIdConfirmType = 59; GetSupportIdConfirmType = 60; SetSupportIdConfirmType = 61; GetWebIdConfirmType = 62; SetWebIdConfirmType = 63; SetPushIdConfirmType = 64; DebugConfirmType = 65; UpgradeConfirmType = 66; SetDeviceSettingsConfirmType = 67; VersionConfirmType = 68; GatewayNotificationType = 100; KeepAliveType = 101; FactoryResetType = 102; CnTimeRequestType = 30; CnTimeConfirmType = 31; CnNodeRequestType = 42; CnNodeNotificationType = 32; CnRmiRequestType = 33; CnRmiResponseType = 34; CnRmiAsyncRequestType = 35; CnRmiAsyncConfirmType = 36; CnRmiAsyncResponseType = 37; CnRpdoRequestType = 38; CnRpdoConfirmType = 39; CnRpdoNotificationType = 40; CnAlarmNotificationType = 41; CnFupReadRegisterRequestType = 70; CnFupReadRegisterConfirmType = 71; CnFupProgramBeginRequestType = 72; CnFupProgramBeginConfirmType = 73; CnFupProgramRequestType = 74; CnFupProgramConfirmType = 75; CnFupProgramEndRequestType = 76; CnFupProgramEndConfirmType = 77; CnFupReadRequestType = 78; CnFupReadConfirmType = 79; CnFupResetRequestType = 80; CnFupResetConfirmType = 81; } enum GatewayResult { OK = 0; BAD_REQUEST = 1; INTERNAL_ERROR = 2; NOT_REACHABLE = 3; OTHER_SESSION = 4; NOT_ALLOWED = 5; NO_RESOURCES = 6; NOT_EXIST = 7; RMI_ERROR = 8; } optional GatewayOperation.OperationType type = 1; optional GatewayOperation.GatewayResult result = 2; optional string resultDescription = 3; optional uint32 reference = 4; } message GatewayNotification { repeated bytes pushUUIDs = 1; optional CnAlarmNotification alarm = 2; } message KeepAlive { } message FactoryReset { required bytes resetKey = 1; } message SetDeviceSettingsRequest { required bytes macAddress = 1; required string serialNumber = 2; } message SetDeviceSettingsConfirm { } message SetAddressRequest { required bytes uuid = 1; } message SetAddressConfirm { } message RegisterAppRequest { required bytes uuid = 1; required uint32 pin = 2; required string devicename = 3; } message RegisterAppConfirm { } message StartSessionRequest { optional bool takeover = 1; } message StartSessionConfirm { optional string devicename = 1; optional bool resumed = 2; } message CloseSessionRequest { } message CloseSessionConfirm { } message ListRegisteredAppsRequest { } message ListRegisteredAppsConfirm { message App { required bytes uuid = 1; required string devicename = 2; } repeated ListRegisteredAppsConfirm.App apps = 1; } message DeregisterAppRequest { required bytes uuid = 1; } message DeregisterAppConfirm { } message ChangePinRequest { required uint32 oldpin = 1; required uint32 newpin = 2; } message ChangePinConfirm { } message GetRemoteAccessIdRequest { } message GetRemoteAccessIdConfirm { optional bytes uuid = 1; } message SetRemoteAccessIdRequest { optional bytes uuid = 1; } message SetRemoteAccessIdConfirm { } message GetSupportIdRequest { } message GetSupportIdConfirm { optional bytes uuid = 1; optional uint32 remainingTime = 2; } message SetSupportIdRequest { optional bytes uuid = 1; optional uint32 validTime = 2; } message SetSupportIdConfirm { } message GetWebIdRequest { } message GetWebIdConfirm { optional bytes uuid = 1; } message SetWebIdRequest { optional bytes uuid = 1; } message SetWebIdConfirm { } message SetPushIdRequest { optional bytes uuid = 1; } message SetPushIdConfirm { } message UpgradeRequest { enum UpgradeRequestCommand { UPGRADE_START = 0; UPGRADE_CONTINUE = 1; UPGRADE_FINISH = 2; UPGRADE_ABORT = 3; } optional UpgradeRequest.UpgradeRequestCommand command = 1; optional bytes chunk = 2; } message UpgradeConfirm { } message DebugRequest { enum DebugRequestCommand { DBG_ECHO = 0; DBG_SLEEP = 1; DBG_SESSION_ECHO = 2; DBG_PRINT_SETTINGS = 3; DBG_ALARM = 4; DBG_LED = 5; DBG_GPI = 6; DBG_GPO = 7; DBG_RS232_WRITE = 8; DBG_RS232_READ = 9; DBG_CAN_WRITE = 10; DBG_CAN_READ = 11; DBG_KNX_WRITE = 12; DBG_KNX_READ = 13; DBG_TOGGLE = 14; DBG_REBOOT = 15; DBG_CLOUD = 16; DBG_EEPROM_READ = 17; DBG_EEPROM_WRITE = 18; } required DebugRequest.DebugRequestCommand command = 1; optional int32 argument = 2; } message DebugConfirm { required int32 result = 1; } message VersionRequest { } message VersionConfirm { required uint32 gatewayVersion = 1; required string serialNumber = 2; required uint32 comfoNetVersion = 3; } message CnTimeRequest { optional uint32 setTime = 1; } message CnTimeConfirm { required uint32 currentTime = 1; } message CnNodeRequest { } message CnNodeNotification { enum NodeModeType { NODE_LEGACY = 0; NODE_OFFLINE = 1; NODE_NORMAL = 2; NODE_UPDATE = 3; } required uint32 nodeId = 1; optional uint32 productId = 2 [default = 0]; optional uint32 zoneId = 3; optional CnNodeNotification.NodeModeType mode = 4; } message CnRmiRequest { required uint32 nodeId = 1; required bytes message = 2; } message CnRmiResponse { optional uint32 result = 1 [default = 0]; optional bytes message = 2; } message CnRmiAsyncRequest { required uint32 nodeId = 1; required bytes message = 2; } message CnRmiAsyncConfirm { optional uint32 result = 1 [default = 0]; } message CnRmiAsyncResponse { optional uint32 result = 1 [default = 0]; optional bytes message = 2; } message CnRpdoRequest { required uint32 pdid = 1; optional uint32 zone = 2 [default = 255]; optional uint32 type = 3; optional uint32 timeout = 4 [default = 4294967295]; } message CnRpdoConfirm { } message CnRpdoNotification { required uint32 pdid = 1; required bytes data = 2; } message CnAlarmNotification { optional uint32 zone = 1; optional uint32 productId = 2; optional uint32 productVariant = 3; optional string serialNumber = 4; optional uint32 swProgramVersion = 5; optional bytes errors = 6; optional uint32 errorId = 7; optional uint32 nodeId = 8; } message CnFupReadRegisterRequest { required uint32 node = 1; required uint32 registerId = 2; optional uint32 index = 3; } message CnFupReadRegisterConfirm { required uint32 value = 1; } message CnFupProgramBeginRequest { repeated uint32 node = 1; optional uint32 block = 2 [default = 0]; } message CnFupProgramBeginConfirm { } message CnFupProgramRequest { required bytes chunk = 1; } message CnFupProgramConfirm { } message CnFupProgramEndRequest { } message CnFupProgramEndConfirm { } message CnFupReadRequest { required uint32 node = 1; optional uint32 block = 2 [default = 0]; } message CnFupReadConfirm { optional bytes chunk = 1; optional bool last = 2 [default = false]; } message CnFupResetRequest { required uint32 node = 1; } message CnFupResetConfirm { } michaelarnauts-comfoconnect-2545700/pycomfoconnect/000077500000000000000000000000001434242167600224145ustar00rootroot00000000000000michaelarnauts-comfoconnect-2545700/pycomfoconnect/__init__.py000077500000000000000000000004771434242167600245400ustar00rootroot00000000000000""" PyComfoConnect: Manage your Zehnder ComfoConnect Q350/Q450/Q650 ventilation unit """ DEFAULT_UUID = '00000000000000000000000000000001' DEFAULT_NAME = 'pycomfoconnect' DEFAULT_PIN = 0 __author__ = 'Michaël Arnauts ' from .comfoconnect import * from .error import * from .const import *michaelarnauts-comfoconnect-2545700/pycomfoconnect/bridge.py000077500000000000000000000072361434242167600242350ustar00rootroot00000000000000import logging import select import socket from .message import * _LOGGER = logging.getLogger('bridge') class Bridge(object): """Implements an interface to send and receive messages from the Bridge.""" PORT = 56747 @staticmethod def discover(host=None, timeout=5): """Broadcast the network and look for local bridges.""" # Setup socket udpsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udpsocket.setblocking(0) udpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # Send broadcast packet if host is None: udpsocket.sendto(b"\x0a\x00", ('', Bridge.PORT)) else: udpsocket.sendto(b"\x0a\x00", (host, Bridge.PORT)) # Try to read response parser = DiscoveryOperation() bridges = [] while True: ready = select.select([udpsocket], [], [], timeout) if not ready[0]: break data, source = udpsocket.recvfrom(100) # Parse data parser.ParseFromString(data) ip_address = parser.searchGatewayResponse.ipaddress uuid = parser.searchGatewayResponse.uuid # Add a new Bridge to the list bridges.append( Bridge(ip_address, uuid) ) # Don't look for other bridges if we directly discovered it by IP if host: break udpsocket.close() # Return found bridges return bridges def __init__(self, host: str, uuid: str) -> None: self.host = host self.uuid = uuid self._socket = None self.debug = False def connect(self) -> bool: """Open connection to the bridge.""" if self._socket is None: tcpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcpsocket.connect((self.host, Bridge.PORT)) tcpsocket.setblocking(0) self._socket = tcpsocket return True def disconnect(self) -> bool: """Close connection to the bridge.""" self._socket.close() self._socket = None return True def is_connected(self): """Returns weather there is an open socket.""" return self._socket is not None def read_message(self, timeout=1) -> Message: """Read a message from the connection.""" if self._socket is None: raise BrokenPipeError() # Check if there is data available ready = select.select([self._socket], [], [], timeout) if not ready[0]: # Timeout return None # Read packet size msg_len_buf = self._socket.recv(4) if not msg_len_buf: # No data, but there has to be. raise BrokenPipeError() # Read rest of packet msg_len = struct.unpack('>L', msg_len_buf)[0] msg_buf = self._socket.recv(msg_len) if not msg_buf: # No data, but there has to be. raise BrokenPipeError() # Decode message message = Message.decode(msg_len_buf + msg_buf) # Debug message _LOGGER.debug("RX %s", message) return message def write_message(self, message: Message) -> bool: """Send a message.""" if self._socket is None: raise Exception('Not connected!') # Construct packet packet = message.encode() # Debug message _LOGGER.debug("TX %s", message) # Send packet try: self._socket.sendall(packet) except BrokenPipeError: self.disconnect() return False return True michaelarnauts-comfoconnect-2545700/pycomfoconnect/comfoconnect.py000077500000000000000000000435161434242167600254570ustar00rootroot00000000000000import logging import queue import struct import threading import time from .bridge import Bridge from .error import * from .message import Message from .zehnder_pb2 import * KEEPALIVE = 60 DEFAULT_LOCAL_UUID = bytes.fromhex('00000000000000000000000000001337') DEFAULT_LOCAL_DEVICENAME = 'pycomfoconnect' DEFAULT_PIN = 0 _LOGGER = logging.getLogger('comfoconnect') # Sensor variable size RPDO_TYPE_MAP = { 16: 1, 33: 1, 37: 1, 49: 1, 53: 1, 56: 1, 65: 1, 66: 1, 67: 1, 70: 1, 71: 1, 81: 3, 82: 3, 85: 3, 86: 3, 87: 3, 117: 1, 118: 1, 119: 2, 120: 2, 121: 2, 122: 2, 128: 2, 129: 2, 130: 2, 144: 2, 145: 2, 146: 2, 176: 1, 192: 2, 208: 1, 209: 6, 210: 0, 211: 0, 212: 6, 213: 2, 214: 2, 215: 2, 216: 2, 217: 2, 218: 2, 219: 2, 221: 6, 224: 1, 225: 1, 226: 2, 227: 1, 228: 1, 274: 6, 275: 6, 276: 6, 277: 6, 290: 1, 291: 1, 292: 1, 293: 1, 294: 1, 321: 2, 325: 2, 337: 3, 338: 3, 341: 3, 369: 1, 370: 1, 371: 1, 372: 1, 384: 6, 386: 0, 400: 6, 401: 1, 402: 0, 416: 6, 417: 6, 418: 1, 419: 0, } # Product ID Map PRODUCT_ID_MAP = { 1: "ComfoAirQ", 2: "ComfoSense", 3: "ComfoSwitch", 4: "OptionBox", 5: "ZehnderGateway", 6: "ComfoCool", 7: "KNXGateway", 8: "Service Tool", 9: "Production test tool", 10: "Design verification test tool" } class ComfoConnect(object): """Implements the commands to communicate with the ComfoConnect ventilation unit.""" """Callback function to invoke when sensor updates are received.""" callback_sensor = None def __init__(self, bridge: Bridge, local_uuid=DEFAULT_LOCAL_UUID, local_devicename=DEFAULT_LOCAL_DEVICENAME, pin=DEFAULT_PIN): self._bridge = bridge self._local_uuid = local_uuid self._local_devicename = local_devicename self._pin = pin self._reference = 1 self._queue = queue.Queue() self._connected = threading.Event() self._stopping = False self._message_thread = None self._connection_thread = None self.sensors = {} # ================================================================================================================== # Core functions # ================================================================================================================== def connect(self, takeover=False): """Connect to the bridge and login. Disconnect existing clients if needed by default.""" try: # Start connection self._connect(takeover=takeover) except PyComfoConnectNotAllowed: raise Exception('Could not connect to the bridge since the PIN seems to be invalid.') except PyComfoConnectOtherSession: raise Exception('Could not connect to the bridge since there is already an open session.') except Exception as exc: _LOGGER.error(exc) raise Exception('Could not connect to the bridge.') # Set the stopping flag self._stopping = False self._connected.clear() # Start connection thread self._connection_thread = threading.Thread(target=self._connection_thread_loop) self._connection_thread.start() if not self._connected.wait(10): raise Exception('Could not connect to bridge since it didn\'t reply on time.') return True def disconnect(self): """Disconnect from the bridge.""" # Set the stopping flag self._stopping = True # Wait for the background thread to finish self._connection_thread.join() self._connection_thread = None def is_connected(self): """Returns whether there is a connection with the bridge.""" return self._bridge.is_connected() def register_sensor(self, sensor_id: int, sensor_type: int = None): """Register a sensor on the bridge and keep it in memory that we are registered to this sensor.""" if not sensor_type: sensor_type = RPDO_TYPE_MAP.get(sensor_id) if sensor_type is None: raise Exception("Registering sensor %d with unknown type" % sensor_id) # Register on bridge try: reply = self.cmd_rpdo_request(sensor_id, sensor_type) except PyComfoConnectNotAllowed: return None # Register in memory self.sensors[sensor_id] = sensor_type return reply def unregister_sensor(self, sensor_id: int, sensor_type: int = None): """Register a sensor on the bridge and keep it in memory that we are registered to this sensor.""" if sensor_type is None: sensor_type = RPDO_TYPE_MAP.get(sensor_id) if sensor_type is None: raise Exception("Unregistering sensor %d with unknown type" % sensor_id) # Unregister in memory self.sensors.pop(sensor_id, None) # Unregister on bridge self.cmd_rpdo_request(sensor_id, sensor_type, timeout=0) def _command(self, command, params=None, use_queue=True): """Sends a command and wait for a response if the request is known to return a result.""" # Construct the message message = Message.create( self._local_uuid, self._bridge.uuid, command, {'reference': self._reference}, params ) # Increase message reference self._reference += 1 # Send the message self._bridge.write_message(message) try: # Check if this command has a confirm type set confirm_type = message.class_to_confirm[command] # Read a message reply = self._get_reply(confirm_type, use_queue=use_queue) return reply except KeyError: return None def _get_reply(self, confirm_type=None, timeout=5, use_queue=True): """Pops a message of the queue, optionally looking for a specific type.""" start = time.time() while True: message = None if use_queue: try: # Fetch the message from the queue. The network thread has put it there for us. message = self._queue.get(timeout=timeout) if message: self._queue.task_done() except queue.Empty: # We got no message pass else: # Fetch the message directly from the socket message = self._bridge.read_message(timeout=timeout) if message: # Check status code if message.cmd.result == GatewayOperation.OK: pass elif message.cmd.result == GatewayOperation.BAD_REQUEST: raise PyComfoConnectBadRequest() elif message.cmd.result == GatewayOperation.INTERNAL_ERROR: raise PyComfoConnectInternalError() elif message.cmd.result == GatewayOperation.NOT_REACHABLE: raise PyComfoConnectNotReachable() elif message.cmd.result == GatewayOperation.OTHER_SESSION: raise PyComfoConnectOtherSession(message.msg.devicename) elif message.cmd.result == GatewayOperation.NOT_ALLOWED: raise PyComfoConnectNotAllowed() elif message.cmd.result == GatewayOperation.NO_RESOURCES: raise PyComfoConnectNoResources() elif message.cmd.result == GatewayOperation.NOT_EXIST: raise PyComfoConnectNotExist() elif message.cmd.result == GatewayOperation.RMI_ERROR: raise PyComfoConnectRmiError() if confirm_type is None: # We just need a message return message elif message.msg.__class__ == confirm_type: # We need the message with the correct type return message else: # We got a message with an incorrect type. Hopefully, this doesn't happen to often, # since we just put it back on the queue. self._queue.put(message) if time.time() - start > timeout: raise ValueError('Timeout waiting for response.') # ================================================================================================================== # Connection thread # ================================================================================================================== def _connection_thread_loop(self): """Makes sure that there is a connection open.""" self._stopping = False while not self._stopping: # Start connection if not self.is_connected(): # Wait a bit to avoid hammering the bridge time.sleep(5) try: # Connect or re-connect self._connect() except PyComfoConnectOtherSession: self._bridge.disconnect() _LOGGER.error('Could not connect to the bridge since there is already an open session.') continue except Exception as exc: _LOGGER.error(exc) raise Exception('Could not connect to the bridge.') # Start background thread self._message_thread = threading.Thread(target=self._message_thread_loop) self._message_thread.start() # Re-register for sensor updates for sensor_id in self.sensors: self.cmd_rpdo_request(sensor_id, self.sensors[sensor_id]) # Send the event that we are ready self._connected.set() # Wait until the message thread stops working self._message_thread.join() # Close socket connection self._bridge.disconnect() def _connect(self, takeover=False): """Connect to the bridge and login. Disconnect existing clients if needed by default.""" try: # Connect to the bridge self._bridge.connect() # Login self.cmd_start_session(takeover, use_queue=False) except PyComfoConnectNotAllowed: # No dice, maybe we are not registered yet... # Register self.cmd_register_app(self._local_uuid, self._local_devicename, self._pin, use_queue=False) # Login self.cmd_start_session(takeover, use_queue=False) return True # ================================================================================================================== # Message thread # ================================================================================================================== def _message_thread_loop(self): """Listen for incoming messages and queue them or send them to a callback method.""" # Reinitialise the queues self._queue = queue.Queue() next_keepalive = 0 while not self._stopping: # Sends a keepalive every KEEPALIVE seconds. if time.time() > next_keepalive: next_keepalive = time.time() + KEEPALIVE self.cmd_keepalive() try: # Read a message from the bridge. message = self._bridge.read_message() except BrokenPipeError as exc: # Close this thread. The connection_thread will restart us. _LOGGER.warning('The connection was broken. We will try to reconnect later.') return if message: if message.cmd.type == GatewayOperation.CnRpdoNotificationType: self._handle_rpdo_notification(message) elif message.cmd.type == GatewayOperation.GatewayNotificationType: _LOGGER.info('Unhandled GatewayNotificationType') # TODO: We should probably handle these somehow pass elif message.cmd.type == GatewayOperation.CnNodeNotificationType: _LOGGER.info('CnNodeNotificationType: %s @ Node Id %d [%s]', PRODUCT_ID_MAP[message.msg.productId], message.msg.nodeId, message.msg.NodeModeType.Name(message.msg.mode)) # TODO: We should probably handle these somehow pass elif message.cmd.type == GatewayOperation.CnAlarmNotificationType: _LOGGER.info('Unhandled CnAlarmNotificationType') # TODO: We should probably handle these somehow pass elif message.cmd.type == GatewayOperation.CloseSessionRequestType: _LOGGER.info('The Bridge has asked us to close the connection. We will try to reconnect later.') # Close this thread. The connection_thread will restart us. return else: # Send other messages to a queue self._queue.put(message) return def _handle_rpdo_notification(self, message): """Update internal sensor state and invoke callback.""" # Only process CnRpdoNotificationType if message.cmd.type != GatewayOperation.CnRpdoNotificationType: return False # Extract data data = message.msg.data.hex() if len(data) == 2: val = struct.unpack('b', message.msg.data)[0] elif len(data) == 4: val = struct.unpack('h', message.msg.data)[0] elif len(data) == 8: val = data else: val = data # Update local state # self.sensors[message.msg.pdid] = val if self.callback_sensor: self.callback_sensor(message.msg.pdid, val) return True # ================================================================================================================== # Commands # ================================================================================================================== def cmd_start_session(self, take_over=False, use_queue: bool = True): """Starts the session on the device by logging in and optionally disconnecting an already existing session.""" reply = self._command( StartSessionRequest, { 'takeover': take_over }, use_queue=use_queue ) return reply # TODO: parse output def cmd_close_session(self, use_queue: bool = True): """Stops the current session.""" reply = self._command( CloseSessionRequest, use_queue=use_queue ) return reply # TODO: parse output def cmd_list_registered_apps(self, use_queue: bool = True): """Returns a list of all the registered clients.""" reply = self._command( ListRegisteredAppsRequest, use_queue=use_queue ) return [ {'uuid': app.uuid, 'devicename': app.devicename} for app in reply.msg.apps ] def cmd_register_app(self, uuid, device_name, pin, use_queue: bool = True): """Register a new app by specifying our own uuid, device_name and pin code.""" reply = self._command( RegisterAppRequest, { 'uuid': uuid, 'devicename': device_name, 'pin': pin, }, use_queue=use_queue ) return reply # TODO: parse output def cmd_deregister_app(self, uuid, use_queue: bool = True): """Remove the specified app from the registration list.""" if uuid == self._local_uuid: raise Exception('You should not deregister yourself.') try: self._command( DeregisterAppRequest, { 'uuid': uuid }, use_queue=use_queue ) return True except PyComfoConnectBadRequest: return False def cmd_version_request(self, use_queue: bool = True): """Returns version information.""" reply = self._command( VersionRequest, use_queue=use_queue ) return { 'gatewayVersion': reply.msg.gatewayVersion, 'serialNumber': reply.msg.serialNumber, 'comfoNetVersion': reply.msg.comfoNetVersion, } def cmd_time_request(self, use_queue: bool = True): """Returns the current time on the device.""" reply = self._command( CnTimeRequest, use_queue=use_queue ) return reply.msg.currentTime def cmd_rmi_request(self, message, node_id: int = 1, use_queue: bool = True): """Sends a RMI request.""" reply = self._command( CnRmiRequest, { 'nodeId': node_id or 1, 'message': message }, use_queue=use_queue ) return True def cmd_rpdo_request(self, pdid: int, type: int = 1, zone: int = 1, timeout=None, use_queue: bool = True): """Register a RPDO request.""" reply = self._command( CnRpdoRequest, { 'pdid': pdid, 'type': type, 'zone': zone or 1, 'timeout': timeout }, use_queue=use_queue ) return reply def cmd_keepalive(self, use_queue: bool = True): """Sends a keepalive.""" self._command( KeepAlive, use_queue=use_queue ) return True michaelarnauts-comfoconnect-2545700/pycomfoconnect/const.py000066400000000000000000000102321434242167600241120ustar00rootroot00000000000000# Commands CMD_FAN_MODE_AWAY = b'\x84\x15\x01\x01\x00\x00\x00\x00\x01\x00\x00\x00\x00' CMD_FAN_MODE_LOW = b'\x84\x15\x01\x01\x00\x00\x00\x00\x01\x00\x00\x00\x01' CMD_FAN_MODE_MEDIUM = b'\x84\x15\x01\x01\x00\x00\x00\x00\x01\x00\x00\x00\x02' CMD_FAN_MODE_HIGH = b'\x84\x15\x01\x01\x00\x00\x00\x00\x01\x00\x00\x00\x03' CMD_MODE_AUTO = b'\x85\x15\x08\x01' CMD_MODE_MANUAL = b'\x84\x15\x08\x01\x00\x00\x00\x00\x01\x00\x00\x00\x01' CMD_VENTMODE_SUPPLY = b'\x84\x15\x06\x01\x00\x00\x00\x00\x10\x0e\x00\x00\x01' CMD_VENTMODE_BALANCE = b'\x85\x15\x06\x01' CMD_VENTMODE_STOP_SUPPLY_FAN_1 = b'\x84\x15\x07\x01\x00\x00\x00\x00\x10\x0e\x00\x00\x01' # Stoppt Supply-Fan für 1h CMD_START_SUPPLY_FAN = b'\x85\x15\x07\x01' CMD_VENTMODE_STOP_EXHAUST_FAN_1 = b'\x84\x15\x06\x01\x00\x00\x00\x00\x10\x0e\x00\x00\x01' # Stoppt Exhaust-Fan für 1h CMD_START_EXHAUST_FAN = b'\x85\x15\x06\x01' CMD_TEMPPROF_NORMAL = b'\x84\x15\x03\x01\x00\x00\x00\x00\xff\xff\xff\xff\x00' CMD_TEMPPROF_COOL = b'\x84\x15\x03\x01\x00\x00\x00\x00\xff\xff\xff\xff\x01' CMD_TEMPPROF_WARM = b'\x84\x15\x03\x01\x00\x00\x00\x00\xff\xff\xff\xff\x02' CMD_BYPASS_ON = b'\x84\x15\x02\x01\x00\x00\x00\x00\x10\x0e\x00\x00\x01' CMD_BYPASS_OFF = b'\x84\x15\x02\x01\x00\x00\x00\x00\x10\x0e\x00\x00\x02' CMD_BYPASS_AUTO = b'\x85\x15\x02\x01' CMD_SENSOR_TEMP_OFF = b'\x03\x1d\x01\x04\x00' CMD_SENSOR_TEMP_AUTO = b'\x03\x1d\x01\x04\x01' CMD_SENSOR_TEMP_ON = b'\x03\x1d\x01\x04\x02' CMD_SENSOR_HUMC_OFF = b'\x03\x1d\x01\x06\x00' CMD_SENSOR_HUMC_AUTO = b'\x03\x1d\x01\x06\x01' CMD_SENSOR_HUMC_ON = b'\x03\x1d\x01\x06\x02' CMD_SENSOR_HUMP_OFF = b'\x03\x1d\x01\x07\x00' CMD_SENSOR_HUMP_AUTO = b'\x03\x1d\x01\x07\x01' CMD_SENSOR_HUMP_ON = b'\x03\x1d\x01\x07\x02' CMD_BOOST_MODE_1 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x08\x07\x00\x00\x03' # Partymode 0.5h CMD_BOOST_MODE_2 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x10\x0E\x00\x00\x03' # Partymode 1h CMD_BOOST_MODE_3 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x20\x1C\x00\x00\x03' # Partymode 2h CMD_BOOST_MODE_4 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x30\x2A\x00\x00\x03' # Partymode 3h CMD_BOOST_MODE_5 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x40\x38\x00\x00\x03' # Partymode 4h CMD_BOOST_MODE_6 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x50\x46\x00\x00\x03' # Partymode 5h CMD_BOOST_MODE_7 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x60\x54\x00\x00\x03' # Partymode 6h CMD_BOOST_MODE_8 = b'\x84\x15\x01\x06\x00\x00\x00\x00\x80\x70\x00\x00\x03' # Partymode 8h CMD_BOOST_MODE_0 = b'\x85\x15\x01\x06' # Sensor locations SENSOR_AWAY = 16 SENSOR_OPERATING_MODE_BIS = 49 SENSOR_OPERATING_MODE = 56 SENSOR_FAN_SPEED_MODE = 65 SENSOR_BYPASS_ACTIVATIONSTATE = 66 SENSOR_PROFILE_TEMPERATURE = 67 SENSOR_FAN_MODE_SUPPLY = 70 SENSOR_FAN_MODE_EXHAUST = 71 SENSOR_FAN_NEXT_CHANGE = 81 SENSOR_BYPASS_NEXT_CHANGE = 82 SENSOR_SUPPLY_NEXT_CHANGE = 86 SENSOR_EXHAUST_NEXT_CHANGE = 87 SENSOR_FAN_EXHAUST_DUTY = 117 SENSOR_FAN_SUPPLY_DUTY = 118 SENSOR_FAN_EXHAUST_FLOW = 119 SENSOR_FAN_SUPPLY_FLOW = 120 SENSOR_FAN_EXHAUST_SPEED = 121 SENSOR_FAN_SUPPLY_SPEED = 122 SENSOR_POWER_CURRENT = 128 SENSOR_POWER_TOTAL_YEAR = 129 SENSOR_POWER_TOTAL = 130 SENSOR_PREHEATER_POWER_TOTAL_YEAR = 144 SENSOR_PREHEATER_POWER_TOTAL = 145 SENSOR_PREHEATER_POWER_CURRENT = 146 SENSOR_SETTING_RF_PAIRING = 176 SENSOR_DAYS_TO_REPLACE_FILTER = 192 SENSOR_CURRENT_RMOT = 209 SENSOR_HEATING_SEASON = 210 SENSOR_COOLING_SEASON = 211 SENSOR_TARGET_TEMPERATURE = 212 SENSOR_AVOIDED_HEATING_CURRENT = 213 SENSOR_AVOIDED_HEATING_TOTAL_YEAR = 214 SENSOR_AVOIDED_HEATING_TOTAL = 215 SENSOR_AVOIDED_COOLING_CURRENT = 216 SENSOR_AVOIDED_COOLING_TOTAL_YEAR = 217 SENSOR_AVOIDED_COOLING_TOTAL = 218 SENSOR_AVOIDED_COOLING_CURRENT_Target = 219 SENSOR_TEMPERATURE_SUPPLY = 221 SENSOR_COMFORTCONTROL_MODE = 225 SENSOR_BYPASS_STATE = 227 SENSOR_FROSTPROTECTION_UNBALANCE = 228 SENSOR_TEMPERATURE_EXTRACT = 274 SENSOR_TEMPERATURE_EXHAUST = 275 SENSOR_TEMPERATURE_OUTDOOR = 276 SENSOR_TEMPERATURE_AFTER_PREHEATER = 277 SENSOR_HUMIDITY_EXTRACT = 290 SENSOR_HUMIDITY_EXHAUST = 291 SENSOR_HUMIDITY_OUTDOOR = 292 SENSOR_HUMIDITY_AFTER_PREHEATER = 293 SENSOR_HUMIDITY_SUPPLY = 294 michaelarnauts-comfoconnect-2545700/pycomfoconnect/error.py000066400000000000000000000020401434242167600241130ustar00rootroot00000000000000class PyComfoConnectError(Exception): """ Base error for PyComfoConnect """ pass class PyComfoConnectBadRequest(PyComfoConnectError): """ An error occured because the request was invalid. """ pass class PyComfoConnectInternalError(PyComfoConnectError): """ An error occured because something went wrong inside the bridge. """ pass class PyComfoConnectNotReachable(PyComfoConnectError): """ An error occured because the bridge could not reach the ventilation unit. """ pass class PyComfoConnectOtherSession(PyComfoConnectError): """ An error occured because the bridge is already connected to a different device. """ def __init__(self, devicename): self.devicename = devicename class PyComfoConnectNotAllowed(PyComfoConnectError): """ An error occured because you have not authenticated yet. """ pass class PyComfoConnectNoResources(PyComfoConnectError): pass class PyComfoConnectNotExist(PyComfoConnectError): pass class PyComfoConnectRmiError(PyComfoConnectError): pass michaelarnauts-comfoconnect-2545700/pycomfoconnect/message.py000066400000000000000000000277131434242167600244240ustar00rootroot00000000000000import struct from .error import * from .zehnder_pb2 import * class Message(object): class_to_type = { SetAddressRequest: GatewayOperation.SetAddressRequestType, RegisterAppRequest: GatewayOperation.RegisterAppRequestType, StartSessionRequest: GatewayOperation.StartSessionRequestType, CloseSessionRequest: GatewayOperation.CloseSessionRequestType, ListRegisteredAppsRequest: GatewayOperation.ListRegisteredAppsRequestType, DeregisterAppRequest: GatewayOperation.DeregisterAppRequestType, ChangePinRequest: GatewayOperation.ChangePinRequestType, GetRemoteAccessIdRequest: GatewayOperation.GetRemoteAccessIdRequestType, SetRemoteAccessIdRequest: GatewayOperation.SetRemoteAccessIdRequestType, GetSupportIdRequest: GatewayOperation.GetSupportIdRequestType, SetSupportIdRequest: GatewayOperation.SetSupportIdRequestType, GetWebIdRequest: GatewayOperation.GetWebIdRequestType, SetWebIdRequest: GatewayOperation.SetWebIdRequestType, SetPushIdRequest: GatewayOperation.SetPushIdRequestType, DebugRequest: GatewayOperation.DebugRequestType, UpgradeRequest: GatewayOperation.UpgradeRequestType, SetDeviceSettingsRequest: GatewayOperation.SetDeviceSettingsRequestType, VersionRequest: GatewayOperation.VersionRequestType, SetAddressConfirm: GatewayOperation.SetAddressConfirmType, RegisterAppConfirm: GatewayOperation.RegisterAppConfirmType, StartSessionConfirm: GatewayOperation.StartSessionConfirmType, CloseSessionConfirm: GatewayOperation.CloseSessionConfirmType, ListRegisteredAppsConfirm: GatewayOperation.ListRegisteredAppsConfirmType, DeregisterAppConfirm: GatewayOperation.DeregisterAppConfirmType, ChangePinConfirm: GatewayOperation.ChangePinConfirmType, GetRemoteAccessIdConfirm: GatewayOperation.GetRemoteAccessIdConfirmType, SetRemoteAccessIdConfirm: GatewayOperation.SetRemoteAccessIdConfirmType, GetSupportIdConfirm: GatewayOperation.GetSupportIdConfirmType, SetSupportIdConfirm: GatewayOperation.SetSupportIdConfirmType, GetWebIdConfirm: GatewayOperation.GetWebIdConfirmType, SetWebIdConfirm: GatewayOperation.SetWebIdConfirmType, SetPushIdConfirm: GatewayOperation.SetPushIdConfirmType, DebugConfirm: GatewayOperation.DebugConfirmType, UpgradeConfirm: GatewayOperation.UpgradeConfirmType, SetDeviceSettingsConfirm: GatewayOperation.SetDeviceSettingsConfirmType, VersionConfirm: GatewayOperation.VersionConfirmType, GatewayNotification: GatewayOperation.GatewayNotificationType, KeepAlive: GatewayOperation.KeepAliveType, FactoryReset: GatewayOperation.FactoryResetType, CnTimeRequest: GatewayOperation.CnTimeRequestType, CnTimeConfirm: GatewayOperation.CnTimeConfirmType, CnNodeRequest: GatewayOperation.CnNodeRequestType, CnNodeNotification: GatewayOperation.CnNodeNotificationType, CnRmiRequest: GatewayOperation.CnRmiRequestType, CnRmiResponse: GatewayOperation.CnRmiResponseType, CnRmiAsyncRequest: GatewayOperation.CnRmiAsyncRequestType, CnRmiAsyncConfirm: GatewayOperation.CnRmiAsyncConfirmType, CnRmiAsyncResponse: GatewayOperation.CnRmiAsyncResponseType, CnRpdoRequest: GatewayOperation.CnRpdoRequestType, CnRpdoConfirm: GatewayOperation.CnRpdoConfirmType, CnRpdoNotification: GatewayOperation.CnRpdoNotificationType, CnAlarmNotification: GatewayOperation.CnAlarmNotificationType, CnFupReadRegisterRequest: GatewayOperation.CnFupReadRegisterRequestType, CnFupReadRegisterConfirm: GatewayOperation.CnFupReadRegisterConfirmType, CnFupProgramBeginRequest: GatewayOperation.CnFupProgramBeginRequestType, CnFupProgramBeginConfirm: GatewayOperation.CnFupProgramBeginConfirmType, CnFupProgramRequest: GatewayOperation.CnFupProgramRequestType, CnFupProgramConfirm: GatewayOperation.CnFupProgramConfirmType, CnFupProgramEndRequest: GatewayOperation.CnFupProgramEndRequestType, CnFupProgramEndConfirm: GatewayOperation.CnFupProgramEndConfirmType, CnFupReadRequest: GatewayOperation.CnFupReadRequestType, CnFupReadConfirm: GatewayOperation.CnFupReadConfirmType, CnFupResetRequest: GatewayOperation.CnFupResetRequestType, CnFupResetConfirm: GatewayOperation.CnFupResetConfirmType, } class_to_confirm = { SetAddressRequest: SetAddressConfirm, RegisterAppRequest: RegisterAppConfirm, StartSessionRequest: StartSessionConfirm, CloseSessionRequest: CloseSessionConfirm, ListRegisteredAppsRequest: ListRegisteredAppsConfirm, DeregisterAppRequest: DeregisterAppConfirm, ChangePinRequest: ChangePinConfirm, GetRemoteAccessIdRequest: GetRemoteAccessIdConfirm, SetRemoteAccessIdRequest: SetRemoteAccessIdConfirm, GetSupportIdRequest: GetSupportIdConfirm, SetSupportIdRequest: SetSupportIdConfirm, GetWebIdRequest: GetWebIdConfirm, SetWebIdRequest: SetWebIdConfirm, SetPushIdRequest: SetPushIdConfirm, DebugRequest: DebugConfirm, UpgradeRequest: UpgradeConfirm, SetDeviceSettingsRequest: SetDeviceSettingsConfirm, VersionRequest: VersionConfirm, CnTimeRequest: CnTimeConfirm, CnRmiRequest: CnRmiResponse, CnRmiAsyncRequest: CnRmiAsyncConfirm, CnRpdoRequest: CnRpdoConfirm, CnFupReadRegisterRequest: CnFupReadRegisterConfirm, CnFupProgramBeginRequest: CnFupProgramBeginConfirm, CnFupProgramRequest: CnFupProgramConfirm, CnFupProgramEndRequest: CnFupProgramEndConfirm, CnFupReadRequest: CnFupReadConfirm, CnFupResetRequest: CnFupResetConfirm, } request_type_to_class_mapping = { GatewayOperation.SetAddressRequestType: SetAddressRequest, GatewayOperation.RegisterAppRequestType: RegisterAppRequest, GatewayOperation.StartSessionRequestType: StartSessionRequest, GatewayOperation.CloseSessionRequestType: CloseSessionRequest, GatewayOperation.ListRegisteredAppsRequestType: ListRegisteredAppsRequest, GatewayOperation.DeregisterAppRequestType: DeregisterAppRequest, GatewayOperation.ChangePinRequestType: ChangePinRequest, GatewayOperation.GetRemoteAccessIdRequestType: GetRemoteAccessIdRequest, GatewayOperation.SetRemoteAccessIdRequestType: SetRemoteAccessIdRequest, GatewayOperation.GetSupportIdRequestType: GetSupportIdRequest, GatewayOperation.SetSupportIdRequestType: SetSupportIdRequest, GatewayOperation.GetWebIdRequestType: GetWebIdRequest, GatewayOperation.SetWebIdRequestType: SetWebIdRequest, GatewayOperation.SetPushIdRequestType: SetPushIdRequest, GatewayOperation.DebugRequestType: DebugRequest, GatewayOperation.UpgradeRequestType: UpgradeRequest, GatewayOperation.SetDeviceSettingsRequestType: SetDeviceSettingsRequest, GatewayOperation.VersionRequestType: VersionRequest, GatewayOperation.SetAddressConfirmType: SetAddressConfirm, GatewayOperation.RegisterAppConfirmType: RegisterAppConfirm, GatewayOperation.StartSessionConfirmType: StartSessionConfirm, GatewayOperation.CloseSessionConfirmType: CloseSessionConfirm, GatewayOperation.ListRegisteredAppsConfirmType: ListRegisteredAppsConfirm, GatewayOperation.DeregisterAppConfirmType: DeregisterAppConfirm, GatewayOperation.ChangePinConfirmType: ChangePinConfirm, GatewayOperation.GetRemoteAccessIdConfirmType: GetRemoteAccessIdConfirm, GatewayOperation.SetRemoteAccessIdConfirmType: SetRemoteAccessIdConfirm, GatewayOperation.GetSupportIdConfirmType: GetSupportIdConfirm, GatewayOperation.SetSupportIdConfirmType: SetSupportIdConfirm, GatewayOperation.GetWebIdConfirmType: GetWebIdConfirm, GatewayOperation.SetWebIdConfirmType: SetWebIdConfirm, GatewayOperation.SetPushIdConfirmType: SetPushIdConfirm, GatewayOperation.DebugConfirmType: DebugConfirm, GatewayOperation.UpgradeConfirmType: UpgradeConfirm, GatewayOperation.SetDeviceSettingsConfirmType: SetDeviceSettingsConfirm, GatewayOperation.VersionConfirmType: VersionConfirm, GatewayOperation.GatewayNotificationType: GatewayNotification, GatewayOperation.KeepAliveType: KeepAlive, GatewayOperation.FactoryResetType: FactoryReset, GatewayOperation.CnTimeRequestType: CnTimeRequest, GatewayOperation.CnTimeConfirmType: CnTimeConfirm, GatewayOperation.CnNodeRequestType: CnNodeRequest, GatewayOperation.CnNodeNotificationType: CnNodeNotification, GatewayOperation.CnRmiRequestType: CnRmiRequest, GatewayOperation.CnRmiResponseType: CnRmiResponse, GatewayOperation.CnRmiAsyncRequestType: CnRmiAsyncRequest, GatewayOperation.CnRmiAsyncConfirmType: CnRmiAsyncConfirm, GatewayOperation.CnRmiAsyncResponseType: CnRmiAsyncResponse, GatewayOperation.CnRpdoRequestType: CnRpdoRequest, GatewayOperation.CnRpdoConfirmType: CnRpdoConfirm, GatewayOperation.CnRpdoNotificationType: CnRpdoNotification, GatewayOperation.CnAlarmNotificationType: CnAlarmNotification, GatewayOperation.CnFupReadRegisterRequestType: CnFupReadRegisterRequest, GatewayOperation.CnFupReadRegisterConfirmType: CnFupReadRegisterConfirm, GatewayOperation.CnFupProgramBeginRequestType: CnFupProgramBeginRequest, GatewayOperation.CnFupProgramBeginConfirmType: CnFupProgramBeginConfirm, GatewayOperation.CnFupProgramRequestType: CnFupProgramRequest, GatewayOperation.CnFupProgramConfirmType: CnFupProgramConfirm, GatewayOperation.CnFupProgramEndRequestType: CnFupProgramEndRequest, GatewayOperation.CnFupProgramEndConfirmType: CnFupProgramEndConfirm, GatewayOperation.CnFupReadRequestType: CnFupReadRequest, GatewayOperation.CnFupReadConfirmType: CnFupReadConfirm, GatewayOperation.CnFupResetRequestType: CnFupResetRequest, GatewayOperation.CnFupResetConfirmType: CnFupResetConfirm, } def __init__(self, cmd, msg, src, dst): self.cmd = cmd self.msg = msg self.src = src self.dst = dst @classmethod def create(cls, src, dst, command, cmd_params=None, msg_params=None): cmd = GatewayOperation() cmd.type = cls.class_to_type[command] if cmd_params is not None: for param in cmd_params: if cmd_params[param] is not None: setattr(cmd, param, cmd_params[param]) msg = command() if msg_params is not None: for param in msg_params: if msg_params[param] is not None: setattr(msg, param, msg_params[param]) return Message(cmd, msg, src, dst) def __str__(self): return "%s -> %s: %s %s\n%s\n%s" % ( self.src.hex(), self.dst.hex(), self.cmd.SerializeToString().hex(), self.msg.SerializeToString().hex(), self.cmd, self.msg, ) def encode(self): cmd_buf = self.cmd.SerializeToString() msg_buf = self.msg.SerializeToString() cmd_len_buf = struct.pack('>H', len(cmd_buf)) msg_len_buf = struct.pack('>L', 16 + 16 + 2 + len(cmd_buf) + len(msg_buf)) return msg_len_buf + self.src + self.dst + cmd_len_buf + cmd_buf + msg_buf @classmethod def decode(cls, packet): src_buf = packet[4:20] dst_buf = packet[20:36] cmd_len = struct.unpack('>H', packet[36:38])[0] cmd_buf = packet[38:38 + cmd_len] msg_buf = packet[38 + cmd_len:] # Parse command cmd = GatewayOperation() cmd.ParseFromString(cmd_buf) # Parse message cmd_type = cls.request_type_to_class_mapping.get(cmd.type) msg = cmd_type() msg.ParseFromString(msg_buf) return Message(cmd, msg, src_buf, dst_buf) michaelarnauts-comfoconnect-2545700/pycomfoconnect/zehnder_pb2.py000066400000000000000000000417711434242167600252020ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: zehnder.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rzehnder.proto\"\x80\x01\n\x12\x44iscoveryOperation\x12\x33\n\x14searchGatewayRequest\x18\x01 \x01(\x0b\x32\x15.SearchGatewayRequest\x12\x35\n\x15searchGatewayResponse\x18\x02 \x01(\x0b\x32\x16.SearchGatewayResponse\"\x16\n\x14SearchGatewayRequest\"I\n\x15SearchGatewayResponse\x12\x11\n\tipaddress\x18\x01 \x02(\t\x12\x0c\n\x04uuid\x18\x02 \x02(\x0c\x12\x0f\n\x07version\x18\x03 \x02(\r\"\xde\x10\n\x10GatewayOperation\x12-\n\x04type\x18\x01 \x01(\x0e\x32\x1f.GatewayOperation.OperationType\x12/\n\x06result\x18\x02 \x01(\x0e\x32\x1f.GatewayOperation.GatewayResult\x12\x19\n\x11resultDescription\x18\x03 \x01(\t\x12\x11\n\treference\x18\x04 \x01(\r\"\x95\x0e\n\rOperationType\x12\x0f\n\x0bNoOperation\x10\x00\x12\x19\n\x15SetAddressRequestType\x10\x01\x12\x1a\n\x16RegisterAppRequestType\x10\x02\x12\x1b\n\x17StartSessionRequestType\x10\x03\x12\x1b\n\x17\x43loseSessionRequestType\x10\x04\x12!\n\x1dListRegisteredAppsRequestType\x10\x05\x12\x1c\n\x18\x44\x65registerAppRequestType\x10\x06\x12\x18\n\x14\x43hangePinRequestType\x10\x07\x12 \n\x1cGetRemoteAccessIdRequestType\x10\x08\x12 \n\x1cSetRemoteAccessIdRequestType\x10\t\x12\x1b\n\x17GetSupportIdRequestType\x10\n\x12\x1b\n\x17SetSupportIdRequestType\x10\x0b\x12\x17\n\x13GetWebIdRequestType\x10\x0c\x12\x17\n\x13SetWebIdRequestType\x10\r\x12\x18\n\x14SetPushIdRequestType\x10\x0e\x12\x14\n\x10\x44\x65\x62ugRequestType\x10\x0f\x12\x16\n\x12UpgradeRequestType\x10\x10\x12 \n\x1cSetDeviceSettingsRequestType\x10\x11\x12\x16\n\x12VersionRequestType\x10\x12\x12\x19\n\x15SetAddressConfirmType\x10\x33\x12\x1a\n\x16RegisterAppConfirmType\x10\x34\x12\x1b\n\x17StartSessionConfirmType\x10\x35\x12\x1b\n\x17\x43loseSessionConfirmType\x10\x36\x12!\n\x1dListRegisteredAppsConfirmType\x10\x37\x12\x1c\n\x18\x44\x65registerAppConfirmType\x10\x38\x12\x18\n\x14\x43hangePinConfirmType\x10\x39\x12 \n\x1cGetRemoteAccessIdConfirmType\x10:\x12 \n\x1cSetRemoteAccessIdConfirmType\x10;\x12\x1b\n\x17GetSupportIdConfirmType\x10<\x12\x1b\n\x17SetSupportIdConfirmType\x10=\x12\x17\n\x13GetWebIdConfirmType\x10>\x12\x17\n\x13SetWebIdConfirmType\x10?\x12\x18\n\x14SetPushIdConfirmType\x10@\x12\x14\n\x10\x44\x65\x62ugConfirmType\x10\x41\x12\x16\n\x12UpgradeConfirmType\x10\x42\x12 \n\x1cSetDeviceSettingsConfirmType\x10\x43\x12\x16\n\x12VersionConfirmType\x10\x44\x12\x1b\n\x17GatewayNotificationType\x10\x64\x12\x11\n\rKeepAliveType\x10\x65\x12\x14\n\x10\x46\x61\x63toryResetType\x10\x66\x12\x15\n\x11\x43nTimeRequestType\x10\x1e\x12\x15\n\x11\x43nTimeConfirmType\x10\x1f\x12\x15\n\x11\x43nNodeRequestType\x10*\x12\x1a\n\x16\x43nNodeNotificationType\x10 \x12\x14\n\x10\x43nRmiRequestType\x10!\x12\x15\n\x11\x43nRmiResponseType\x10\"\x12\x19\n\x15\x43nRmiAsyncRequestType\x10#\x12\x19\n\x15\x43nRmiAsyncConfirmType\x10$\x12\x1a\n\x16\x43nRmiAsyncResponseType\x10%\x12\x15\n\x11\x43nRpdoRequestType\x10&\x12\x15\n\x11\x43nRpdoConfirmType\x10\'\x12\x1a\n\x16\x43nRpdoNotificationType\x10(\x12\x1b\n\x17\x43nAlarmNotificationType\x10)\x12 \n\x1c\x43nFupReadRegisterRequestType\x10\x46\x12 \n\x1c\x43nFupReadRegisterConfirmType\x10G\x12 \n\x1c\x43nFupProgramBeginRequestType\x10H\x12 \n\x1c\x43nFupProgramBeginConfirmType\x10I\x12\x1b\n\x17\x43nFupProgramRequestType\x10J\x12\x1b\n\x17\x43nFupProgramConfirmType\x10K\x12\x1e\n\x1a\x43nFupProgramEndRequestType\x10L\x12\x1e\n\x1a\x43nFupProgramEndConfirmType\x10M\x12\x18\n\x14\x43nFupReadRequestType\x10N\x12\x18\n\x14\x43nFupReadConfirmType\x10O\x12\x19\n\x15\x43nFupResetRequestType\x10P\x12\x19\n\x15\x43nFupResetConfirmType\x10Q\"\xa3\x01\n\rGatewayResult\x12\x06\n\x02OK\x10\x00\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10\x01\x12\x12\n\x0eINTERNAL_ERROR\x10\x02\x12\x11\n\rNOT_REACHABLE\x10\x03\x12\x11\n\rOTHER_SESSION\x10\x04\x12\x0f\n\x0bNOT_ALLOWED\x10\x05\x12\x10\n\x0cNO_RESOURCES\x10\x06\x12\r\n\tNOT_EXIST\x10\x07\x12\r\n\tRMI_ERROR\x10\x08\"M\n\x13GatewayNotification\x12\x11\n\tpushUUIDs\x18\x01 \x03(\x0c\x12#\n\x05\x61larm\x18\x02 \x01(\x0b\x32\x14.CnAlarmNotification\"\x0b\n\tKeepAlive\" \n\x0c\x46\x61\x63toryReset\x12\x10\n\x08resetKey\x18\x01 \x02(\x0c\"D\n\x18SetDeviceSettingsRequest\x12\x12\n\nmacAddress\x18\x01 \x02(\x0c\x12\x14\n\x0cserialNumber\x18\x02 \x02(\t\"\x1a\n\x18SetDeviceSettingsConfirm\"!\n\x11SetAddressRequest\x12\x0c\n\x04uuid\x18\x01 \x02(\x0c\"\x13\n\x11SetAddressConfirm\"C\n\x12RegisterAppRequest\x12\x0c\n\x04uuid\x18\x01 \x02(\x0c\x12\x0b\n\x03pin\x18\x02 \x02(\r\x12\x12\n\ndevicename\x18\x03 \x02(\t\"\x14\n\x12RegisterAppConfirm\"\'\n\x13StartSessionRequest\x12\x10\n\x08takeover\x18\x01 \x01(\x08\":\n\x13StartSessionConfirm\x12\x12\n\ndevicename\x18\x01 \x01(\t\x12\x0f\n\x07resumed\x18\x02 \x01(\x08\"\x15\n\x13\x43loseSessionRequest\"\x15\n\x13\x43loseSessionConfirm\"\x1b\n\x19ListRegisteredAppsRequest\"r\n\x19ListRegisteredAppsConfirm\x12,\n\x04\x61pps\x18\x01 \x03(\x0b\x32\x1e.ListRegisteredAppsConfirm.App\x1a\'\n\x03\x41pp\x12\x0c\n\x04uuid\x18\x01 \x02(\x0c\x12\x12\n\ndevicename\x18\x02 \x02(\t\"$\n\x14\x44\x65registerAppRequest\x12\x0c\n\x04uuid\x18\x01 \x02(\x0c\"\x16\n\x14\x44\x65registerAppConfirm\"2\n\x10\x43hangePinRequest\x12\x0e\n\x06oldpin\x18\x01 \x02(\r\x12\x0e\n\x06newpin\x18\x02 \x02(\r\"\x12\n\x10\x43hangePinConfirm\"\x1a\n\x18GetRemoteAccessIdRequest\"(\n\x18GetRemoteAccessIdConfirm\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\"(\n\x18SetRemoteAccessIdRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\"\x1a\n\x18SetRemoteAccessIdConfirm\"\x15\n\x13GetSupportIdRequest\":\n\x13GetSupportIdConfirm\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\x12\x15\n\rremainingTime\x18\x02 \x01(\r\"6\n\x13SetSupportIdRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\x12\x11\n\tvalidTime\x18\x02 \x01(\r\"\x15\n\x13SetSupportIdConfirm\"\x11\n\x0fGetWebIdRequest\"\x1f\n\x0fGetWebIdConfirm\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\"\x1f\n\x0fSetWebIdRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\"\x11\n\x0fSetWebIdConfirm\" \n\x10SetPushIdRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\x0c\"\x12\n\x10SetPushIdConfirm\"\xc0\x01\n\x0eUpgradeRequest\x12\x36\n\x07\x63ommand\x18\x01 \x01(\x0e\x32%.UpgradeRequest.UpgradeRequestCommand\x12\r\n\x05\x63hunk\x18\x02 \x01(\x0c\"g\n\x15UpgradeRequestCommand\x12\x11\n\rUPGRADE_START\x10\x00\x12\x14\n\x10UPGRADE_CONTINUE\x10\x01\x12\x12\n\x0eUPGRADE_FINISH\x10\x02\x12\x11\n\rUPGRADE_ABORT\x10\x03\"\x10\n\x0eUpgradeConfirm\"\xba\x03\n\x0c\x44\x65\x62ugRequest\x12\x32\n\x07\x63ommand\x18\x01 \x02(\x0e\x32!.DebugRequest.DebugRequestCommand\x12\x10\n\x08\x61rgument\x18\x02 \x01(\x05\"\xe3\x02\n\x13\x44\x65\x62ugRequestCommand\x12\x0c\n\x08\x44\x42G_ECHO\x10\x00\x12\r\n\tDBG_SLEEP\x10\x01\x12\x14\n\x10\x44\x42G_SESSION_ECHO\x10\x02\x12\x16\n\x12\x44\x42G_PRINT_SETTINGS\x10\x03\x12\r\n\tDBG_ALARM\x10\x04\x12\x0b\n\x07\x44\x42G_LED\x10\x05\x12\x0b\n\x07\x44\x42G_GPI\x10\x06\x12\x0b\n\x07\x44\x42G_GPO\x10\x07\x12\x13\n\x0f\x44\x42G_RS232_WRITE\x10\x08\x12\x12\n\x0e\x44\x42G_RS232_READ\x10\t\x12\x11\n\rDBG_CAN_WRITE\x10\n\x12\x10\n\x0c\x44\x42G_CAN_READ\x10\x0b\x12\x11\n\rDBG_KNX_WRITE\x10\x0c\x12\x10\n\x0c\x44\x42G_KNX_READ\x10\r\x12\x0e\n\nDBG_TOGGLE\x10\x0e\x12\x0e\n\nDBG_REBOOT\x10\x0f\x12\r\n\tDBG_CLOUD\x10\x10\x12\x13\n\x0f\x44\x42G_EEPROM_READ\x10\x11\x12\x14\n\x10\x44\x42G_EEPROM_WRITE\x10\x12\"\x1e\n\x0c\x44\x65\x62ugConfirm\x12\x0e\n\x06result\x18\x01 \x02(\x05\"\x10\n\x0eVersionRequest\"W\n\x0eVersionConfirm\x12\x16\n\x0egatewayVersion\x18\x01 \x02(\r\x12\x14\n\x0cserialNumber\x18\x02 \x02(\t\x12\x17\n\x0f\x63omfoNetVersion\x18\x03 \x02(\r\" \n\rCnTimeRequest\x12\x0f\n\x07setTime\x18\x01 \x01(\r\"$\n\rCnTimeConfirm\x12\x13\n\x0b\x63urrentTime\x18\x01 \x02(\r\"\x0f\n\rCnNodeRequest\"\xcf\x01\n\x12\x43nNodeNotification\x12\x0e\n\x06nodeId\x18\x01 \x02(\r\x12\x14\n\tproductId\x18\x02 \x01(\r:\x01\x30\x12\x0e\n\x06zoneId\x18\x03 \x01(\r\x12.\n\x04mode\x18\x04 \x01(\x0e\x32 .CnNodeNotification.NodeModeType\"S\n\x0cNodeModeType\x12\x0f\n\x0bNODE_LEGACY\x10\x00\x12\x10\n\x0cNODE_OFFLINE\x10\x01\x12\x0f\n\x0bNODE_NORMAL\x10\x02\x12\x0f\n\x0bNODE_UPDATE\x10\x03\"/\n\x0c\x43nRmiRequest\x12\x0e\n\x06nodeId\x18\x01 \x02(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\"3\n\rCnRmiResponse\x12\x11\n\x06result\x18\x01 \x01(\r:\x01\x30\x12\x0f\n\x07message\x18\x02 \x01(\x0c\"4\n\x11\x43nRmiAsyncRequest\x12\x0e\n\x06nodeId\x18\x01 \x02(\r\x12\x0f\n\x07message\x18\x02 \x02(\x0c\"&\n\x11\x43nRmiAsyncConfirm\x12\x11\n\x06result\x18\x01 \x01(\r:\x01\x30\"8\n\x12\x43nRmiAsyncResponse\x12\x11\n\x06result\x18\x01 \x01(\r:\x01\x30\x12\x0f\n\x07message\x18\x02 \x01(\x0c\"[\n\rCnRpdoRequest\x12\x0c\n\x04pdid\x18\x01 \x02(\r\x12\x11\n\x04zone\x18\x02 \x01(\r:\x03\x32\x35\x35\x12\x0c\n\x04type\x18\x03 \x01(\r\x12\x1b\n\x07timeout\x18\x04 \x01(\r:\n4294967295\"\x0f\n\rCnRpdoConfirm\"0\n\x12\x43nRpdoNotification\x12\x0c\n\x04pdid\x18\x01 \x02(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x02(\x0c\"\xaf\x01\n\x13\x43nAlarmNotification\x12\x0c\n\x04zone\x18\x01 \x01(\r\x12\x11\n\tproductId\x18\x02 \x01(\r\x12\x16\n\x0eproductVariant\x18\x03 \x01(\r\x12\x14\n\x0cserialNumber\x18\x04 \x01(\t\x12\x18\n\x10swProgramVersion\x18\x05 \x01(\r\x12\x0e\n\x06\x65rrors\x18\x06 \x01(\x0c\x12\x0f\n\x07\x65rrorId\x18\x07 \x01(\r\x12\x0e\n\x06nodeId\x18\x08 \x01(\r\"K\n\x18\x43nFupReadRegisterRequest\x12\x0c\n\x04node\x18\x01 \x02(\r\x12\x12\n\nregisterId\x18\x02 \x02(\r\x12\r\n\x05index\x18\x03 \x01(\r\")\n\x18\x43nFupReadRegisterConfirm\x12\r\n\x05value\x18\x01 \x02(\r\":\n\x18\x43nFupProgramBeginRequest\x12\x0c\n\x04node\x18\x01 \x03(\r\x12\x10\n\x05\x62lock\x18\x02 \x01(\r:\x01\x30\"\x1a\n\x18\x43nFupProgramBeginConfirm\"$\n\x13\x43nFupProgramRequest\x12\r\n\x05\x63hunk\x18\x01 \x02(\x0c\"\x15\n\x13\x43nFupProgramConfirm\"\x18\n\x16\x43nFupProgramEndRequest\"\x18\n\x16\x43nFupProgramEndConfirm\"2\n\x10\x43nFupReadRequest\x12\x0c\n\x04node\x18\x01 \x02(\r\x12\x10\n\x05\x62lock\x18\x02 \x01(\r:\x01\x30\"6\n\x10\x43nFupReadConfirm\x12\r\n\x05\x63hunk\x18\x01 \x01(\x0c\x12\x13\n\x04last\x18\x02 \x01(\x08:\x05\x66\x61lse\"!\n\x11\x43nFupResetRequest\x12\x0c\n\x04node\x18\x01 \x02(\r\"\x13\n\x11\x43nFupResetConfirm') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'zehnder_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _DISCOVERYOPERATION._serialized_start=18 _DISCOVERYOPERATION._serialized_end=146 _SEARCHGATEWAYREQUEST._serialized_start=148 _SEARCHGATEWAYREQUEST._serialized_end=170 _SEARCHGATEWAYRESPONSE._serialized_start=172 _SEARCHGATEWAYRESPONSE._serialized_end=245 _GATEWAYOPERATION._serialized_start=248 _GATEWAYOPERATION._serialized_end=2390 _GATEWAYOPERATION_OPERATIONTYPE._serialized_start=411 _GATEWAYOPERATION_OPERATIONTYPE._serialized_end=2224 _GATEWAYOPERATION_GATEWAYRESULT._serialized_start=2227 _GATEWAYOPERATION_GATEWAYRESULT._serialized_end=2390 _GATEWAYNOTIFICATION._serialized_start=2392 _GATEWAYNOTIFICATION._serialized_end=2469 _KEEPALIVE._serialized_start=2471 _KEEPALIVE._serialized_end=2482 _FACTORYRESET._serialized_start=2484 _FACTORYRESET._serialized_end=2516 _SETDEVICESETTINGSREQUEST._serialized_start=2518 _SETDEVICESETTINGSREQUEST._serialized_end=2586 _SETDEVICESETTINGSCONFIRM._serialized_start=2588 _SETDEVICESETTINGSCONFIRM._serialized_end=2614 _SETADDRESSREQUEST._serialized_start=2616 _SETADDRESSREQUEST._serialized_end=2649 _SETADDRESSCONFIRM._serialized_start=2651 _SETADDRESSCONFIRM._serialized_end=2670 _REGISTERAPPREQUEST._serialized_start=2672 _REGISTERAPPREQUEST._serialized_end=2739 _REGISTERAPPCONFIRM._serialized_start=2741 _REGISTERAPPCONFIRM._serialized_end=2761 _STARTSESSIONREQUEST._serialized_start=2763 _STARTSESSIONREQUEST._serialized_end=2802 _STARTSESSIONCONFIRM._serialized_start=2804 _STARTSESSIONCONFIRM._serialized_end=2862 _CLOSESESSIONREQUEST._serialized_start=2864 _CLOSESESSIONREQUEST._serialized_end=2885 _CLOSESESSIONCONFIRM._serialized_start=2887 _CLOSESESSIONCONFIRM._serialized_end=2908 _LISTREGISTEREDAPPSREQUEST._serialized_start=2910 _LISTREGISTEREDAPPSREQUEST._serialized_end=2937 _LISTREGISTEREDAPPSCONFIRM._serialized_start=2939 _LISTREGISTEREDAPPSCONFIRM._serialized_end=3053 _LISTREGISTEREDAPPSCONFIRM_APP._serialized_start=3014 _LISTREGISTEREDAPPSCONFIRM_APP._serialized_end=3053 _DEREGISTERAPPREQUEST._serialized_start=3055 _DEREGISTERAPPREQUEST._serialized_end=3091 _DEREGISTERAPPCONFIRM._serialized_start=3093 _DEREGISTERAPPCONFIRM._serialized_end=3115 _CHANGEPINREQUEST._serialized_start=3117 _CHANGEPINREQUEST._serialized_end=3167 _CHANGEPINCONFIRM._serialized_start=3169 _CHANGEPINCONFIRM._serialized_end=3187 _GETREMOTEACCESSIDREQUEST._serialized_start=3189 _GETREMOTEACCESSIDREQUEST._serialized_end=3215 _GETREMOTEACCESSIDCONFIRM._serialized_start=3217 _GETREMOTEACCESSIDCONFIRM._serialized_end=3257 _SETREMOTEACCESSIDREQUEST._serialized_start=3259 _SETREMOTEACCESSIDREQUEST._serialized_end=3299 _SETREMOTEACCESSIDCONFIRM._serialized_start=3301 _SETREMOTEACCESSIDCONFIRM._serialized_end=3327 _GETSUPPORTIDREQUEST._serialized_start=3329 _GETSUPPORTIDREQUEST._serialized_end=3350 _GETSUPPORTIDCONFIRM._serialized_start=3352 _GETSUPPORTIDCONFIRM._serialized_end=3410 _SETSUPPORTIDREQUEST._serialized_start=3412 _SETSUPPORTIDREQUEST._serialized_end=3466 _SETSUPPORTIDCONFIRM._serialized_start=3468 _SETSUPPORTIDCONFIRM._serialized_end=3489 _GETWEBIDREQUEST._serialized_start=3491 _GETWEBIDREQUEST._serialized_end=3508 _GETWEBIDCONFIRM._serialized_start=3510 _GETWEBIDCONFIRM._serialized_end=3541 _SETWEBIDREQUEST._serialized_start=3543 _SETWEBIDREQUEST._serialized_end=3574 _SETWEBIDCONFIRM._serialized_start=3576 _SETWEBIDCONFIRM._serialized_end=3593 _SETPUSHIDREQUEST._serialized_start=3595 _SETPUSHIDREQUEST._serialized_end=3627 _SETPUSHIDCONFIRM._serialized_start=3629 _SETPUSHIDCONFIRM._serialized_end=3647 _UPGRADEREQUEST._serialized_start=3650 _UPGRADEREQUEST._serialized_end=3842 _UPGRADEREQUEST_UPGRADEREQUESTCOMMAND._serialized_start=3739 _UPGRADEREQUEST_UPGRADEREQUESTCOMMAND._serialized_end=3842 _UPGRADECONFIRM._serialized_start=3844 _UPGRADECONFIRM._serialized_end=3860 _DEBUGREQUEST._serialized_start=3863 _DEBUGREQUEST._serialized_end=4305 _DEBUGREQUEST_DEBUGREQUESTCOMMAND._serialized_start=3950 _DEBUGREQUEST_DEBUGREQUESTCOMMAND._serialized_end=4305 _DEBUGCONFIRM._serialized_start=4307 _DEBUGCONFIRM._serialized_end=4337 _VERSIONREQUEST._serialized_start=4339 _VERSIONREQUEST._serialized_end=4355 _VERSIONCONFIRM._serialized_start=4357 _VERSIONCONFIRM._serialized_end=4444 _CNTIMEREQUEST._serialized_start=4446 _CNTIMEREQUEST._serialized_end=4478 _CNTIMECONFIRM._serialized_start=4480 _CNTIMECONFIRM._serialized_end=4516 _CNNODEREQUEST._serialized_start=4518 _CNNODEREQUEST._serialized_end=4533 _CNNODENOTIFICATION._serialized_start=4536 _CNNODENOTIFICATION._serialized_end=4743 _CNNODENOTIFICATION_NODEMODETYPE._serialized_start=4660 _CNNODENOTIFICATION_NODEMODETYPE._serialized_end=4743 _CNRMIREQUEST._serialized_start=4745 _CNRMIREQUEST._serialized_end=4792 _CNRMIRESPONSE._serialized_start=4794 _CNRMIRESPONSE._serialized_end=4845 _CNRMIASYNCREQUEST._serialized_start=4847 _CNRMIASYNCREQUEST._serialized_end=4899 _CNRMIASYNCCONFIRM._serialized_start=4901 _CNRMIASYNCCONFIRM._serialized_end=4939 _CNRMIASYNCRESPONSE._serialized_start=4941 _CNRMIASYNCRESPONSE._serialized_end=4997 _CNRPDOREQUEST._serialized_start=4999 _CNRPDOREQUEST._serialized_end=5090 _CNRPDOCONFIRM._serialized_start=5092 _CNRPDOCONFIRM._serialized_end=5107 _CNRPDONOTIFICATION._serialized_start=5109 _CNRPDONOTIFICATION._serialized_end=5157 _CNALARMNOTIFICATION._serialized_start=5160 _CNALARMNOTIFICATION._serialized_end=5335 _CNFUPREADREGISTERREQUEST._serialized_start=5337 _CNFUPREADREGISTERREQUEST._serialized_end=5412 _CNFUPREADREGISTERCONFIRM._serialized_start=5414 _CNFUPREADREGISTERCONFIRM._serialized_end=5455 _CNFUPPROGRAMBEGINREQUEST._serialized_start=5457 _CNFUPPROGRAMBEGINREQUEST._serialized_end=5515 _CNFUPPROGRAMBEGINCONFIRM._serialized_start=5517 _CNFUPPROGRAMBEGINCONFIRM._serialized_end=5543 _CNFUPPROGRAMREQUEST._serialized_start=5545 _CNFUPPROGRAMREQUEST._serialized_end=5581 _CNFUPPROGRAMCONFIRM._serialized_start=5583 _CNFUPPROGRAMCONFIRM._serialized_end=5604 _CNFUPPROGRAMENDREQUEST._serialized_start=5606 _CNFUPPROGRAMENDREQUEST._serialized_end=5630 _CNFUPPROGRAMENDCONFIRM._serialized_start=5632 _CNFUPPROGRAMENDCONFIRM._serialized_end=5656 _CNFUPREADREQUEST._serialized_start=5658 _CNFUPREADREQUEST._serialized_end=5708 _CNFUPREADCONFIRM._serialized_start=5710 _CNFUPREADCONFIRM._serialized_end=5764 _CNFUPRESETREQUEST._serialized_start=5766 _CNFUPRESETREQUEST._serialized_end=5799 _CNFUPRESETCONFIRM._serialized_start=5801 _CNFUPRESETCONFIRM._serialized_end=5820 # @@protoc_insertion_point(module_scope) michaelarnauts-comfoconnect-2545700/requirements.txt000066400000000000000000000000201434242167600226420ustar00rootroot00000000000000protobuf>=3.20.3michaelarnauts-comfoconnect-2545700/setup.py000066400000000000000000000016421434242167600211030ustar00rootroot00000000000000# -*- coding: utf-8 -*- from setuptools import setup, find_packages long_description = open('README.rst').read() setup( name='pycomfoconnect', version='0.5.1', license='MIT', url='https://github.com/michaelarnauts/comfoconnect', author='Michaël Arnauts', author_email='michael.arnauts@gmail.com', description='Python interface for the Zehnder ComfoConnect LAN C bridge.', long_description=long_description, packages=find_packages(), zip_safe=False, include_package_data=True, platforms='any', install_requires=list(val.strip() for val in open('requirements.txt')), classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Operating System :: OS Independent', ] )