├── .github └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pylintrc ├── pymelcloud ├── __init__.py ├── ata_device.py ├── atw_device.py ├── client.py ├── const.py ├── device.py └── erv_device.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── samples │ ├── ata_get.json │ ├── ata_guest_get.json │ ├── ata_guest_listdevices.json │ ├── ata_listdevice.json │ ├── atw_1zone_get.json │ ├── atw_1zone_listdevice.json │ ├── atw_2zone_cancool_get.json │ ├── atw_2zone_cancool_listdevice.json │ ├── atw_2zone_get.json │ ├── atw_2zone_listdevice.json │ ├── erv_get.json │ └── erv_listdevice.json ├── test_ata_properties.py ├── test_atw_properties.py ├── test_device.py ├── test_erv_properties.py └── util.py └── tox.ini /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 'on': 3 | push: 4 | branches: [ '*' ] 5 | pull_request: 6 | branches: [ '*' ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.6, 3.7, 3.8, 3.9] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - run: pip3 install tox 16 | - run: tox --skip-missing-interpreters 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | pymelcloud.egg-info/ 5 | .idea 6 | .vscode 7 | .tox 8 | .coverage 9 | .mypy_cache 10 | venv 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v2.3.0 10 | hooks: 11 | - id: flake8 12 | additional_dependencies: [flake8-docstrings] 13 | 14 | - repo: https://github.com/pre-commit/mirrors-isort 15 | rev: v4.3.21 16 | hooks: 17 | - id: isort 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | - Add report based daily energy consumption for all devices. 10 | 11 | ### Changed 12 | - Guard against zero Ata device energy meter reading. Latest firmware returns occasional zeroes breaking energy consumption integrations. 13 | - Round temperatures being set to the nearest temperature_increment using round half up. 14 | 15 | ## [2.11.0] - 2021-10-03 16 | ### Added 17 | - Boiler flow and mixing tank temperatures for Atw devices. 18 | 19 | ### Changed 20 | - Increate ListDevices poll rate to match the sensor poll rates. 21 | 22 | ## [2.10.2] - 2021-09-11 23 | ### Fixed 24 | - Read zone specific flow/return temperatures from the actually working global temperature variable. 25 | 26 | ## [2.10.1] - 2021-05-21 27 | ### Fixed 28 | - Prevent forbidden request to `ListDeviceUnits` on guest devices. 29 | 30 | ## [2.10.0] - 2020-07-14 31 | ### Added 32 | - Add actual fan speed status for Ata device. 33 | 34 | ### Fixed 35 | - Read `wifi_signal` from device conf instead of state. 36 | 37 | ## [2.9.0] - 2020-07-05 38 | ### Added 39 | - Support for Erv devices. 40 | - `wifi_signal` strength and `error_code` for all device types. 41 | 42 | ## [2.8.0] - 2020-05-02 43 | ### Added 44 | - Add Atw zone operation mode control. 45 | 46 | ### Removed 47 | - Remove heat/cool simplification from Atw zone operation modes. 48 | 49 | ## [2.7.0] - 2020-05-01 50 | ### Added 51 | - Add individual get/set for Atw zone flow temperatures in heat and cool modes. 52 | 53 | ## [2.6.0] - 2020-05-01 54 | ### Added 55 | - Add Atw zone flow temperature control. 56 | 57 | ## [2.5.2] - 2020-04-18 58 | ### Fixed 59 | - Use fixed tank temperature minimum. 60 | 61 | ## [2.5.1] - 2020-04-05 62 | ### Fixed 63 | - Map Atw zone `"curve"` mode to `heat` zone operation mode. 64 | 65 | ## [2.5.0] - 2020-04-05 66 | ### Added 67 | - Add `cool` operation mode to `AtwDevice` zones. 68 | - Add zone flow and flow return temperature read. 69 | 70 | ## [2.4.1] - 2020-03-29 71 | ### Fixed 72 | - Fix search for devices assigned to Structure/Areas. 73 | 74 | ## [2.4.0] - 2020-02-20 75 | ### Changed 76 | - Update `User-Agent`. 77 | - Rename Atw status `"off"` to `"idle"`. 78 | 79 | ### Removed 80 | - Remove `holiday_mode` set logic after testing with real devices. 81 | 82 | ## [2.3.0] - 2020-02-17 83 | ### Added 84 | - Add a settable `holiday_mode` property to `AtwDevice`. 85 | 86 | ## [2.2.0] - 2020-02-17 87 | ### Changed 88 | - Return same device types from `get_devices` and `Device` `device_type`. 89 | - Use `MaxTankTemperature` as maximum target tank temperature and 90 | calculate minimum using the previously used `MaxSetTemperature` and 91 | `MinSetTemperature`. 92 | - Remove keyword arguments from `login`. These values are not retained 93 | after the function call. 94 | 95 | ### Fixed 96 | - Make `AtwDevice` `status` consistent in out of sync state. 97 | 98 | ## [2.1.0] - 2020-02-14 99 | ### Added 100 | - Add `temperature_increment` property to `Device`. 101 | - Add `has_energy_consumed_meter` property to `AtaDevice`. 102 | 103 | ### Changed 104 | - Forward AtaDevice `target_temperature_step` calls to 105 | `temperature_increment`. 106 | - Rename ATW zone `state` to `status`. 107 | - Rename ATW `state` to `status`. 108 | - Return heat statuses to `heat_zones` and `heat_water` 109 | 110 | ### Fixed 111 | - Fix `get_devices` type hints. 112 | - Fix `conf_update_interval` and `device_set_debounce` forwarding in `login`. 113 | - Fix detached ATW zone state. 114 | - Convert zone operation mode set to no-op instead of raising `ValueError`. 115 | - Fix ATW zone name fallback. 116 | - Add `None` state guards to ATW zone properties. 117 | 118 | ## [2.0.0] - 2020-02-08 119 | ### Added 120 | - Experimental operation mode logic for ATW zones. 121 | 122 | ### Changed 123 | - Use device type specific set endpoint. 124 | - Return devices in a device type keyed dict from `get_devices` so that 125 | caller does not have to do `isinstance` based filtering. 126 | 127 | ## [1.2.0] - 2020-01-30 128 | ### Changed 129 | - Removed slug from fan speeds. 130 | 131 | ## [1.1.0] - 2020-01-30 132 | ### Changed 133 | - Use underscores instead of dashes in state constants. 134 | 135 | ## [1.0.1] - 2020-01-29 136 | ### Fixed 137 | - Remove invalid assertion from fan speed conversion. 138 | 139 | ## [1.0.0] - 2020-01-29 140 | ### Added 141 | - `get_devices` module method. `Client` does not need to be accessed 142 | directly anymore. 143 | 144 | ### Changed 145 | - Support for multiple device types. Implemented `AtaDevice` (previous 146 | implementation) and `AtwDevice`. 147 | - `operation_modes`, `fan_speeds` and other list getters are 148 | implemented as properties. 149 | - `login` method returns only acquired access token. 150 | 151 | ## [0.7.1] - 2020-01-13 152 | ### Fixed 153 | - Base `EffectiveFlags` update on current state and apply only new 154 | flags. 155 | - Use longer device write and conf update intervals. 156 | - Fix target temperature flag logic. 157 | 158 | ## [0.7.0] - 2020-01-11 159 | ### Changed 160 | - Moved login method to module. Original staticmethod implementation 161 | is still available and forwards calls to the module method. 162 | 163 | ## [0.6.0] - 2020-01-11 164 | ### Added 165 | - Token exposed as a property. 166 | 167 | ### Changed 168 | - Removed destruct. 169 | 170 | ## [0.5.1] - 2020-01-05 171 | ### Fixed 172 | - Removed `TypedDict` usage to support Python <3.8. 173 | 174 | ## [0.5.0] - 2020-01-05 175 | ### Added 176 | - `total_energy_consumed` property returning kWh counter reading. 177 | - `units` model information. 178 | 179 | ## [0.4.0] - 2019-12-30 180 | ### Added 181 | - Horizontal and vertical vane support. 182 | 183 | ## [0.3.0] - 2019-12-27 184 | ### Changed 185 | - Use proper async `set` function for `Device`. `asyncio.Event` is used 186 | to signal a in-progress `set` operation. Multiple calls to `set` will 187 | all wait for the same event that is set when the eventual write has been performed. 188 | 189 | ## [0.2.0] - 2019-12-26 190 | ### Fixed 191 | - Return `None` when trying to read stale state instead of crashing. 192 | 193 | ## [0.1.1] - 2019-12-26 194 | ### Fixed 195 | - Reset pending writes after they are applied to prevent rewriting them 196 | on every subsequent write. 197 | 198 | ## [0.1.0] - 2019-12-25 199 | Initial release 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Vilppu Vuorinen, vilppu.jotain@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymelcloud 2 | 3 | [![PyPI version](https://badge.fury.io/py/pymelcloud.svg)](https://badge.fury.io/py/pymelcloud) 4 | 5 | This is a package for interacting with MELCloud and Mitsubishi Electric 6 | devices. It's still a little rough around the edges and the documentation 7 | is non-existent. 8 | 9 | The goals for this package are: 10 | 11 | * To control and automate devices, not to configure them. 12 | * Handle device capabilities behind the scenes. 13 | * Make the different device types behave in predictable way. 14 | 15 | ## Notes on usage 16 | 17 | There are built-in rate limits and debouncing for most of the methods 18 | with the exception of the `Device` `update` method. 19 | 20 | * Initialize devices for each account only once during application 21 | runtime. 22 | * Make sure the `update` calls for each `Device` are rate limited. A 60 23 | second update interval is a good starting point. Going much faster will 24 | exceed the expected load for MELCloud and can potentially cause 25 | availability issues. 26 | * Make absolutely sure the `update` calls are rate limited. 27 | 28 | ## Supported devices 29 | 30 | * Air-to-air heat pumps (DeviceType=0) 31 | * Air-to-water heat pumps (DeviceType=1) 32 | * Energy Recovery Ventilators (DeviceType=3) 33 | 34 | ## Read 35 | 36 | Reads access only locally cached state. Call `device.update()` to 37 | fetch the latest state. 38 | 39 | Available properties: 40 | 41 | * `name` 42 | * `mac` 43 | * `serial` 44 | * `units` - model info of related units. 45 | * `temp_unit` 46 | * `last_seen` 47 | * `power` 48 | * `daily_energy_consumed` 49 | * `wifi_signal` 50 | 51 | 52 | Other properties are available through `_` prefixed state objects if 53 | one has the time to go through the source. 54 | 55 | ### Air-to-air heat pump properties 56 | * `room_temperature` 57 | * `target_temperature` 58 | * `target_temperature_step` 59 | * `target_temperature_min` 60 | * `target_temperature_max` 61 | * `operation_mode` 62 | * available `operation_modes` 63 | * `fan_speed` 64 | * available `fan_speeds` 65 | * `vane_horizontal` 66 | * available `vane_horizontal_positions` 67 | * `vane_vertical` 68 | * available `vane_vertical_positions` 69 | * `total_energy_consumed` in kWh. See [notes below.](#energy-consumption) 70 | 71 | ### Air-to-water heat pump properties 72 | * `tank_temperature` 73 | * `target_tank_temperature` 74 | * `tank_temperature_min` 75 | * `tank_temperature_max` 76 | * `outside_temperature` 77 | * `zones` 78 | * `name` 79 | * `status` 80 | * `room_temperature` 81 | * `target_temperature` 82 | * `status` 83 | * `operation_mode` 84 | * available `operation_modes` 85 | 86 | ### Energy recovery ventilator properties 87 | * `room_temperature` 88 | * `outdoor_temperature` 89 | * available `fan_speeds` 90 | * `fan_speed` 91 | * `actual_supply_fan_speed` 92 | * `actual_exhaust_fan_speed` 93 | * available `ventilation_modes` 94 | * `ventilation_mode` 95 | * `actual_ventilation_mode` 96 | * `total_energy_consumed` 97 | * `wifi_signal` 98 | * `presets` 99 | * `error_code` 100 | * `core_maintenance_required` 101 | * `filter_maintenance_required` 102 | * `night_purge_mode` 103 | * `room_co2_level` 104 | 105 | ### Energy consumption 106 | 107 | The energy consumption reading is a little strange. The API returns a 108 | value of 1.8e6 for my unit. Judging by the scale the unit is either kJ 109 | or Wh. However, neither of them quite fits. 110 | 111 | * Total reading in kJ matches better what I would expect based on the 112 | energy reports in MELCloud. 113 | * In Wh the reading is 3-5 times greater than what I would expect, but 114 | the reading is increasing at a rate that seems to match energy reports 115 | in MELCloud. 116 | 117 | Here are couple of readings with monthly reported usage as reference: 118 | 119 | * 2020-01-04T23:42:00+02:00 - 1820400, 28.5 kWh 120 | * 2020-01-05T09:44:00+02:00 - 1821300, 29.4 kWh 121 | * 2020-01-05T10:49:00+02:00 - 1821500, 29.6 kWh 122 | 123 | I'd say it's pretty clear that it is Wh and the total reading is not 124 | reflective of unit lifetime energy consumption. `total_energy_consumed` 125 | converts Wh to kWh. 126 | 127 | ## Write 128 | 129 | Writes are applied after a debounce and update the local state once 130 | completed. The physical device does not register the changes 131 | immediately due to the 60 second polling interval. 132 | 133 | Writable properties are: 134 | 135 | * `power` 136 | 137 | ### Air-to-air heat pump write 138 | 139 | * `target_temperature` 140 | * `operation_mode` 141 | * `fan_speed` 142 | * `vane_horizontal` 143 | * `vane_vertical` 144 | 145 | There's weird behavior associated with the horizontal vane swing. 146 | Toggling it on will also toggle vertical swing on and the horizontal 147 | swing has to be disabled before vertical vanes can be adjusted to any 148 | other position. This behavior can be replicated using the MELCloud user 149 | inteface. 150 | 151 | ### Air-to-water heat pump write 152 | 153 | * `target_tank_temperature` 154 | * `operation_mode` 155 | * `zone_1_target_temperature` 156 | * `zone_2_target_tempeature` 157 | 158 | Zone target temperatures can also be set via the `Zone` object 159 | returned by `zones` property on `AtwDevice`. 160 | 161 | ### Energy recovery ventilator write 162 | 163 | * `ventilation_mode` 164 | * `fan_speed` 165 | 166 | ## Example usage 167 | 168 | ```python 169 | import aiohttp 170 | import asyncio 171 | import pymelcloud 172 | 173 | 174 | async def main(): 175 | 176 | async with aiohttp.ClientSession() as session: 177 | # call the login method with the session 178 | token = await pymelcloud.login("my@example.com", "mysecretpassword", session=session) 179 | 180 | # lookup the device 181 | devices = await pymelcloud.get_devices(token, session=session) 182 | device = devices[pymelcloud.DEVICE_TYPE_ATW][0] 183 | 184 | # perform logic on the device 185 | await device.update() 186 | 187 | print(device.name) 188 | await session.close() 189 | 190 | loop = asyncio.get_event_loop() 191 | loop.run_until_complete(main()) 192 | ``` 193 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore=tests 3 | # Use a conservative default here; 2 should speed up most setups and not hurt 4 | # any too bad. Override on command line as appropriate. 5 | jobs=2 6 | persistent=no 7 | 8 | [BASIC] 9 | good-names=id,i,j,k,ex,Run,_,fp 10 | 11 | [MESSAGES CONTROL] 12 | # Reasons disabled: 13 | # format - handled by black 14 | # locally-disabled - it spams too much 15 | # duplicate-code - unavoidable 16 | # cyclic-import - doesn't test if both import on load 17 | # abstract-class-little-used - prevents from setting right foundation 18 | # unused-argument - generic callbacks and setup methods create a lot of warnings 19 | # global-statement - used for the on-demand requirement installation 20 | # redefined-variable-type - this is Python, we're duck typing! 21 | # too-many-* - are not enforced for the sake of readability 22 | # too-few-* - same as too-many-* 23 | # abstract-method - with intro of async there are always methods missing 24 | # inconsistent-return-statements - doesn't handle raise 25 | # unnecessary-pass - readability for functions which only contain pass 26 | # import-outside-toplevel - TODO 27 | # too-many-ancestors - it's too strict. 28 | # wrong-import-order - isort guards this 29 | disable= 30 | format, 31 | abstract-class-little-used, 32 | abstract-method, 33 | cyclic-import, 34 | duplicate-code, 35 | global-statement, 36 | import-outside-toplevel, 37 | inconsistent-return-statements, 38 | locally-disabled, 39 | not-context-manager, 40 | redefined-variable-type, 41 | too-few-public-methods, 42 | too-many-ancestors, 43 | too-many-arguments, 44 | too-many-branches, 45 | too-many-instance-attributes, 46 | too-many-lines, 47 | too-many-locals, 48 | too-many-public-methods, 49 | too-many-return-statements, 50 | too-many-statements, 51 | too-many-boolean-expressions, 52 | unnecessary-pass, 53 | unused-argument, 54 | wrong-import-order 55 | enable= 56 | use-symbolic-message-instead 57 | 58 | [REPORTS] 59 | score=no 60 | 61 | [TYPECHECK] 62 | # For attrs 63 | ignored-classes=_CountingAttr 64 | 65 | [FORMAT] 66 | expected-line-ending-format=LF 67 | 68 | [EXCEPTIONS] 69 | overgeneral-exceptions=BaseException,Exception 70 | -------------------------------------------------------------------------------- /pymelcloud/__init__.py: -------------------------------------------------------------------------------- 1 | """MELCloud client library.""" 2 | from datetime import timedelta 3 | from typing import Dict, List, Optional 4 | 5 | from aiohttp import ClientSession 6 | 7 | from pymelcloud.ata_device import AtaDevice 8 | from pymelcloud.atw_device import AtwDevice 9 | from pymelcloud.erv_device import ErvDevice 10 | from pymelcloud.client import Client as _Client 11 | from pymelcloud.client import login as _login 12 | from pymelcloud.const import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW, DEVICE_TYPE_ERV 13 | from pymelcloud.device import Device 14 | 15 | 16 | async def login( 17 | email: str, password: str, session: Optional[ClientSession] = None, 18 | ) -> str: 19 | """Log in to MELCloud with given credentials. 20 | 21 | Returns access token. 22 | """ 23 | _client = await _login(email, password, session,) 24 | return _client.token 25 | 26 | 27 | async def get_devices( 28 | token: str, 29 | session: Optional[ClientSession] = None, 30 | *, 31 | conf_update_interval=timedelta(minutes=5), 32 | device_set_debounce=timedelta(seconds=1), 33 | ) -> Dict[str, List[Device]]: 34 | """Initialize Devices available with the token. 35 | 36 | The devices share a the same Client instance and pool config fetches. The devices 37 | should be fetched only once during application life cycle to leverage the request 38 | pooling and rate limits. 39 | 40 | Keyword arguments: 41 | conf_update_interval -- rate limit for fetching device confs. (default = 5 min) 42 | device_set_debounce -- debounce time for writing device state. (default = 1 s) 43 | """ 44 | _client = _Client( 45 | token, 46 | session, 47 | conf_update_interval=conf_update_interval, 48 | device_set_debounce=device_set_debounce, 49 | ) 50 | await _client.update_confs() 51 | return { 52 | DEVICE_TYPE_ATA: [ 53 | AtaDevice(conf, _client, set_debounce=device_set_debounce) 54 | for conf in _client.device_confs 55 | if conf.get("Device", {}).get("DeviceType") == 0 56 | ], 57 | DEVICE_TYPE_ATW: [ 58 | AtwDevice(conf, _client, set_debounce=device_set_debounce) 59 | for conf in _client.device_confs 60 | if conf.get("Device", {}).get("DeviceType") == 1 61 | ], 62 | DEVICE_TYPE_ERV: [ 63 | ErvDevice(conf, _client, set_debounce=device_set_debounce) 64 | for conf in _client.device_confs 65 | if conf.get("Device", {}).get("DeviceType") == 3 66 | ], 67 | } 68 | -------------------------------------------------------------------------------- /pymelcloud/ata_device.py: -------------------------------------------------------------------------------- 1 | """Air-To-Air (DeviceType=0) device definition.""" 2 | from datetime import timedelta 3 | from typing import Any, Dict, List, Optional 4 | 5 | from pymelcloud.device import EFFECTIVE_FLAGS, Device 6 | from pymelcloud.client import Client 7 | 8 | PROPERTY_TARGET_TEMPERATURE = "target_temperature" 9 | PROPERTY_OPERATION_MODE = "operation_mode" 10 | PROPERTY_FAN_SPEED = "fan_speed" 11 | PROPERTY_VANE_HORIZONTAL = "vane_horizontal" 12 | PROPERTY_VANE_VERTICAL = "vane_vertical" 13 | 14 | FAN_SPEED_AUTO = "auto" 15 | 16 | OPERATION_MODE_HEAT = "heat" 17 | OPERATION_MODE_DRY = "dry" 18 | OPERATION_MODE_COOL = "cool" 19 | OPERATION_MODE_FAN_ONLY = "fan_only" 20 | OPERATION_MODE_HEAT_COOL = "heat_cool" 21 | OPERATION_MODE_UNDEFINED = "undefined" 22 | 23 | _OPERATION_MODE_LOOKUP = { 24 | 1: OPERATION_MODE_HEAT, 25 | 2: OPERATION_MODE_DRY, 26 | 3: OPERATION_MODE_COOL, 27 | 7: OPERATION_MODE_FAN_ONLY, 28 | 8: OPERATION_MODE_HEAT_COOL, 29 | } 30 | 31 | _OPERATION_MODE_MIN_TEMP_LOOKUP = { 32 | OPERATION_MODE_HEAT: "MinTempHeat", 33 | OPERATION_MODE_DRY: "MinTempCoolDry", 34 | OPERATION_MODE_COOL: "MinTempCoolDry", 35 | OPERATION_MODE_FAN_ONLY: "MinTempHeat", # Fake it just in case. 36 | OPERATION_MODE_HEAT_COOL: "MinTempAutomatic", 37 | OPERATION_MODE_UNDEFINED: "MinTempHeat", 38 | } 39 | 40 | _OPERATION_MODE_MAX_TEMP_LOOKUP = { 41 | OPERATION_MODE_HEAT: "MaxTempHeat", 42 | OPERATION_MODE_DRY: "MaxTempCoolDry", 43 | OPERATION_MODE_COOL: "MaxTempCoolDry", 44 | OPERATION_MODE_FAN_ONLY: "MaxTempHeat", # Fake it just in case. 45 | OPERATION_MODE_HEAT_COOL: "MaxTempAutomatic", 46 | OPERATION_MODE_UNDEFINED: "MaxTempHeat", 47 | } 48 | 49 | V_VANE_POSITION_AUTO = "auto" 50 | V_VANE_POSITION_1 = "1_up" 51 | V_VANE_POSITION_2 = "2" 52 | V_VANE_POSITION_3 = "3" 53 | V_VANE_POSITION_4 = "4" 54 | V_VANE_POSITION_5 = "5_down" 55 | V_VANE_POSITION_SWING = "swing" 56 | V_VANE_POSITION_UNDEFINED = "undefined" 57 | 58 | 59 | H_VANE_POSITION_AUTO = "auto" 60 | H_VANE_POSITION_1 = "1_left" 61 | H_VANE_POSITION_2 = "2" 62 | H_VANE_POSITION_3 = "3" 63 | H_VANE_POSITION_4 = "4" 64 | H_VANE_POSITION_5 = "5_right" 65 | H_VANE_POSITION_SPLIT = "split" 66 | H_VANE_POSITION_SWING = "swing" 67 | H_VANE_POSITION_UNDEFINED = "undefined" 68 | 69 | 70 | def _fan_speed_from(speed: int) -> str: 71 | if speed == 0: 72 | return FAN_SPEED_AUTO 73 | return str(speed) 74 | 75 | 76 | def _fan_speed_to(speed: str) -> int: 77 | if speed == FAN_SPEED_AUTO: 78 | return 0 79 | return int(speed) 80 | 81 | 82 | def _operation_mode_from(mode: int) -> str: 83 | return _OPERATION_MODE_LOOKUP.get(mode, OPERATION_MODE_UNDEFINED) 84 | 85 | 86 | def _operation_mode_to(mode: str) -> int: 87 | for k, value in _OPERATION_MODE_LOOKUP.items(): 88 | if value == mode: 89 | return k 90 | raise ValueError(f"Invalid operation_mode [{mode}]") 91 | 92 | 93 | _H_VANE_POSITION_LOOKUP = { 94 | 0: H_VANE_POSITION_AUTO, 95 | 1: H_VANE_POSITION_1, 96 | 2: H_VANE_POSITION_2, 97 | 3: H_VANE_POSITION_3, 98 | 4: H_VANE_POSITION_4, 99 | 5: H_VANE_POSITION_5, 100 | 8: H_VANE_POSITION_SPLIT, 101 | 12: H_VANE_POSITION_SWING, 102 | } 103 | 104 | 105 | def _horizontal_vane_from(position: int) -> str: 106 | return _H_VANE_POSITION_LOOKUP.get(position, H_VANE_POSITION_UNDEFINED) 107 | 108 | 109 | def _horizontal_vane_to(position: str) -> int: 110 | for k, value in _H_VANE_POSITION_LOOKUP.items(): 111 | if value == position: 112 | return k 113 | raise ValueError(f"Invalid horizontal vane position [{position}]") 114 | 115 | 116 | _V_VANE_POSITION_LOOKUP = { 117 | 0: V_VANE_POSITION_AUTO, 118 | 1: V_VANE_POSITION_1, 119 | 2: V_VANE_POSITION_2, 120 | 3: V_VANE_POSITION_3, 121 | 4: V_VANE_POSITION_4, 122 | 5: V_VANE_POSITION_5, 123 | 7: V_VANE_POSITION_SWING, 124 | } 125 | 126 | 127 | def _vertical_vane_from(position: int) -> str: 128 | return _V_VANE_POSITION_LOOKUP.get(position, V_VANE_POSITION_UNDEFINED) 129 | 130 | 131 | def _vertical_vane_to(position: str) -> int: 132 | for k, value in _V_VANE_POSITION_LOOKUP.items(): 133 | if value == position: 134 | return k 135 | raise ValueError(f"Invalid vertical vane position [{position}]") 136 | 137 | 138 | class AtaDevice(Device): 139 | """Air-to-Air device.""" 140 | 141 | def __init__( 142 | self, 143 | device_conf: Dict[str, Any], 144 | client: Client, 145 | set_debounce=timedelta(seconds=1), 146 | ): 147 | """Initialize an ATA device.""" 148 | super().__init__(device_conf, client, set_debounce) 149 | self.last_energy_value = None 150 | 151 | def apply_write(self, state: Dict[str, Any], key: str, value: Any): 152 | """Apply writes to state object. 153 | 154 | Used for property validation, do not modify device state. 155 | """ 156 | flags = state.get(EFFECTIVE_FLAGS, 0) 157 | 158 | if key == PROPERTY_TARGET_TEMPERATURE: 159 | state["SetTemperature"] = self.round_temperature(value) 160 | flags = flags | 0x04 161 | elif key == PROPERTY_OPERATION_MODE: 162 | state["OperationMode"] = _operation_mode_to(value) 163 | flags = flags | 0x02 164 | elif key == PROPERTY_FAN_SPEED: 165 | state["SetFanSpeed"] = _fan_speed_to(value) 166 | flags = flags | 0x08 167 | elif key == PROPERTY_VANE_HORIZONTAL: 168 | state["VaneHorizontal"] = _horizontal_vane_to(value) 169 | flags = flags | 0x100 170 | elif key == PROPERTY_VANE_VERTICAL: 171 | state["VaneVertical"] = _vertical_vane_to(value) 172 | flags = flags | 0x10 173 | else: 174 | raise ValueError(f"Cannot set {key}, invalid property") 175 | 176 | state[EFFECTIVE_FLAGS] = flags 177 | 178 | @property 179 | def has_energy_consumed_meter(self) -> bool: 180 | """Return True if the device has an energy consumption meter.""" 181 | return self._device_conf.get("Device", {}).get("HasEnergyConsumedMeter", False) 182 | 183 | @property 184 | def total_energy_consumed(self) -> Optional[float]: 185 | """Return total consumed energy as kWh. 186 | 187 | The update interval is extremely slow and inconsistent. Empirical evidence 188 | suggests that it can vary between 1h 30min and 3h. 189 | """ 190 | if self._device_conf is None: 191 | return None 192 | device = self._device_conf.get("Device", {}) 193 | value = device.get("CurrentEnergyConsumed", None) 194 | if value is None: 195 | return None 196 | 197 | if value == 0.0: 198 | return self.last_energy_value 199 | 200 | self.last_energy_value = value / 1000.0 201 | return self.last_energy_value 202 | 203 | @property 204 | def room_temperature(self) -> Optional[float]: 205 | """Return room temperature reported by the device.""" 206 | if self._state is None: 207 | return None 208 | return self._state.get("RoomTemperature") 209 | 210 | @property 211 | def target_temperature(self) -> Optional[float]: 212 | """Return target temperature set for the device.""" 213 | if self._state is None: 214 | return None 215 | return self._state.get("SetTemperature") 216 | 217 | @property 218 | def target_temperature_step(self) -> float: 219 | """Return target temperature set precision.""" 220 | return self.temperature_increment 221 | 222 | @property 223 | def target_temperature_min(self) -> Optional[float]: 224 | """Return maximum target temperature for the currently active operation mode.""" 225 | if self._state is None: 226 | return None 227 | return self._device_conf.get("Device", {}).get( 228 | _OPERATION_MODE_MIN_TEMP_LOOKUP.get(self.operation_mode), 10 229 | ) 230 | 231 | @property 232 | def target_temperature_max(self) -> Optional[float]: 233 | """Return maximum target temperature for the currently active operation mode.""" 234 | if self._state is None: 235 | return None 236 | return self._device_conf.get("Device", {}).get( 237 | _OPERATION_MODE_MAX_TEMP_LOOKUP.get(self.operation_mode), 31 238 | ) 239 | 240 | @property 241 | def operation_mode(self) -> str: 242 | """Return currently active operation mode.""" 243 | if self._state is None: 244 | return OPERATION_MODE_UNDEFINED 245 | return _operation_mode_from(self._state.get("OperationMode", -1)) 246 | 247 | @property 248 | def operation_modes(self) -> List[str]: 249 | """Return available operation modes.""" 250 | modes: List[str] = [] 251 | 252 | conf_dev = self._device_conf.get("Device", {}) 253 | if conf_dev.get("CanHeat", False): 254 | modes.append(OPERATION_MODE_HEAT) 255 | 256 | if conf_dev.get("CanDry", False): 257 | modes.append(OPERATION_MODE_DRY) 258 | 259 | if conf_dev.get("CanCool", False): 260 | modes.append(OPERATION_MODE_COOL) 261 | 262 | modes.append(OPERATION_MODE_FAN_ONLY) 263 | 264 | if conf_dev.get("ModelSupportsAuto", False): 265 | modes.append(OPERATION_MODE_HEAT_COOL) 266 | 267 | return modes 268 | 269 | @property 270 | def fan_speed(self) -> Optional[str]: 271 | """Return currently active fan speed. 272 | 273 | The argument must be on of the fan speeds returned by fan_speeds. 274 | """ 275 | if self._state is None: 276 | return None 277 | return _fan_speed_from(self._state.get("SetFanSpeed")) 278 | 279 | @property 280 | def fan_speeds(self) -> Optional[List[str]]: 281 | """Return available fan speeds. 282 | 283 | The supported fan speeds vary from device to device. The available modes are 284 | read from the Device capability attributes. 285 | 286 | For example, a 5 speed device with auto fan speed would produce the following 287 | list (formatted '"[pymelcloud]" -- "[device controls]"') 288 | 289 | - "auto" -- "auto" 290 | - "1" -- "silent" 291 | - "2" -- "1" 292 | - "3" -- "2" 293 | - "4" -- "3" 294 | - "5" -- "4" 295 | 296 | MELCloud is not aware of the device type making it infeasible to match the 297 | fan speed names with the device documentation. 298 | """ 299 | if self._state is None: 300 | return None 301 | speeds = [] 302 | if self._device_conf.get("Device", {}).get("HasAutomaticFanSpeed", False): 303 | speeds.append(FAN_SPEED_AUTO) 304 | 305 | num_fan_speeds = self._state.get("NumberOfFanSpeeds", 0) 306 | for num in range(1, num_fan_speeds + 1): 307 | speeds.append(_fan_speed_from(num)) 308 | 309 | return speeds 310 | 311 | @property 312 | def vane_horizontal(self) -> Optional[str]: 313 | """Return horizontal vane position.""" 314 | if self._state is None: 315 | return None 316 | return _horizontal_vane_from(self._state.get("VaneHorizontal")) 317 | 318 | @property 319 | def vane_horizontal_positions(self) -> Optional[List[str]]: 320 | """Return available horizontal vane positions.""" 321 | if self._device_conf.get("HideVaneControls", False): 322 | return [] 323 | device = self._device_conf.get("Device", {}) 324 | if not device.get("ModelSupportsVaneHorizontal", False): 325 | return [] 326 | 327 | positions = [ 328 | H_VANE_POSITION_AUTO, # ModelSupportsAuto could affect this. 329 | H_VANE_POSITION_1, 330 | H_VANE_POSITION_2, 331 | H_VANE_POSITION_3, 332 | H_VANE_POSITION_4, 333 | H_VANE_POSITION_5, 334 | H_VANE_POSITION_SPLIT, 335 | ] 336 | if device.get("SwingFunction", False): 337 | positions.append(H_VANE_POSITION_SWING) 338 | 339 | return positions 340 | 341 | @property 342 | def vane_vertical(self) -> Optional[str]: 343 | """Return vertical vane position.""" 344 | if self._state is None: 345 | return None 346 | return _vertical_vane_from(self._state.get("VaneVertical")) 347 | 348 | @property 349 | def vane_vertical_positions(self) -> Optional[List[str]]: 350 | """Return available vertical vane positions.""" 351 | if self._device_conf.get("HideVaneControls", False): 352 | return [] 353 | device = self._device_conf.get("Device", {}) 354 | if not device.get("ModelSupportsVaneVertical", False): 355 | return [] 356 | 357 | positions = [ 358 | V_VANE_POSITION_AUTO, # ModelSupportsAuto could affect this. 359 | V_VANE_POSITION_1, 360 | V_VANE_POSITION_2, 361 | V_VANE_POSITION_3, 362 | V_VANE_POSITION_4, 363 | V_VANE_POSITION_5, 364 | ] 365 | if device.get("SwingFunction", False): 366 | positions.append(V_VANE_POSITION_SWING) 367 | 368 | return positions 369 | 370 | @property 371 | def actual_fan_speed(self) -> Optional[str]: 372 | """Return actual fan speed. 373 | 374 | 0 is stopped, not auto 375 | 376 | """ 377 | if self._state is None: 378 | return None 379 | return str(self._device_conf.get("Device", {}).get("ActualFanSpeed", -1)) 380 | -------------------------------------------------------------------------------- /pymelcloud/atw_device.py: -------------------------------------------------------------------------------- 1 | """Air-To-Water (DeviceType=1) device definition.""" 2 | from typing import Any, Callable, Dict, List, Optional 3 | 4 | from pymelcloud.device import EFFECTIVE_FLAGS, Device 5 | 6 | PROPERTY_TARGET_TANK_TEMPERATURE = "target_tank_temperature" 7 | PROPERTY_OPERATION_MODE = "operation_mode" 8 | PROPERTY_ZONE_1_TARGET_TEMPERATURE = "zone_1_target_temperature" 9 | PROPERTY_ZONE_2_TARGET_TEMPERATURE = "zone_2_target_temperature" 10 | PROPERTY_ZONE_1_TARGET_HEAT_FLOW_TEMPERATURE = "zone_1_target_heat_flow_temperature" 11 | PROPERTY_ZONE_2_TARGET_HEAT_FLOW_TEMPERATURE = "zone_2_target_heat_flow_temperature" 12 | PROPERTY_ZONE_1_TARGET_COOL_FLOW_TEMPERATURE = "zone_1_target_heat_cool_temperature" 13 | PROPERTY_ZONE_2_TARGET_COOL_FLOW_TEMPERATURE = "zone_2_target_heat_cool_temperature" 14 | PROPERTY_ZONE_1_OPERATION_MODE = "zone_1_operation_mode" 15 | PROPERTY_ZONE_2_OPERATION_MODE = "zone_2_operation_mode" 16 | 17 | OPERATION_MODE_AUTO = "auto" 18 | OPERATION_MODE_FORCE_HOT_WATER = "force_hot_water" 19 | 20 | STATUS_IDLE = "idle" 21 | STATUS_HEAT_WATER = "heat_water" 22 | STATUS_HEAT_ZONES = "heat_zones" 23 | STATUS_COOL = "cool" 24 | STATUS_DEFROST = "defrost" 25 | STATUS_STANDBY = "standby" 26 | STATUS_LEGIONELLA = "legionella" 27 | STATUS_UNKNOWN = "unknown" 28 | 29 | _STATE_LOOKUP = { 30 | 0: STATUS_IDLE, 31 | 1: STATUS_HEAT_WATER, 32 | 2: STATUS_HEAT_ZONES, 33 | 3: STATUS_COOL, 34 | 4: STATUS_DEFROST, 35 | 5: STATUS_STANDBY, 36 | 6: STATUS_LEGIONELLA, 37 | } 38 | 39 | 40 | _ZONE_INT_MODE_HEAT_THERMOSTAT = 0 41 | _ZONE_INT_MODE_HEAT_FLOW = 1 42 | _ZONE_INT_MODE_CURVE = 2 43 | _ZONE_INT_MODE_COOL_THERMOSTAT = 3 44 | _ZONE_INT_MODE_COOL_FLOW = 4 45 | 46 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT = "heat-thermostat" 47 | ZONE_OPERATION_MODE_COOL_THERMOSTAT = "cool-thermostat" 48 | ZONE_OPERATION_MODE_HEAT_FLOW = "heat-flow" 49 | ZONE_OPERATION_MODE_COOL_FLOW = "cool-flow" 50 | ZONE_OPERATION_MODE_CURVE = "curve" 51 | ZONE_OPERATION_MODE_UNKNOWN = "unknown" 52 | _ZONE_OPERATION_MODE_LOOKUP = { 53 | _ZONE_INT_MODE_HEAT_THERMOSTAT: ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 54 | _ZONE_INT_MODE_HEAT_FLOW: ZONE_OPERATION_MODE_HEAT_FLOW, 55 | _ZONE_INT_MODE_CURVE: ZONE_OPERATION_MODE_CURVE, 56 | _ZONE_INT_MODE_COOL_THERMOSTAT: ZONE_OPERATION_MODE_COOL_THERMOSTAT, 57 | _ZONE_INT_MODE_COOL_FLOW: ZONE_OPERATION_MODE_COOL_FLOW, 58 | } 59 | _REVERSE_ZONE_OPERATION_MODE_LOOKUP = { 60 | value: key for key, value in _ZONE_OPERATION_MODE_LOOKUP.items() 61 | } 62 | 63 | ZONE_STATUS_HEAT = "heat" 64 | ZONE_STATUS_IDLE = "idle" 65 | ZONE_STATUS_COOL = "cool" 66 | ZONE_STATUS_UNKNOWN = "unknown" 67 | 68 | 69 | class Zone: 70 | """Zone controlled by Air-to-Water device.""" 71 | 72 | def __init__( 73 | self, 74 | device, 75 | device_state: Callable[[], Optional[Dict[Any, Any]]], 76 | device_conf: Callable[[], Dict[Any, Any]], 77 | zone_index: int, 78 | ): 79 | """Initialize Zone.""" 80 | self._device = device 81 | self._device_state = device_state 82 | self._device_conf = device_conf 83 | self.zone_index = zone_index 84 | 85 | @property 86 | def name(self) -> Optional[str]: 87 | """Return zone name. 88 | 89 | If a name is not defined, a name is generated using format "Zone n" where "n" 90 | is the number of the zone. 91 | """ 92 | zone_name = self._device_conf().get(f"Zone{self.zone_index}Name") 93 | if zone_name is None: 94 | return f"Zone {self.zone_index}" 95 | return zone_name 96 | 97 | @property 98 | def prohibit(self) -> Optional[bool]: 99 | """Return prohibit flag of the zone.""" 100 | state = self._device_state() 101 | if state is None: 102 | return None 103 | return state.get(f"ProhibitZone{self.zone_index}") 104 | 105 | @property 106 | def status(self) -> str: 107 | """Return the current status. 108 | 109 | This is a Air-to-Water device specific property. The value can be - depending 110 | on the device capabilities - "heat", "cool" or "idle". 111 | """ 112 | state = self._device_state() 113 | if state is None: 114 | return ZONE_STATUS_UNKNOWN 115 | if state.get(f"IdleZone{self.zone_index}", False): 116 | return ZONE_STATUS_IDLE 117 | 118 | op_mode = self.operation_mode 119 | if op_mode in [ 120 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 121 | ZONE_OPERATION_MODE_HEAT_FLOW, 122 | ZONE_OPERATION_MODE_CURVE, 123 | ]: 124 | return ZONE_STATUS_HEAT 125 | if op_mode in [ 126 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 127 | ZONE_OPERATION_MODE_COOL_FLOW, 128 | ]: 129 | return ZONE_STATUS_COOL 130 | 131 | return ZONE_STATUS_UNKNOWN 132 | 133 | @property 134 | def room_temperature(self) -> Optional[float]: 135 | """Return room temperature.""" 136 | state = self._device_state() 137 | if state is None: 138 | return None 139 | return state.get(f"RoomTemperatureZone{self.zone_index}") 140 | 141 | @property 142 | def target_temperature(self) -> Optional[float]: 143 | """Return target temperature.""" 144 | state = self._device_state() 145 | if state is None: 146 | return None 147 | return state.get(f"SetTemperatureZone{self.zone_index}") 148 | 149 | async def set_target_temperature(self, target_temperature): 150 | """Set target temperature for this zone.""" 151 | if self.zone_index == 1: 152 | prop = PROPERTY_ZONE_1_TARGET_TEMPERATURE 153 | else: 154 | prop = PROPERTY_ZONE_2_TARGET_TEMPERATURE 155 | await self._device.set({prop: target_temperature}) 156 | 157 | @property 158 | def flow_temperature(self) -> float: 159 | """Return current flow temperature. 160 | 161 | This value is not available in the standard state poll response. The poll 162 | update frequency can be a little bit lower that expected. 163 | """ 164 | return self._device_conf()["Device"]["FlowTemperature"] 165 | 166 | @property 167 | def return_temperature(self) -> float: 168 | """Return current return flow temperature. 169 | 170 | This value is not available in the standard state poll response. The poll 171 | update frequency can be a little bit lower that expected. 172 | """ 173 | return self._device_conf()["Device"]["ReturnTemperature"] 174 | 175 | @property 176 | def target_flow_temperature(self) -> Optional[float]: 177 | """Return target flow temperature of the currently active operation mode.""" 178 | op_mode = self.operation_mode 179 | if op_mode is None: 180 | return None 181 | 182 | if op_mode in [ 183 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 184 | ZONE_OPERATION_MODE_COOL_FLOW, 185 | ]: 186 | return self.target_cool_flow_temperature 187 | 188 | return self.target_heat_flow_temperature 189 | 190 | @property 191 | def target_heat_flow_temperature(self) -> Optional[float]: 192 | """Return target heat flow temperature.""" 193 | state = self._device_state() 194 | if state is None: 195 | return None 196 | 197 | return state.get(f"SetHeatFlowTemperatureZone{self.zone_index}") 198 | 199 | @property 200 | def target_cool_flow_temperature(self) -> Optional[float]: 201 | """Return target cool flow temperature.""" 202 | state = self._device_state() 203 | if state is None: 204 | return None 205 | 206 | return state.get(f"SetCoolFlowTemperatureZone{self.zone_index}") 207 | 208 | async def set_target_flow_temperature(self, target_flow_temperature): 209 | """Set target flow temperature for the currently active operation mode.""" 210 | op_mode = self.operation_mode 211 | if op_mode is None: 212 | return None 213 | 214 | if op_mode in [ 215 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 216 | ZONE_OPERATION_MODE_COOL_FLOW, 217 | ]: 218 | await self.set_target_cool_flow_temperature(target_flow_temperature) 219 | else: 220 | await self.set_target_heat_flow_temperature(target_flow_temperature) 221 | 222 | async def set_target_heat_flow_temperature(self, target_flow_temperature): 223 | """Set target heat flow temperature of this zone.""" 224 | if self.zone_index == 1: 225 | prop = PROPERTY_ZONE_1_TARGET_HEAT_FLOW_TEMPERATURE 226 | else: 227 | prop = PROPERTY_ZONE_2_TARGET_HEAT_FLOW_TEMPERATURE 228 | await self._device.set({prop: target_flow_temperature}) 229 | 230 | async def set_target_cool_flow_temperature(self, target_flow_temperature): 231 | """Set target cool flow temperature of this zone.""" 232 | if self.zone_index == 1: 233 | prop = PROPERTY_ZONE_1_TARGET_COOL_FLOW_TEMPERATURE 234 | else: 235 | prop = PROPERTY_ZONE_2_TARGET_COOL_FLOW_TEMPERATURE 236 | await self._device.set({prop: target_flow_temperature}) 237 | 238 | @property 239 | def operation_mode(self) -> Optional[str]: 240 | """Return current operation mode.""" 241 | state = self._device_state() 242 | if state is None: 243 | return None 244 | 245 | print(state) 246 | 247 | mode = state.get(f"OperationModeZone{self.zone_index}") 248 | if not isinstance(mode, int): 249 | raise ValueError(f"Invalid operation mode [{mode}]") 250 | 251 | return _ZONE_OPERATION_MODE_LOOKUP.get( 252 | mode, 253 | ZONE_OPERATION_MODE_UNKNOWN, 254 | ) 255 | 256 | @property 257 | def operation_modes(self) -> List[str]: 258 | """Return list of available operation modes.""" 259 | modes = [] 260 | device = self._device_conf().get("Device", {}) 261 | if device.get("CanHeat", False): 262 | modes += [ 263 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 264 | ZONE_OPERATION_MODE_HEAT_FLOW, 265 | ZONE_OPERATION_MODE_CURVE, 266 | ] 267 | if device.get("CanCool", False): 268 | modes += [ 269 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 270 | ZONE_OPERATION_MODE_COOL_FLOW, 271 | ] 272 | return modes 273 | 274 | async def set_operation_mode(self, mode: str): 275 | """Change operation mode.""" 276 | state = self._device_state() 277 | if state is None: 278 | return 279 | 280 | int_mode = _REVERSE_ZONE_OPERATION_MODE_LOOKUP.get(mode) 281 | if int_mode is None: 282 | raise ValueError(f"Invalid mode '{mode}'") 283 | 284 | if self.zone_index == 1: 285 | prop = PROPERTY_ZONE_1_OPERATION_MODE 286 | else: 287 | prop = PROPERTY_ZONE_2_OPERATION_MODE 288 | 289 | await self._device.set({prop: int_mode}) 290 | 291 | 292 | class AtwDevice(Device): 293 | """Air-to-Water device.""" 294 | 295 | def apply_write(self, state: Dict[str, Any], key: str, value: Any): 296 | """Apply writes to state object.""" 297 | flags = state.get(EFFECTIVE_FLAGS, 0) 298 | 299 | if key == PROPERTY_TARGET_TANK_TEMPERATURE: 300 | state["SetTankWaterTemperature"] = self.round_temperature(value) 301 | flags |= 0x1000000000020 302 | elif key == PROPERTY_OPERATION_MODE: 303 | state["ForcedHotWaterMode"] = value == OPERATION_MODE_FORCE_HOT_WATER 304 | flags |= 0x10000 305 | elif key == PROPERTY_ZONE_1_TARGET_TEMPERATURE: 306 | state["SetTemperatureZone1"] = self.round_temperature(value) 307 | flags |= 0x200000080 308 | elif key == PROPERTY_ZONE_2_TARGET_TEMPERATURE: 309 | state["SetTemperatureZone2"] = self.round_temperature(value) 310 | flags |= 0x800000200 311 | elif key == PROPERTY_ZONE_1_TARGET_HEAT_FLOW_TEMPERATURE: 312 | state["SetHeatFlowTemperatureZone1"] = self.round_temperature(value) 313 | flags |= 0x1000000000000 314 | elif key == PROPERTY_ZONE_1_TARGET_COOL_FLOW_TEMPERATURE: 315 | state["SetCoolFlowTemperatureZone1"] = self.round_temperature(value) 316 | flags |= 0x1000000000000 317 | elif key == PROPERTY_ZONE_2_TARGET_HEAT_FLOW_TEMPERATURE: 318 | state["SetHeatFlowTemperatureZone2"] = self.round_temperature(value) 319 | flags |= 0x1000000000000 320 | elif key == PROPERTY_ZONE_2_TARGET_COOL_FLOW_TEMPERATURE: 321 | state["SetCoolFlowTemperatureZone2"] = self.round_temperature(value) 322 | flags |= 0x1000000000000 323 | elif key == PROPERTY_ZONE_1_OPERATION_MODE: 324 | state["OperationModeZone1"] = value 325 | flags |= 0x08 326 | elif key == PROPERTY_ZONE_2_OPERATION_MODE: 327 | state["OperationModeZone2"] = value 328 | flags |= 0x10 329 | else: 330 | raise ValueError(f"Cannot set {key}, invalid property") 331 | 332 | state[EFFECTIVE_FLAGS] = flags 333 | 334 | @property 335 | def tank_temperature(self) -> Optional[float]: 336 | """Return tank water temperature.""" 337 | return self.get_state_prop("TankWaterTemperature") 338 | 339 | @property 340 | def target_tank_temperature(self) -> Optional[float]: 341 | """Return target tank water temperature.""" 342 | return self.get_state_prop("SetTankWaterTemperature") 343 | 344 | @property 345 | def target_tank_temperature_min(self) -> Optional[float]: 346 | """Return minimum target tank water temperature. 347 | 348 | The value does not seem to be available on the API. A fixed value is used 349 | instead. 350 | """ 351 | return 40.0 352 | 353 | @property 354 | def target_tank_temperature_max(self) -> Optional[float]: 355 | """Return maximum target tank water temperature. 356 | 357 | This value can be set using PROPERTY_TARGET_TANK_TEMPERATURE. 358 | """ 359 | return self.get_device_prop("MaxTankTemperature") 360 | 361 | @property 362 | def outside_temperature(self) -> Optional[float]: 363 | """Return outdoor temperature reported by the device. 364 | 365 | Outside temperature sensor cannot be complimented on its precision or sample 366 | rate. The value is reported using 1°C (2°F?) accuracy and updated every 2 367 | hours. 368 | """ 369 | return self.get_state_prop("OutdoorTemperature") 370 | 371 | @property 372 | def flow_temperature_boiler(self) -> Optional[float]: 373 | """Return flow temperature of the boiler.""" 374 | return self.get_device_prop("FlowTemperatureBoiler") 375 | 376 | @property 377 | def return_temperature_boiler(self) -> Optional[float]: 378 | """Return flow temperature of the boiler.""" 379 | return self.get_device_prop("FlowTemperatureBoiler") 380 | 381 | @property 382 | def mixing_tank_temperature(self) -> Optional[float]: 383 | """Return mixing tank temperature.""" 384 | return self.get_device_prop("MixingTankWaterTemperature") 385 | 386 | @property 387 | def zones(self) -> Optional[List[Zone]]: 388 | """Return zones controlled by this device. 389 | 390 | Zones without a thermostat are not returned. 391 | """ 392 | _zones = [] 393 | 394 | device = self._device_conf.get("Device", {}) 395 | if device.get("HasThermostatZone1", False): 396 | _zones.append(Zone(self, lambda: self._state, lambda: self._device_conf, 1)) 397 | 398 | if device.get("HasZone2") and device.get("HasThermostatZone2", False): 399 | _zones.append(Zone(self, lambda: self._state, lambda: self._device_conf, 2)) 400 | 401 | return _zones 402 | 403 | @property 404 | def status(self) -> Optional[str]: 405 | """Return current state. 406 | 407 | This is a Air-to-Water device specific property. MELCloud uses "OperationMode" 408 | to indicate what the device is currently doing to meet its control values. 409 | """ 410 | if self._state is None: 411 | return STATUS_UNKNOWN 412 | return _STATE_LOOKUP.get(self._state.get("OperationMode", -1), STATUS_UNKNOWN) 413 | 414 | @property 415 | def operation_mode(self) -> Optional[str]: 416 | """Return active operation mode. 417 | 418 | This value can be set using PROPERTY_OPERATION_MODE. 419 | """ 420 | if self._state is None: 421 | return None 422 | if self._state.get("ForcedHotWaterMode", False): 423 | return OPERATION_MODE_FORCE_HOT_WATER 424 | return OPERATION_MODE_AUTO 425 | 426 | @property 427 | def operation_modes(self) -> List[str]: 428 | """Return available operation modes.""" 429 | return [OPERATION_MODE_AUTO, OPERATION_MODE_FORCE_HOT_WATER] 430 | 431 | @property 432 | def holiday_mode(self) -> Optional[bool]: 433 | """Return holiday mode status.""" 434 | if self._state is None: 435 | return None 436 | return self._state.get("HolidayMode", False) 437 | -------------------------------------------------------------------------------- /pymelcloud/client.py: -------------------------------------------------------------------------------- 1 | """MEL API access.""" 2 | from datetime import datetime, timedelta 3 | from typing import Any, Dict, List, Optional 4 | 5 | from aiohttp import ClientSession 6 | 7 | BASE_URL = "https://app.melcloud.com/Mitsubishi.Wifi.Client" 8 | 9 | 10 | def _headers(token: str) -> Dict[str, str]: 11 | return { 12 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:73.0) " 13 | "Gecko/20100101 Firefox/73.0", 14 | "Accept": "application/json, text/javascript, */*; q=0.01", 15 | "Accept-Language": "en-US,en;q=0.5", 16 | "Accept-Encoding": "gzip, deflate, br", 17 | "X-MitsContextKey": token, 18 | "X-Requested-With": "XMLHttpRequest", 19 | "Cookie": "policyaccepted=true", 20 | } 21 | 22 | 23 | async def _do_login(_session: ClientSession, email: str, password: str): 24 | body = { 25 | "Email": email, 26 | "Password": password, 27 | "Language": 0, 28 | "AppVersion": "1.19.1.1", 29 | "Persist": True, 30 | "CaptchaResponse": None, 31 | } 32 | 33 | async with _session.post( 34 | f"{BASE_URL}/Login/ClientLogin", json=body, raise_for_status=True 35 | ) as resp: 36 | return await resp.json() 37 | 38 | 39 | async def login( 40 | email: str, 41 | password: str, 42 | session: Optional[ClientSession] = None, 43 | *, 44 | user_update_interval: Optional[timedelta] = None, 45 | conf_update_interval: Optional[timedelta] = None, 46 | device_set_debounce: Optional[timedelta] = None, 47 | ): 48 | """Login using email and password.""" 49 | if session: 50 | response = await _do_login(session, email, password) 51 | else: 52 | async with ClientSession() as _session: 53 | response = await _do_login(_session, email, password) 54 | 55 | return Client( 56 | response.get("LoginData").get("ContextKey"), 57 | session, 58 | user_update_interval=user_update_interval, 59 | conf_update_interval=conf_update_interval, 60 | device_set_debounce=device_set_debounce, 61 | ) 62 | 63 | 64 | class Client: 65 | """MELCloud client. 66 | 67 | Please do not use this class directly. It is better to use the get_devices 68 | method exposed by the __init__.py. 69 | """ 70 | 71 | def __init__( 72 | self, 73 | token: str, 74 | session: Optional[ClientSession] = None, 75 | *, 76 | user_update_interval=timedelta(minutes=5), 77 | conf_update_interval=timedelta(seconds=59), 78 | device_set_debounce=timedelta(seconds=1), 79 | ): 80 | """Initialize MELCloud client.""" 81 | self._token = token 82 | if session: 83 | self._session = session 84 | self._managed_session = False 85 | else: 86 | self._session = ClientSession() 87 | self._managed_session = True 88 | self._user_update_interval = user_update_interval 89 | self._conf_update_interval = conf_update_interval 90 | self._device_set_debounce = device_set_debounce 91 | 92 | self._last_user_update = None 93 | self._last_conf_update = None 94 | self._device_confs: List[Dict[str, Any]] = [] 95 | self._account: Optional[Dict[str, Any]] = None 96 | 97 | @property 98 | def token(self) -> str: 99 | """Return currently used token.""" 100 | return self._token 101 | 102 | @property 103 | def device_confs(self) -> List[Dict[Any, Any]]: 104 | """Return device configurations.""" 105 | return self._device_confs 106 | 107 | @property 108 | def account(self) -> Optional[Dict[Any, Any]]: 109 | """Return account.""" 110 | return self._account 111 | 112 | async def _fetch_user_details(self): 113 | """Fetch user details.""" 114 | async with self._session.get( 115 | f"{BASE_URL}/User/GetUserDetails", 116 | headers=_headers(self._token), 117 | raise_for_status=True, 118 | ) as resp: 119 | self._account = await resp.json() 120 | 121 | async def _fetch_device_confs(self): 122 | """Fetch all configured devices.""" 123 | url = f"{BASE_URL}/User/ListDevices" 124 | async with self._session.get( 125 | url, headers=_headers(self._token), raise_for_status=True 126 | ) as resp: 127 | entries = await resp.json() 128 | new_devices = [] 129 | for entry in entries: 130 | new_devices = new_devices + entry["Structure"]["Devices"] 131 | 132 | for area in entry["Structure"]["Areas"]: 133 | new_devices = new_devices + area["Devices"] 134 | 135 | for floor in entry["Structure"]["Floors"]: 136 | new_devices = new_devices + floor["Devices"] 137 | 138 | for area in floor["Areas"]: 139 | new_devices = new_devices + area["Devices"] 140 | 141 | visited = set() 142 | self._device_confs = [ 143 | d 144 | for d in new_devices 145 | if d["DeviceID"] not in visited and not visited.add(d["DeviceID"]) 146 | ] 147 | 148 | async def update_confs(self): 149 | """Update device_confs and account. 150 | 151 | Calls are rate limited to allow Device instances to freely poll their own 152 | state while refreshing the device_confs list and account. 153 | """ 154 | now = datetime.now() 155 | 156 | if ( 157 | self._last_conf_update is None 158 | or now - self._last_conf_update > self._conf_update_interval 159 | ): 160 | await self._fetch_device_confs() 161 | self._last_conf_update = now 162 | 163 | if ( 164 | self._last_user_update is None 165 | or now - self._last_user_update > self._user_update_interval 166 | ): 167 | await self._fetch_user_details() 168 | self._last_user_update = now 169 | 170 | async def fetch_device_units(self, device) -> Optional[Dict[Any, Any]]: 171 | """Fetch unit information for a device. 172 | 173 | User provided info such as indoor/outdoor unit model names and 174 | serial numbers. 175 | """ 176 | async with self._session.post( 177 | f"{BASE_URL}/Device/ListDeviceUnits", 178 | headers=_headers(self._token), 179 | json={"deviceId": device.device_id}, 180 | raise_for_status=True, 181 | ) as resp: 182 | return await resp.json() 183 | 184 | async def fetch_device_state(self, device) -> Optional[Dict[Any, Any]]: 185 | """Fetch state information of a device. 186 | 187 | This method should not be called more than once a minute. Rate 188 | limiting is left to the caller. 189 | """ 190 | device_id = device.device_id 191 | building_id = device.building_id 192 | async with self._session.get( 193 | f"{BASE_URL}/Device/Get?id={device_id}&buildingID={building_id}", 194 | headers=_headers(self._token), 195 | raise_for_status=True, 196 | ) as resp: 197 | return await resp.json() 198 | 199 | async def fetch_energy_report(self, device) -> Optional[Dict[Any, Any]]: 200 | """Fetch energy report containing today and 1-2 days from the past.""" 201 | device_id = device.device_id 202 | from_str = (datetime.today() - timedelta(days=2)).strftime("%Y-%m-%d") 203 | to_str = (datetime.today() + timedelta(days=2)).strftime("%Y-%m-%d") 204 | 205 | async with self._session.post( 206 | f"{BASE_URL}/EnergyCost/Report", 207 | headers=_headers(self._token), 208 | json={ 209 | "DeviceId": device_id, 210 | "UseCurrency": False, 211 | "FromDate": f"{from_str}T00:00:00", 212 | "ToDate": f"{to_str}T00:00:00" 213 | }, 214 | raise_for_status=True, 215 | ) as resp: 216 | return await resp.json() 217 | 218 | async def set_device_state(self, device): 219 | """Update device state. 220 | 221 | This method is as dumb as it gets. Device is responsible for updating 222 | the state and managing EffectiveFlags. 223 | """ 224 | device_type = device.get("DeviceType") 225 | if device_type == 0: 226 | setter = "SetAta" 227 | elif device_type == 1: 228 | setter = "SetAtw" 229 | elif device_type == 3: 230 | setter = "SetErv" 231 | else: 232 | raise ValueError(f"Unsupported device type [{device_type}]") 233 | 234 | async with self._session.post( 235 | f"{BASE_URL}/Device/{setter}", 236 | headers=_headers(self._token), 237 | json=device, 238 | raise_for_status=True, 239 | ) as resp: 240 | return await resp.json() 241 | -------------------------------------------------------------------------------- /pymelcloud/const.py: -------------------------------------------------------------------------------- 1 | """Constants for pymelcloud.""" 2 | 3 | DEVICE_TYPE_ATA = "ata" 4 | DEVICE_TYPE_ATW = "atw" 5 | DEVICE_TYPE_ERV = "erv" 6 | DEVICE_TYPE_UNKNOWN = "unknown" 7 | 8 | ACCESS_LEVEL = { 9 | "GUEST": 3, 10 | "OWNER": 4, 11 | } 12 | 13 | DEVICE_TYPE_LOOKUP = { 14 | 0: DEVICE_TYPE_ATA, 15 | 1: DEVICE_TYPE_ATW, 16 | 3: DEVICE_TYPE_ERV, 17 | } 18 | 19 | UNIT_TEMP_CELSIUS = "celsius" 20 | UNIT_TEMP_FAHRENHEIT = "fahrenheit" 21 | -------------------------------------------------------------------------------- /pymelcloud/device.py: -------------------------------------------------------------------------------- 1 | """Base MELCloud device.""" 2 | import asyncio 3 | from abc import ABC, abstractmethod 4 | from datetime import datetime, timedelta, timezone 5 | from decimal import Decimal, ROUND_HALF_UP 6 | from typing import Any, Dict, List, Optional 7 | 8 | from pymelcloud.client import Client 9 | from pymelcloud.const import ( 10 | DEVICE_TYPE_LOOKUP, 11 | DEVICE_TYPE_UNKNOWN, 12 | UNIT_TEMP_CELSIUS, 13 | UNIT_TEMP_FAHRENHEIT, 14 | ACCESS_LEVEL, 15 | ) 16 | 17 | PROPERTY_POWER = "power" 18 | 19 | EFFECTIVE_FLAGS = "EffectiveFlags" 20 | HAS_PENDING_COMMAND = "HasPendingCommand" 21 | 22 | 23 | class Device(ABC): 24 | """MELCloud base device representation.""" 25 | 26 | def __init__( 27 | self, 28 | device_conf: Dict[str, Any], 29 | client: Client, 30 | set_debounce=timedelta(seconds=1), 31 | ): 32 | """Initialize a device.""" 33 | self.device_id = device_conf.get("DeviceID") 34 | self.building_id = device_conf.get("BuildingID") 35 | self.mac = device_conf.get("MacAddress") 36 | self.serial = device_conf.get("SerialNumber") 37 | self.access_level = device_conf.get("AccessLevel") 38 | 39 | self._use_fahrenheit = False 40 | if client.account is not None: 41 | self._use_fahrenheit = client.account.get("UseFahrenheit", False) 42 | 43 | self._device_conf = device_conf 44 | self._state = None 45 | self._device_units = None 46 | self._energy_report = None 47 | self._client = client 48 | 49 | self._set_debounce = set_debounce 50 | self._set_event = asyncio.Event() 51 | self._write_task: Optional[asyncio.Future[None]] = None 52 | self._pending_writes: Dict[str, Any] = {} 53 | 54 | def get_device_prop(self, name: str) -> Optional[Any]: 55 | """Access device properties while shortcutting the nested device access.""" 56 | device = self._device_conf.get("Device", {}) 57 | return device.get(name) 58 | 59 | def get_state_prop(self, name: str) -> Optional[Any]: 60 | """Access state prop without None check.""" 61 | if self._state is None: 62 | return None 63 | return self._state.get(name) 64 | 65 | def round_temperature(self, temperature: float) -> float: 66 | """Round a temperature to the nearest temperature increment.""" 67 | return float( 68 | Decimal(str(temperature / self.temperature_increment)) 69 | .quantize(Decimal('1'), rounding=ROUND_HALF_UP) 70 | ) * self.temperature_increment 71 | 72 | @abstractmethod 73 | def apply_write(self, state: Dict[str, Any], key: str, value: Any): 74 | """Apply writes to state object. 75 | 76 | Used for property validation, do not modify device state. 77 | """ 78 | pass 79 | 80 | async def update(self): 81 | """Fetch state of the device from MELCloud. 82 | 83 | List of device_confs is also updated. 84 | 85 | Please, rate limit calls to this method. Polling every 60 seconds should be 86 | enough to catch all events at the rate they are coming in to MELCloud with the 87 | exception of changes performed through MELCloud directly. 88 | """ 89 | await self._client.update_confs() 90 | self._device_conf = next( 91 | c 92 | for c in self._client.device_confs 93 | if c.get("DeviceID") == self.device_id 94 | and c.get("BuildingID") == self.building_id 95 | ) 96 | self._state = await self._client.fetch_device_state(self) 97 | self._energy_report = await self._client.fetch_energy_report(self) 98 | 99 | if self._device_units is None and self.access_level != ACCESS_LEVEL.get( 100 | "GUEST" 101 | ): 102 | self._device_units = await self._client.fetch_device_units(self) 103 | 104 | async def set(self, properties: Dict[str, Any]): 105 | """Schedule property write to MELCloud.""" 106 | if self._write_task is not None: 107 | self._write_task.cancel() 108 | 109 | for k, value in properties.items(): 110 | if k == PROPERTY_POWER: 111 | continue 112 | self.apply_write({}, k, value) 113 | 114 | self._pending_writes.update(properties) 115 | 116 | self._write_task = asyncio.ensure_future(self._write()) 117 | await self._set_event.wait() 118 | 119 | async def _write(self): 120 | await asyncio.sleep(self._set_debounce.total_seconds()) 121 | new_state = self._state.copy() 122 | 123 | for k, value in self._pending_writes.items(): 124 | if k == PROPERTY_POWER: 125 | new_state["Power"] = value 126 | new_state[EFFECTIVE_FLAGS] = new_state.get(EFFECTIVE_FLAGS, 0) | 0x01 127 | else: 128 | self.apply_write(new_state, k, value) 129 | 130 | if new_state[EFFECTIVE_FLAGS] != 0: 131 | new_state.update({HAS_PENDING_COMMAND: True}) 132 | 133 | self._pending_writes = {} 134 | self._state = await self._client.set_device_state(new_state) 135 | self._set_event.set() 136 | self._set_event.clear() 137 | 138 | @property 139 | def name(self) -> str: 140 | """Return device name.""" 141 | return self._device_conf["DeviceName"] 142 | 143 | @property 144 | def device_type(self) -> str: 145 | """Return type of the device.""" 146 | return DEVICE_TYPE_LOOKUP.get( 147 | self._device_conf.get("Device", {}).get("DeviceType", -1), 148 | DEVICE_TYPE_UNKNOWN, 149 | ) 150 | 151 | @property 152 | def units(self) -> Optional[List[dict]]: 153 | """Return device model info.""" 154 | if self._device_units is None: 155 | return None 156 | 157 | infos: List[dict] = [] 158 | for unit in self._device_units: 159 | infos.append( 160 | { 161 | "model_number": unit.get("ModelNumber"), 162 | "model": unit.get("Model"), 163 | "serial_number": unit.get("SerialNumber"), 164 | } 165 | ) 166 | return infos 167 | 168 | @property 169 | def temp_unit(self) -> str: 170 | """Return temperature unit used by the device.""" 171 | if self._use_fahrenheit: 172 | return UNIT_TEMP_FAHRENHEIT 173 | return UNIT_TEMP_CELSIUS 174 | 175 | @property 176 | def temperature_increment(self) -> float: 177 | """Return temperature increment.""" 178 | return self._device_conf.get("Device", {}).get("TemperatureIncrement", 0.5) 179 | 180 | @property 181 | def last_seen(self) -> Optional[datetime]: 182 | """Return timestamp of the last communication from device to MELCloud. 183 | 184 | The timestamp is in UTC. 185 | """ 186 | if self._state is None: 187 | return None 188 | return datetime.strptime( 189 | self._state.get("LastCommunication"), "%Y-%m-%dT%H:%M:%S.%f" 190 | ).replace(tzinfo=timezone.utc) 191 | 192 | @property 193 | def power(self) -> Optional[bool]: 194 | """Return power on / standby state of the device.""" 195 | if self._state is None: 196 | return None 197 | return self._state.get("Power") 198 | 199 | @property 200 | def daily_energy_consumed(self) -> Optional[float]: 201 | """Return daily energy consumption for the current day in kWh. 202 | 203 | The value resets at midnight MELCloud time. The logic here is a bit iffy and 204 | fragmented between Device and Client. Here's how it goes: 205 | - Client requests a 5 day report. Today, 2 days from the past and 2 days from 206 | the past. 207 | - MELCloud, with its clock potentially set to a different timezone than the 208 | client, returns a report containing data from a couple of days from the 209 | past and from the current day in MELCloud time. 210 | - Device sums up the date from the last day bucket in the report. 211 | 212 | TLDR: Request some days from the past and some days from the future -> receive 213 | the latest day bucket. 214 | """ 215 | if self._energy_report is None: 216 | return None 217 | 218 | consumption = 0 219 | 220 | for mode in ['Heating', 'Cooling', 'Auto', 'Dry', 'Fan', 'Other']: 221 | previous_reports = self._energy_report.get(mode, [0.0]) 222 | if previous_reports: 223 | last_report = previous_reports[-1] 224 | else: 225 | last_report = 0.0 226 | consumption += last_report 227 | 228 | return consumption 229 | 230 | @property 231 | def wifi_signal(self) -> Optional[int]: 232 | """Return wifi signal in dBm (negative value).""" 233 | if self._device_conf is None: 234 | return None 235 | return self._device_conf.get("Device", {}).get("WifiSignalStrength", None) 236 | 237 | @property 238 | def has_error(self) -> bool: 239 | """Return True if the device has error state.""" 240 | if self._state is None: 241 | return False 242 | return self._state.get("HasError", False) 243 | 244 | @property 245 | def error_code(self) -> Optional[str]: 246 | """Return error_code. 247 | This is a property that probably should be checked if "has_error" = true 248 | Till now I have a fixed code = 8000 and never have error on the units 249 | """ 250 | if self._state is None: 251 | return None 252 | return self._state.get("ErrorCode", None) 253 | -------------------------------------------------------------------------------- /pymelcloud/erv_device.py: -------------------------------------------------------------------------------- 1 | """Energy-Recovery-Ventilation (DeviceType=3) device definition.""" 2 | from typing import Any, Dict, List, Optional 3 | 4 | from pymelcloud.device import EFFECTIVE_FLAGS, Device 5 | 6 | PROPERTY_VENTILATION_MODE = "ventilation_mode" 7 | PROPERTY_FAN_SPEED = "fan_speed" 8 | 9 | FAN_SPEED_UNDEFINED = "undefined" 10 | FAN_SPEED_STOPPED = "0" 11 | 12 | VENTILATION_MODE_RECOVERY = "recovery" 13 | VENTILATION_MODE_BYPASS = "bypass" 14 | VENTILATION_MODE_AUTO = "auto" 15 | VENTILATION_MODE_UNDEFINED = "undefined" 16 | 17 | _VENTILATION_MODE_LOOKUP = { 18 | 0: VENTILATION_MODE_RECOVERY, 19 | 1: VENTILATION_MODE_BYPASS, 20 | 2: VENTILATION_MODE_AUTO, 21 | } 22 | 23 | 24 | def _fan_speed_from(speed: int) -> str: 25 | if speed == -1: 26 | return FAN_SPEED_UNDEFINED 27 | if speed == 0: 28 | return FAN_SPEED_STOPPED 29 | return str(speed) 30 | 31 | 32 | def _fan_speed_to(speed: str) -> int: 33 | if speed == FAN_SPEED_UNDEFINED: 34 | return -1 35 | if speed == FAN_SPEED_STOPPED: 36 | return 0 37 | return int(speed) 38 | 39 | 40 | def _ventilation_mode_from(mode: int) -> str: 41 | return _VENTILATION_MODE_LOOKUP.get(mode, VENTILATION_MODE_UNDEFINED) 42 | 43 | 44 | def _ventilation_mode_to(mode: str) -> int: 45 | for k, value in _VENTILATION_MODE_LOOKUP.items(): 46 | if value == mode: 47 | return k 48 | raise ValueError(f"Invalid ventilation_mode [{mode}]") 49 | 50 | 51 | class ErvDevice(Device): 52 | """Energy-Recovery-Ventilation device.""" 53 | 54 | def apply_write(self, state: Dict[str, Any], key: str, value: Any): 55 | """Apply writes to state object. 56 | 57 | Used for property validation, do not modify device state. 58 | """ 59 | flags = state.get(EFFECTIVE_FLAGS, 0) 60 | 61 | if key == PROPERTY_VENTILATION_MODE: 62 | state["VentilationMode"] = _ventilation_mode_to(value) 63 | flags = flags | 0x04 64 | elif key == PROPERTY_FAN_SPEED: 65 | state["SetFanSpeed"] = _fan_speed_to(value) 66 | flags = flags | 0x08 67 | else: 68 | raise ValueError(f"Cannot set {key}, invalid property") 69 | 70 | state[EFFECTIVE_FLAGS] = flags 71 | 72 | def _device(self) -> Dict[str, Any]: 73 | return self._device_conf.get("Device", {}) 74 | 75 | @property 76 | def has_energy_consumed_meter(self) -> bool: 77 | """Return True if the device has an energy consumption meter.""" 78 | if self._device_conf is None: 79 | return False 80 | return self._device().get("HasEnergyConsumedMeter", False) 81 | 82 | @property 83 | def total_energy_consumed(self) -> Optional[float]: 84 | """Return total consumed energy as kWh. 85 | 86 | The update interval is extremely slow and inconsistent. Empirical evidence 87 | suggests can vary between 1h 30min and 3h. 88 | """ 89 | if self._device_conf is None: 90 | return None 91 | reading = self._device().get("CurrentEnergyConsumed", None) 92 | if reading is None: 93 | return None 94 | return reading / 1000.0 95 | 96 | @property 97 | def presets(self) -> List[Dict[Any, Any]]: 98 | """Return presets configuration (preset created using melcloud app).""" 99 | retval = [] 100 | if self._device_conf is not None: 101 | presets_conf = self._device_conf.get("Presets", {}) 102 | for p in presets_conf: 103 | retval.append(p) 104 | 105 | return retval 106 | 107 | @property 108 | def room_temperature(self) -> Optional[float]: 109 | """Return room temperature reported by the device.""" 110 | if self._state is None: 111 | return None 112 | return self._state.get("RoomTemperature") 113 | 114 | @property 115 | def outside_temperature(self) -> Optional[float]: 116 | """Return outdoor temperature reported by the device.""" 117 | if self._state is None: 118 | return None 119 | return self._state.get("OutdoorTemperature") 120 | 121 | @property 122 | def ventilation_mode(self) -> Optional[str]: 123 | """Return currently active ventilation mode.""" 124 | if self._state is None: 125 | return None 126 | return _ventilation_mode_from(self._state.get("VentilationMode", -1)) 127 | 128 | @property 129 | def actual_ventilation_mode(self) -> Optional[str]: 130 | """Return actual ventilation mode.""" 131 | if self._state is None: 132 | return None 133 | return _ventilation_mode_from(self._device().get("ActualVentilationMode", -1)) 134 | 135 | @property 136 | def fan_speed(self) -> Optional[str]: 137 | """Return currently active fan speed. 138 | 139 | The argument must be one of the fan speeds returned by fan_speeds. 140 | """ 141 | if self._state is None: 142 | return None 143 | return _fan_speed_from(self._state.get("SetFanSpeed", -1)) 144 | 145 | @property 146 | def actual_supply_fan_speed(self) -> Optional[str]: 147 | """Return actual supply fan speed. 148 | 149 | The argument must be one of the fan speeds returned by fan_speeds. 150 | """ 151 | if self._state is None: 152 | return None 153 | return _fan_speed_from(self._device().get("ActualSupplyFanSpeed", -1)) 154 | 155 | @property 156 | def actual_exhaust_fan_speed(self) -> Optional[str]: 157 | """Return actual exhaust fan speed. 158 | 159 | The argument must be one of the fan speeds returned by fan_speeds. 160 | """ 161 | if self._state is None: 162 | return None 163 | return _fan_speed_from(self._device().get("ActualExhaustFanSpeed", -1)) 164 | 165 | @property 166 | def core_maintenance_required(self) -> bool: 167 | """Return True if core maintenance required.""" 168 | if self._device_conf is None: 169 | return False 170 | return self._device().get("CoreMaintenanceRequired", False) 171 | 172 | @property 173 | def filter_maintenance_required(self) -> bool: 174 | """Return True if filter maintenance required.""" 175 | if self._device_conf is None: 176 | return False 177 | return self._device().get("FilterMaintenanceRequired", False) 178 | 179 | @property 180 | def night_purge_mode(self) -> bool: 181 | """Return True if NightPurgeMode.""" 182 | if self._device_conf is None: 183 | return False 184 | return self._device().get("NightPurgeMode", False) 185 | 186 | @property 187 | def room_co2_level(self) -> Optional[float]: 188 | """Return co2 level if supported by the device.""" 189 | if self._state is None: 190 | return None 191 | 192 | if not self._state.get("HasCO2Sensor", False): 193 | return None 194 | 195 | return self._device().get("RoomCO2Level", None) 196 | 197 | @property 198 | def fan_speeds(self) -> Optional[List[str]]: 199 | """Return available fan speeds. 200 | 201 | The supported fan speeds vary from device to device. The available modes are 202 | read from the Device capability attributes. 203 | 204 | For example, a 5 speed device with auto fan speed would produce the following 205 | list (formatted '"[pymelcloud]" -- "[device controls]"') 206 | 207 | - "auto" -- "auto" 208 | - "1" -- "silent" 209 | - "2" -- "1" 210 | - "3" -- "2" 211 | - "4" -- "3" 212 | - "5" -- "4" 213 | 214 | MELCloud is not aware of the device type making it infeasible to match the 215 | fan speed names with the device documentation. 216 | """ 217 | if self._state is None: 218 | return None 219 | speeds = [] 220 | 221 | num_fan_speeds = self._state.get("NumberOfFanSpeeds", 0) 222 | for num in range(1, num_fan_speeds + 1): 223 | speeds.append(_fan_speed_from(num)) 224 | 225 | return speeds 226 | 227 | @property 228 | def ventilation_modes(self) -> List[str]: 229 | """Return available ventilation modes.""" 230 | modes: List[str] = [VENTILATION_MODE_RECOVERY] 231 | 232 | device = self._device() 233 | 234 | if device.get("HasBypassVentilationMode", False): 235 | modes.append(VENTILATION_MODE_BYPASS) 236 | 237 | if device.get("HasAutoVentilationMode", False): 238 | modes.append(VENTILATION_MODE_AUTO) 239 | 240 | return modes 241 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | asynctest 3 | pre-commit 4 | pytest 5 | pytest-asyncio 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # noqa: D100 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name="pymelcloud", 7 | version="2.11.0", 8 | description="Python MELCloud interface", 9 | author="Vilppu Vuorinen", 10 | author_email="vilppu.jotain@gmail.com", 11 | license="MIT", 12 | url="https://github.com/vilppuvuorinen/pymelcloud", 13 | python_requires=">3.5", 14 | packages=["pymelcloud"], 15 | keywords=["homeautomation", "melcloud"], 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | "Topic :: Software Development :: Libraries :: Application Frameworks", 21 | "Topic :: Home Automation", 22 | ], 23 | install_requires=["aiohttp"], 24 | scripts=[], 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """pymelcloud tests.""" 2 | -------------------------------------------------------------------------------- /tests/samples/ata_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "EffectiveFlags": 0, 3 | "LocalIPAddress": null, 4 | "RoomTemperature": 28.0, 5 | "SetTemperature": 22.0, 6 | "SetFanSpeed": 3, 7 | "OperationMode": 3, 8 | "VaneHorizontal": 3, 9 | "VaneVertical": 0, 10 | "Name": null, 11 | "NumberOfFanSpeeds": 5, 12 | "WeatherObservations": [], 13 | "ErrorMessage": null, 14 | "ErrorCode": 8000, 15 | "DefaultHeatingSetTemperature": 23.0, 16 | "DefaultCoolingSetTemperature": 21.0, 17 | "HideVaneControls": false, 18 | "HideDryModeControl": false, 19 | "RoomTemperatureLabel": 0, 20 | "InStandbyMode": false, 21 | "TemperatureIncrementOverride": 0, 22 | "ProhibitSetTemperature": false, 23 | "ProhibitOperationMode": false, 24 | "ProhibitPower": false, 25 | "DeviceID": 222222, 26 | "DeviceType": 0, 27 | "LastCommunication": "2020-07-03T09:03:50.32", 28 | "NextCommunication": "2020-07-03T09:04:50.32", 29 | "Power": false, 30 | "HasPendingCommand": false, 31 | "Offline": true, 32 | "Scene": null, 33 | "SceneOwner": null 34 | } -------------------------------------------------------------------------------- /tests/samples/ata_guest_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "EffectiveFlags": 0, 3 | "LocalIPAddress": null, 4 | "RoomTemperature": 28.0, 5 | "SetTemperature": 22.0, 6 | "SetFanSpeed": 3, 7 | "OperationMode": 3, 8 | "VaneHorizontal": 3, 9 | "VaneVertical": 0, 10 | "Name": null, 11 | "NumberOfFanSpeeds": 5, 12 | "WeatherObservations": [], 13 | "ErrorMessage": null, 14 | "ErrorCode": 8000, 15 | "DefaultHeatingSetTemperature": 23.0, 16 | "DefaultCoolingSetTemperature": 21.0, 17 | "HideVaneControls": false, 18 | "HideDryModeControl": false, 19 | "RoomTemperatureLabel": 0, 20 | "InStandbyMode": false, 21 | "TemperatureIncrementOverride": 0, 22 | "ProhibitSetTemperature": false, 23 | "ProhibitOperationMode": false, 24 | "ProhibitPower": false, 25 | "DeviceID": 222222, 26 | "DeviceType": 0, 27 | "LastCommunication": "2020-07-03T09:03:50.32", 28 | "NextCommunication": "2020-07-03T09:04:50.32", 29 | "Power": false, 30 | "HasPendingCommand": false, 31 | "Offline": true, 32 | "Scene": null, 33 | "SceneOwner": null 34 | } 35 | -------------------------------------------------------------------------------- /tests/samples/ata_guest_listdevices.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 0, 3 | "DeviceName": "", 4 | "BuildingID": 0, 5 | "BuildingName": null, 6 | "FloorID": null, 7 | "FloorName": null, 8 | "AreaID": null, 9 | "AreaName": null, 10 | "ImageID": -10020, 11 | "InstallationDate": null, 12 | "LastServiceDate": null, 13 | "Presets": [], 14 | "OwnerID": null, 15 | "OwnerName": null, 16 | "OwnerEmail": null, 17 | "AccessLevel": 3, 18 | "DirectAccess": false, 19 | "EndDate": "2500-01-01T00:00:00", 20 | "Zone1Name": null, 21 | "Zone2Name": null, 22 | "MinTemperature": 0, 23 | "MaxTemperature": 40, 24 | "HideVaneControls": false, 25 | "HideDryModeControl": false, 26 | "HideRoomTemperature": false, 27 | "HideSupplyTemperature": false, 28 | "HideOutdoorTemperature": false, 29 | "BuildingCountry": null, 30 | "OwnerCountry": null, 31 | "AdaptorType": -1, 32 | "LinkedDevice": "11111111111111111111111111111", 33 | "Type": 0, 34 | "MacAddress": "cc:cc:cc:cc:cc:cc", 35 | "SerialNumber": "1111111111111111", 36 | "Device": { 37 | "ListHistory24Formatters": [], 38 | "DeviceType": 0, 39 | "CanCool": true, 40 | "CanHeat": true, 41 | "CanDry": true, 42 | "HasAutomaticFanSpeed": true, 43 | "AirDirectionFunction": true, 44 | "SwingFunction": true, 45 | "NumberOfFanSpeeds": 5, 46 | "UseTemperatureA": true, 47 | "TemperatureIncrementOverride": 0, 48 | "TemperatureIncrement": 0.5, 49 | "MinTempCoolDry": 16.0, 50 | "MaxTempCoolDry": 31.0, 51 | "MinTempHeat": 10.0, 52 | "MaxTempHeat": 31.0, 53 | "MinTempAutomatic": 16.0, 54 | "MaxTempAutomatic": 31.0, 55 | "LegacyDevice": false, 56 | "UnitSupportsStandbyMode": true, 57 | "HasWideVane": false, 58 | "ModelIsAirCurtain": false, 59 | "ModelSupportsFanSpeed": true, 60 | "ModelSupportsAuto": true, 61 | "ModelSupportsHeat": true, 62 | "ModelSupportsDry": true, 63 | "ModelSupportsVaneVertical": true, 64 | "ModelSupportsVaneHorizontal": true, 65 | "ModelSupportsWideVane": true, 66 | "ModelDisableEnergyReport": false, 67 | "ModelSupportsStandbyMode": true, 68 | "ModelSupportsEnergyReporting": true, 69 | "ProhibitSetTemperature": false, 70 | "ProhibitOperationMode": false, 71 | "ProhibitPower": false, 72 | "Power": false, 73 | "RoomTemperature": 28.0, 74 | "SetTemperature": 22.0, 75 | "ActualFanSpeed": 0, 76 | "FanSpeed": 3, 77 | "AutomaticFanSpeed": false, 78 | "VaneVerticalDirection": 2, 79 | "VaneVerticalSwing": false, 80 | "VaneHorizontalDirection": 3, 81 | "VaneHorizontalSwing": false, 82 | "OperationMode": 3, 83 | "EffectiveFlags": 0, 84 | "LastEffectiveFlags": 0, 85 | "InStandbyMode": false, 86 | "DefaultCoolingSetTemperature": 21.0, 87 | "DefaultHeatingSetTemperature": 23.0, 88 | "RoomTemperatureLabel": 0, 89 | "HeatingEnergyConsumedRate1": 0, 90 | "HeatingEnergyConsumedRate2": 0, 91 | "CoolingEnergyConsumedRate1": 0, 92 | "CoolingEnergyConsumedRate2": 0, 93 | "AutoEnergyConsumedRate1": 0, 94 | "AutoEnergyConsumedRate2": 0, 95 | "DryEnergyConsumedRate1": 0, 96 | "DryEnergyConsumedRate2": 0, 97 | "FanEnergyConsumedRate1": 0, 98 | "FanEnergyConsumedRate2": 0, 99 | "OtherEnergyConsumedRate1": 0, 100 | "OtherEnergyConsumedRate2": 0, 101 | "HasEnergyConsumedMeter": false, 102 | "CurrentEnergyConsumed": 0, 103 | "CurrentEnergyMode": 3, 104 | "CoolingDisabled": false, 105 | "MinPcycle": 1, 106 | "MaxPcycle": 1, 107 | "EffectivePCycle": 1, 108 | "MaxOutdoorUnits": 255, 109 | "MaxIndoorUnits": 255, 110 | "MaxTemperatureControlUnits": 0, 111 | "DeviceID": 0, 112 | "MacAddress": "cc:cc:cc:cc:cc:cc", 113 | "SerialNumber": "1111111111", 114 | "TimeZoneID": 0, 115 | "DiagnosticMode": 0, 116 | "DiagnosticEndDate": null, 117 | "ExpectedCommand": 1, 118 | "Owner": null, 119 | "DetectedCountry": null, 120 | "AdaptorType": -1, 121 | "FirmwareDeployment": null, 122 | "FirmwareUpdateAborted": false, 123 | "LinkedDevice": "11111111111111111111111", 124 | "WifiSignalStrength": -51, 125 | "WifiAdapterStatus": "NORMAL", 126 | "Position": "Unknown", 127 | "PCycle": 1, 128 | "RecordNumMax": 0, 129 | "LastTimeStamp": "2020-06-27T20:57:00", 130 | "ErrorCode": 8000, 131 | "HasError": false, 132 | "LastReset": "2020-06-25T06:57:18.41", 133 | "FlashWrites": 0, 134 | "Scene": null, 135 | "SSLExpirationDate": "2037-12-31T00:00:00", 136 | "SPTimeout": 1, 137 | "Passcode": null, 138 | "ServerCommunicationDisabled": false, 139 | "ConsecutiveUploadErrors": 0, 140 | "DoNotRespondAfter": null, 141 | "OwnerRoleAccessLevel": 1, 142 | "OwnerCountry": 0, 143 | "HideEnergyReport": false, 144 | "ExceptionHash": null, 145 | "ExceptionDate": null, 146 | "ExceptionCount": null, 147 | "Rate1StartTime": null, 148 | "Rate2StartTime": null, 149 | "ProtocolVersion": 0, 150 | "UnitVersion": 0, 151 | "FirmwareAppVersion": 11000, 152 | "FirmwareWebVersion": 0, 153 | "FirmwareWlanVersion": 0, 154 | "HasErrorMessages": false, 155 | "HasZone2": false, 156 | "Offline": false, 157 | "Units": [] 158 | }, 159 | "DiagnosticMode": 0, 160 | "DiagnosticEndDate": null, 161 | "Location": 0, 162 | "DetectedCountry": null, 163 | "Registrations": 1, 164 | "LocalIPAddress": null, 165 | "TimeZone": 0, 166 | "RegistReason": "STARTUP", 167 | "ExpectedCommand": 1, 168 | "RegistRetry": 0, 169 | "DateCreated": "2020-06-25T06:57:18.41", 170 | "FirmwareDeployment": null, 171 | "FirmwareUpdateAborted": false, 172 | "Permissions": { 173 | "CanSetOperationMode": true, 174 | "CanSetFanSpeed": true, 175 | "CanSetVaneDirection": true, 176 | "CanSetPower": true, 177 | "CanSetTemperatureIncrementOverride": true, 178 | "CanDisableLocalController": true 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/samples/ata_listdevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 0, 3 | "DeviceName": "", 4 | "BuildingID": 0, 5 | "BuildingName": null, 6 | "FloorID": null, 7 | "FloorName": null, 8 | "AreaID": null, 9 | "AreaName": null, 10 | "ImageID": -10020, 11 | "InstallationDate": null, 12 | "LastServiceDate": null, 13 | "Presets": [], 14 | "OwnerID": null, 15 | "OwnerName": null, 16 | "OwnerEmail": null, 17 | "AccessLevel": 4, 18 | "DirectAccess": false, 19 | "EndDate": "2500-01-01T00:00:00", 20 | "Zone1Name": null, 21 | "Zone2Name": null, 22 | "MinTemperature": 0, 23 | "MaxTemperature": 40, 24 | "HideVaneControls": false, 25 | "HideDryModeControl": false, 26 | "HideRoomTemperature": false, 27 | "HideSupplyTemperature": false, 28 | "HideOutdoorTemperature": false, 29 | "BuildingCountry": null, 30 | "OwnerCountry": null, 31 | "AdaptorType": -1, 32 | "LinkedDevice": "11111111111111111111111111111", 33 | "Type": 0, 34 | "MacAddress": "cc:cc:cc:cc:cc:cc", 35 | "SerialNumber": "1111111111111111", 36 | "Device": { 37 | "ListHistory24Formatters": [], 38 | "DeviceType": 0, 39 | "CanCool": true, 40 | "CanHeat": true, 41 | "CanDry": true, 42 | "HasAutomaticFanSpeed": true, 43 | "AirDirectionFunction": true, 44 | "SwingFunction": true, 45 | "NumberOfFanSpeeds": 5, 46 | "UseTemperatureA": true, 47 | "TemperatureIncrementOverride": 0, 48 | "TemperatureIncrement": 0.5, 49 | "MinTempCoolDry": 16.0, 50 | "MaxTempCoolDry": 31.0, 51 | "MinTempHeat": 10.0, 52 | "MaxTempHeat": 31.0, 53 | "MinTempAutomatic": 16.0, 54 | "MaxTempAutomatic": 31.0, 55 | "LegacyDevice": false, 56 | "UnitSupportsStandbyMode": true, 57 | "HasWideVane": false, 58 | "ModelIsAirCurtain": false, 59 | "ModelSupportsFanSpeed": true, 60 | "ModelSupportsAuto": true, 61 | "ModelSupportsHeat": true, 62 | "ModelSupportsDry": true, 63 | "ModelSupportsVaneVertical": true, 64 | "ModelSupportsVaneHorizontal": true, 65 | "ModelSupportsWideVane": true, 66 | "ModelDisableEnergyReport": false, 67 | "ModelSupportsStandbyMode": true, 68 | "ModelSupportsEnergyReporting": true, 69 | "ProhibitSetTemperature": false, 70 | "ProhibitOperationMode": false, 71 | "ProhibitPower": false, 72 | "Power": false, 73 | "RoomTemperature": 28.0, 74 | "SetTemperature": 22.0, 75 | "ActualFanSpeed": 0, 76 | "FanSpeed": 3, 77 | "AutomaticFanSpeed": false, 78 | "VaneVerticalDirection": 2, 79 | "VaneVerticalSwing": false, 80 | "VaneHorizontalDirection": 3, 81 | "VaneHorizontalSwing": false, 82 | "OperationMode": 3, 83 | "EffectiveFlags": 0, 84 | "LastEffectiveFlags": 0, 85 | "InStandbyMode": false, 86 | "DefaultCoolingSetTemperature": 21.0, 87 | "DefaultHeatingSetTemperature": 23.0, 88 | "RoomTemperatureLabel": 0, 89 | "HeatingEnergyConsumedRate1": 0, 90 | "HeatingEnergyConsumedRate2": 0, 91 | "CoolingEnergyConsumedRate1": 0, 92 | "CoolingEnergyConsumedRate2": 0, 93 | "AutoEnergyConsumedRate1": 0, 94 | "AutoEnergyConsumedRate2": 0, 95 | "DryEnergyConsumedRate1": 0, 96 | "DryEnergyConsumedRate2": 0, 97 | "FanEnergyConsumedRate1": 0, 98 | "FanEnergyConsumedRate2": 0, 99 | "OtherEnergyConsumedRate1": 0, 100 | "OtherEnergyConsumedRate2": 0, 101 | "HasEnergyConsumedMeter": false, 102 | "CurrentEnergyConsumed": 0, 103 | "CurrentEnergyMode": 3, 104 | "CoolingDisabled": false, 105 | "MinPcycle": 1, 106 | "MaxPcycle": 1, 107 | "EffectivePCycle": 1, 108 | "MaxOutdoorUnits": 255, 109 | "MaxIndoorUnits": 255, 110 | "MaxTemperatureControlUnits": 0, 111 | "DeviceID": 0, 112 | "MacAddress": "cc:cc:cc:cc:cc:cc", 113 | "SerialNumber": "1111111111", 114 | "TimeZoneID": 0, 115 | "DiagnosticMode": 0, 116 | "DiagnosticEndDate": null, 117 | "ExpectedCommand": 1, 118 | "Owner": null, 119 | "DetectedCountry": null, 120 | "AdaptorType": -1, 121 | "FirmwareDeployment": null, 122 | "FirmwareUpdateAborted": false, 123 | "LinkedDevice": "11111111111111111111111", 124 | "WifiSignalStrength": -51, 125 | "WifiAdapterStatus": "NORMAL", 126 | "Position": "Unknown", 127 | "PCycle": 1, 128 | "RecordNumMax": 0, 129 | "LastTimeStamp": "2020-06-27T20:57:00", 130 | "ErrorCode": 8000, 131 | "HasError": false, 132 | "LastReset": "2020-06-25T06:57:18.41", 133 | "FlashWrites": 0, 134 | "Scene": null, 135 | "SSLExpirationDate": "2037-12-31T00:00:00", 136 | "SPTimeout": 1, 137 | "Passcode": null, 138 | "ServerCommunicationDisabled": false, 139 | "ConsecutiveUploadErrors": 0, 140 | "DoNotRespondAfter": null, 141 | "OwnerRoleAccessLevel": 1, 142 | "OwnerCountry": 0, 143 | "HideEnergyReport": false, 144 | "ExceptionHash": null, 145 | "ExceptionDate": null, 146 | "ExceptionCount": null, 147 | "Rate1StartTime": null, 148 | "Rate2StartTime": null, 149 | "ProtocolVersion": 0, 150 | "UnitVersion": 0, 151 | "FirmwareAppVersion": 11000, 152 | "FirmwareWebVersion": 0, 153 | "FirmwareWlanVersion": 0, 154 | "HasErrorMessages": false, 155 | "HasZone2": false, 156 | "Offline": false, 157 | "Units": [] 158 | }, 159 | "DiagnosticMode": 0, 160 | "DiagnosticEndDate": null, 161 | "Location": 0, 162 | "DetectedCountry": null, 163 | "Registrations": 1, 164 | "LocalIPAddress": null, 165 | "TimeZone": 0, 166 | "RegistReason": "STARTUP", 167 | "ExpectedCommand": 1, 168 | "RegistRetry": 0, 169 | "DateCreated": "2020-06-25T06:57:18.41", 170 | "FirmwareDeployment": null, 171 | "FirmwareUpdateAborted": false, 172 | "Permissions": { 173 | "CanSetOperationMode": true, 174 | "CanSetFanSpeed": true, 175 | "CanSetVaneDirection": true, 176 | "CanSetPower": true, 177 | "CanSetTemperatureIncrementOverride": true, 178 | "CanDisableLocalController": true 179 | } 180 | } -------------------------------------------------------------------------------- /tests/samples/atw_1zone_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "TemperatureIncrement": 0.5, 3 | "DefrostMode": 0, 4 | "HeatPumpFrequency": 50, 5 | "MaxSetTemperature": 50, 6 | "MinSetTemperature": 30, 7 | "RoomTemperatureZone1": 27, 8 | "RoomTemperatureZone2": -39, 9 | "TankWaterTemperature": 52, 10 | "UnitStatus": 0, 11 | "HeatingFunctionEnabled": true, 12 | "ServerTimerEnabled": false, 13 | "ThermostatStatusZone1": false, 14 | "ThermostatStatusZone2": false, 15 | "ThermostatTypeZone1": 0, 16 | "ThermostatTypeZone2": 2, 17 | "MixingTankWaterTemperature": 0, 18 | "CondensingTemperature": 0, 19 | "EffectiveFlags": 0, 20 | "LastEffectiveFlags": 0, 21 | "Power": true, 22 | "EcoHotWater": true, 23 | "OperationMode": 2, 24 | "OperationModeZone1": 1, 25 | "OperationModeZone2": 2, 26 | "SetTemperatureZone1": 30, 27 | "SetTemperatureZone2": 20, 28 | "SetTankWaterTemperature": 50, 29 | "TargetHCTemperatureZone1": 60, 30 | "TargetHCTemperatureZone2": 35, 31 | "ForcedHotWaterMode": false, 32 | "HolidayMode": false, 33 | "ProhibitHotWater": false, 34 | "ProhibitHeatingZone1": false, 35 | "ProhibitHeatingZone2": false, 36 | "ProhibitCoolingZone1": false, 37 | "ProhibitCoolingZone2": false, 38 | "ServerTimerDesired": false, 39 | "SecondaryZoneHeatCurve": false, 40 | "SetHeatFlowTemperatureZone1": 60, 41 | "SetHeatFlowTemperatureZone2": 0, 42 | "SetCoolFlowTemperatureZone1": 0, 43 | "SetCoolFlowTemperatureZone2": 0, 44 | "ThermostatTemperatureZone1": 0, 45 | "ThermostatTemperatureZone2": 0, 46 | "DECCReport": false, 47 | "CSVReport1min": false, 48 | "Zone2Master": false, 49 | "DailyEnergyConsumedDate": "2020-01-01T00:00:00", 50 | "DailyEnergyProducedDate": "2020-01-01T00:00:00", 51 | "CurrentEnergyConsumed": 2, 52 | "CurrentEnergyProduced": 9, 53 | "CurrentEnergyMode": null, 54 | "HeatingEnergyConsumedRate1": 2, 55 | "HeatingEnergyConsumedRate2": 0, 56 | "CoolingEnergyConsumedRate1": 0, 57 | "CoolingEnergyConsumedRate2": 0, 58 | "HotWaterEnergyConsumedRate1": 0, 59 | "HotWaterEnergyConsumedRate2": 0, 60 | "HeatingEnergyProducedRate1": 9, 61 | "HeatingEnergyProducedRate2": 0, 62 | "CoolingEnergyProducedRate1": 0, 63 | "CoolingEnergyProducedRate2": 0, 64 | "HotWaterEnergyProducedRate1": 0, 65 | "HotWaterEnergyProducedRate2": 0, 66 | "ErrorCode2Digit": 0, 67 | "SendSpecialFunctions": 0, 68 | "RequestSpecialFunctions": 0, 69 | "SpecialFunctionsState": 0, 70 | "PendingSendSpecialFunctions": 0, 71 | "PendingRequestSpecialFunctions": 0, 72 | "HasZone2": false, 73 | "HasSimplifiedZone2": false, 74 | "CanHeat": true, 75 | "CanCool": false, 76 | "HasHotWaterTank": true, 77 | "CanSetTankTemperature": true, 78 | "CanSetEcoHotWater": true, 79 | "HasEnergyConsumedMeter": true, 80 | "HasEnergyProducedMeter": true, 81 | "CanMeasureEnergyProduced": false, 82 | "CanMeasureEnergyConsumed": false, 83 | "Zone1InRoomMode": false, 84 | "Zone2InRoomMode": false, 85 | "Zone1InHeatMode": true, 86 | "Zone2InHeatMode": true, 87 | "Zone1InCoolMode": false, 88 | "Zone2InCoolMode": false, 89 | "AllowDualRoomTemperature": false, 90 | "IsGeodan": false, 91 | "HasEcoCuteSettings": false, 92 | "HasFTC45Settings": true, 93 | "HasFTC6Settings": false, 94 | "CanEstimateEnergyUsage": true, 95 | "CanUseRoomTemperatureCooling": false, 96 | "IsFtcModelSupported": true, 97 | "MaxTankTemperature": 60, 98 | "IdleZone1": false, 99 | "IdleZone2": true, 100 | "MinPcycle": 1, 101 | "MaxPcycle": 1, 102 | "MaxOutdoorUnits": 255, 103 | "MaxIndoorUnits": 255, 104 | "MaxTemperatureControlUnits": 0, 105 | "DeviceID": 1, 106 | "MacAddress": "de:ad:be:ef:11:11", 107 | "SerialNumber": "1111111111", 108 | "TimeZoneID": 1, 109 | "DiagnosticMode": 0, 110 | "DiagnosticEndDate": null, 111 | "ExpectedCommand": 1, 112 | "Owner": null, 113 | "DetectedCountry": null, 114 | "AdaptorType": -1, 115 | "FirmwareDeployment": null, 116 | "FirmwareUpdateAborted": false, 117 | "LinkedDevice": null, 118 | "WifiSignalStrength": -73, 119 | "WifiAdapterStatus": "NORMAL", 120 | "Position": "Unknown", 121 | "PCycle": 1, 122 | "RecordNumMax": 1, 123 | "LastTimeStamp": "2020-01-01T00:00:00", 124 | "ErrorCode": 8000, 125 | "HasError": false, 126 | "LastReset": "2020-01-01T00:00:00.00", 127 | "FlashWrites": 0, 128 | "Scene": null, 129 | "TemperatureIncrementOverride": 0, 130 | "SSLExpirationDate": "2040-12-31T00:00:00", 131 | "SPTimeout": 1, 132 | "Passcode": null, 133 | "ServerCommunicationDisabled": false, 134 | "ConsecutiveUploadErrors": 0, 135 | "DoNotRespondAfter": null, 136 | "OwnerRoleAccessLevel": 1, 137 | "OwnerCountry": 1, 138 | "HideEnergyReport": false, 139 | "ExceptionHash": null, 140 | "ExceptionDate": null, 141 | "ExceptionCount": null, 142 | "Rate1StartTime": null, 143 | "Rate2StartTime": null, 144 | "ProtocolVersion": 0, 145 | "UnitVersion": 0, 146 | "FirmwareAppVersion": 11000, 147 | "FirmwareWebVersion": 0, 148 | "FirmwareWlanVersion": 0, 149 | "EffectivePCycle": 1, 150 | "HasErrorMessages": false, 151 | "Offline": false, 152 | "Units": [ 153 | ] 154 | } -------------------------------------------------------------------------------- /tests/samples/atw_1zone_listdevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 1, 3 | "DeviceName": "Heater and Water", 4 | "BuildingID": 2, 5 | "BuildingName": null, 6 | "FloorID": 3, 7 | "FloorName": null, 8 | "AreaID": 4, 9 | "AreaName": null, 10 | "ImageID": 5, 11 | "InstallationDate": "2020-01-01T00:00:00", 12 | "LastServiceDate": "2020-01-01T00:00:00", 13 | "Presets": [ 14 | ], 15 | "OwnerID": null, 16 | "OwnerName": null, 17 | "OwnerEmail": null, 18 | "AccessLevel": 4, 19 | "DirectAccess": false, 20 | "EndDate": "2500-01-01T00:00:00", 21 | "Zone1Name": null, 22 | "Zone2Name": null, 23 | "MinTemperature": 0, 24 | "MaxTemperature": 40, 25 | "HideVaneControls": false, 26 | "HideDryModeControl": false, 27 | "HideRoomTemperature": false, 28 | "HideSupplyTemperature": false, 29 | "HideOutdoorTemperature": false, 30 | "BuildingCountry": null, 31 | "OwnerCountry": null, 32 | "AdaptorType": -1, 33 | "LinkedDevice": null, 34 | "Type": 1, 35 | "MacAddress": "00:0a:95:9d:68:16", 36 | "SerialNumber": "111111111111", 37 | "Device": { 38 | "ListHistory24Formatters": [ 39 | ], 40 | "DeviceType": 1, 41 | "FTCVersion": 1700, 42 | "FTCRevision": "r0 ", 43 | "LastFTCVersion": 1700, 44 | "LastFTCRevision": "r0 ", 45 | "FTCModel": 2, 46 | "RefridgerentAddress": 0, 47 | "DipSwitch1": 38, 48 | "DipSwitch2": 133, 49 | "DipSwitch3": 80, 50 | "DipSwitch4": 0, 51 | "DipSwitch5": 0, 52 | "DipSwitch6": 0, 53 | "HasThermostatZone1": true, 54 | "HasThermostatZone2": true, 55 | "TemperatureIncrement": 0.5, 56 | "DefrostMode": 0, 57 | "HeatPumpFrequency": 50, 58 | "MaxSetTemperature": 50, 59 | "MinSetTemperature": 30, 60 | "RoomTemperatureZone1": 27, 61 | "RoomTemperatureZone2": -39, 62 | "OutdoorTemperature": 13, 63 | "FlowTemperature": 57.5, 64 | "FlowTemperatureZone1": 25, 65 | "FlowTemperatureZone2": 25, 66 | "FlowTemperatureBoiler": 25, 67 | "ReturnTemperature": 53, 68 | "ReturnTemperatureZone1": 25, 69 | "ReturnTemperatureZone2": 25, 70 | "ReturnTemperatureBoiler": 25, 71 | "BoilerStatus": false, 72 | "BoosterHeater1Status": false, 73 | "BoosterHeater2Status": false, 74 | "BoosterHeater2PlusStatus": false, 75 | "ImmersionHeaterStatus": false, 76 | "WaterPump1Status": true, 77 | "WaterPump2Status": true, 78 | "WaterPump3Status": false, 79 | "ValveStatus3Way": false, 80 | "ValveStatus2Way": true, 81 | "WaterPump4Status": false, 82 | "ValveStatus2Way2a": false, 83 | "ValveStatus2Way2b": false, 84 | "TankWaterTemperature": 52, 85 | "UnitStatus": 0, 86 | "HeatingFunctionEnabled": true, 87 | "ServerTimerEnabled": false, 88 | "ThermostatStatusZone1": false, 89 | "ThermostatStatusZone2": false, 90 | "ThermostatTypeZone1": 0, 91 | "ThermostatTypeZone2": 2, 92 | "MixingTankWaterTemperature": 0, 93 | "CondensingTemperature": 0, 94 | "EffectiveFlags": 0, 95 | "LastEffectiveFlags": 0, 96 | "Power": true, 97 | "EcoHotWater": true, 98 | "OperationMode": 2, 99 | "OperationModeZone1": 1, 100 | "OperationModeZone2": 2, 101 | "SetTemperatureZone1": 30, 102 | "SetTemperatureZone2": 20, 103 | "SetTankWaterTemperature": 50, 104 | "TargetHCTemperatureZone1": 60, 105 | "TargetHCTemperatureZone2": 35, 106 | "ForcedHotWaterMode": false, 107 | "HolidayMode": false, 108 | "ProhibitHotWater": false, 109 | "ProhibitHeatingZone1": false, 110 | "ProhibitHeatingZone2": false, 111 | "ProhibitCoolingZone1": false, 112 | "ProhibitCoolingZone2": false, 113 | "ServerTimerDesired": false, 114 | "SecondaryZoneHeatCurve": false, 115 | "SetHeatFlowTemperatureZone1": 60, 116 | "SetHeatFlowTemperatureZone2": 0, 117 | "SetCoolFlowTemperatureZone1": 0, 118 | "SetCoolFlowTemperatureZone2": 0, 119 | "ThermostatTemperatureZone1": 0, 120 | "ThermostatTemperatureZone2": 0, 121 | "DECCReport": false, 122 | "CSVReport1min": false, 123 | "Zone2Master": false, 124 | "DailyEnergyConsumedDate": "2020-01-01T00:00:00", 125 | "DailyEnergyProducedDate": "2020-01-01T00:00:00", 126 | "CurrentEnergyConsumed": 2, 127 | "CurrentEnergyProduced": 9, 128 | "CurrentEnergyMode": null, 129 | "HeatingEnergyConsumedRate1": 2, 130 | "HeatingEnergyConsumedRate2": 0, 131 | "CoolingEnergyConsumedRate1": 0, 132 | "CoolingEnergyConsumedRate2": 0, 133 | "HotWaterEnergyConsumedRate1": 0, 134 | "HotWaterEnergyConsumedRate2": 0, 135 | "HeatingEnergyProducedRate1": 9, 136 | "HeatingEnergyProducedRate2": 0, 137 | "CoolingEnergyProducedRate1": 0, 138 | "CoolingEnergyProducedRate2": 0, 139 | "HotWaterEnergyProducedRate1": 0, 140 | "HotWaterEnergyProducedRate2": 0, 141 | "ErrorCode2Digit": 0, 142 | "SendSpecialFunctions": 0, 143 | "RequestSpecialFunctions": 0, 144 | "SpecialFunctionsState": 0, 145 | "PendingSendSpecialFunctions": 0, 146 | "PendingRequestSpecialFunctions": 0, 147 | "HasZone2": false, 148 | "HasSimplifiedZone2": false, 149 | "CanHeat": true, 150 | "CanCool": false, 151 | "HasHotWaterTank": true, 152 | "CanSetTankTemperature": true, 153 | "CanSetEcoHotWater": true, 154 | "HasEnergyConsumedMeter": true, 155 | "HasEnergyProducedMeter": true, 156 | "CanMeasureEnergyProduced": false, 157 | "CanMeasureEnergyConsumed": false, 158 | "Zone1InRoomMode": false, 159 | "Zone2InRoomMode": false, 160 | "Zone1InHeatMode": true, 161 | "Zone2InHeatMode": true, 162 | "Zone1InCoolMode": false, 163 | "Zone2InCoolMode": false, 164 | "AllowDualRoomTemperature": false, 165 | "IsGeodan": false, 166 | "HasEcoCuteSettings": false, 167 | "HasFTC45Settings": true, 168 | "HasFTC6Settings": false, 169 | "CanEstimateEnergyUsage": true, 170 | "CanUseRoomTemperatureCooling": false, 171 | "IsFtcModelSupported": true, 172 | "MaxTankTemperature": 60, 173 | "IdleZone1": false, 174 | "IdleZone2": true, 175 | "MinPcycle": 1, 176 | "MaxPcycle": 1, 177 | "MaxOutdoorUnits": 255, 178 | "MaxIndoorUnits": 255, 179 | "MaxTemperatureControlUnits": 0, 180 | "DeviceID": 1, 181 | "MacAddress": "de:ad:be:ef:11:11", 182 | "SerialNumber": "1111111111", 183 | "TimeZoneID": 1, 184 | "DiagnosticMode": 0, 185 | "DiagnosticEndDate": null, 186 | "ExpectedCommand": 1, 187 | "Owner": null, 188 | "DetectedCountry": null, 189 | "AdaptorType": -1, 190 | "FirmwareDeployment": null, 191 | "FirmwareUpdateAborted": false, 192 | "LinkedDevice": null, 193 | "WifiSignalStrength": -73, 194 | "WifiAdapterStatus": "NORMAL", 195 | "Position": "Unknown", 196 | "PCycle": 1, 197 | "RecordNumMax": 1, 198 | "LastTimeStamp": "2020-01-01T00:00:00", 199 | "ErrorCode": 8000, 200 | "HasError": false, 201 | "LastReset": "2020-01-01T00:00:00.00", 202 | "FlashWrites": 0, 203 | "Scene": null, 204 | "TemperatureIncrementOverride": 0, 205 | "SSLExpirationDate": "2040-12-31T00:00:00", 206 | "SPTimeout": 1, 207 | "Passcode": null, 208 | "ServerCommunicationDisabled": false, 209 | "ConsecutiveUploadErrors": 0, 210 | "DoNotRespondAfter": null, 211 | "OwnerRoleAccessLevel": 1, 212 | "OwnerCountry": 1, 213 | "HideEnergyReport": false, 214 | "ExceptionHash": null, 215 | "ExceptionDate": null, 216 | "ExceptionCount": null, 217 | "Rate1StartTime": null, 218 | "Rate2StartTime": null, 219 | "ProtocolVersion": 0, 220 | "UnitVersion": 0, 221 | "FirmwareAppVersion": 11000, 222 | "FirmwareWebVersion": 0, 223 | "FirmwareWlanVersion": 0, 224 | "EffectivePCycle": 1, 225 | "HasErrorMessages": false, 226 | "Offline": false, 227 | "Units": [ 228 | ] 229 | }, 230 | "DiagnosticMode": 0, 231 | "DiagnosticEndDate": null, 232 | "Location": 2, 233 | "DetectedCountry": null, 234 | "Registrations": 1, 235 | "LocalIPAddress": null, 236 | "TimeZone": "", 237 | "RegistReason": "CONFIG", 238 | "ExpectedCommand": 1, 239 | "RegistRetry": 0, 240 | "DateCreated": "2020-01-01T00:00:00.00", 241 | "FirmwareDeployment": null, 242 | "FirmwareUpdateAborted": false, 243 | "Permissions": { 244 | "CanSetForcedHotWater": true, 245 | "CanSetOperationMode": true, 246 | "CanSetPower": true, 247 | "CanSetTankWaterTemperature": true, 248 | "CanSetEcoHotWater": false, 249 | "CanSetFlowTemperature": true, 250 | "CanSetTemperatureIncrementOverride": true 251 | } 252 | } -------------------------------------------------------------------------------- /tests/samples/atw_2zone_cancool_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "EffectiveFlags": 0, 3 | "LocalIPAddress": null, 4 | "SetTemperatureZone1": 20.5, 5 | "SetTemperatureZone2": 21.0, 6 | "RoomTemperatureZone1": 21.5, 7 | "RoomTemperatureZone2": 21.0, 8 | "OperationMode": 1, 9 | "OperationModeZone1": 2, 10 | "OperationModeZone2": 2, 11 | "WeatherObservations": [], 12 | "ErrorMessage": null, 13 | "ErrorCode": 8000, 14 | "SetHeatFlowTemperatureZone1": 5.0, 15 | "SetHeatFlowTemperatureZone2": 5.0, 16 | "SetCoolFlowTemperatureZone1": 25.0, 17 | "SetCoolFlowTemperatureZone2": 22.0, 18 | "HCControlType": 1, 19 | "TankWaterTemperature": 47.5, 20 | "SetTankWaterTemperature": 52.0, 21 | "ForcedHotWaterMode": false, 22 | "UnitStatus": 0, 23 | "OutdoorTemperature": 12.0, 24 | "EcoHotWater": false, 25 | "Zone1Name": null, 26 | "Zone2Name": null, 27 | "HolidayMode": false, 28 | "ProhibitZone1": false, 29 | "ProhibitZone2": false, 30 | "ProhibitHotWater": false, 31 | "TemperatureIncrementOverride": 0, 32 | "IdleZone1": true, 33 | "IdleZone2": true, 34 | "DeviceID": 1, 35 | "DeviceType": 1, 36 | "LastCommunication": "2020-01-01T12:00:00.000", 37 | "NextCommunication": "2020-01-01T12:00:00.000", 38 | "Power": true, 39 | "HasPendingCommand": false, 40 | "Offline": false, 41 | "Scene": null, 42 | "SceneOwner": null 43 | } -------------------------------------------------------------------------------- /tests/samples/atw_2zone_cancool_listdevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 1, 3 | "DeviceName": "Mitsubishi", 4 | "BuildingID": 2, 5 | "BuildingName": null, 6 | "FloorID": null, 7 | "FloorName": null, 8 | "AreaID": null, 9 | "AreaName": null, 10 | "ImageID": 1, 11 | "InstallationDate": "2020-01-01T00:00:00", 12 | "LastServiceDate": "2020-01-01T00:00:00", 13 | "Presets": [], 14 | "OwnerID": null, 15 | "OwnerName": null, 16 | "OwnerEmail": null, 17 | "AccessLevel": 4, 18 | "DirectAccess": false, 19 | "EndDate": "2500-01-01T00:00:00", 20 | "Zone1Name": null, 21 | "Zone2Name": null, 22 | "MinTemperature": 0, 23 | "MaxTemperature": 40, 24 | "HideVaneControls": false, 25 | "HideDryModeControl": false, 26 | "HideRoomTemperature": false, 27 | "HideSupplyTemperature": false, 28 | "HideOutdoorTemperature": false, 29 | "BuildingCountry": null, 30 | "OwnerCountry": null, 31 | "AdaptorType": -1, 32 | "LinkedDevice": null, 33 | "Type": 1, 34 | "MacAddress": "de:ad:be:ee:ee:ef", 35 | "SerialNumber": "1", 36 | "Device": { 37 | "ListHistory24Formatters": [], 38 | "DeviceType": 1, 39 | "FTCVersion": 1700, 40 | "FTCRevision": "r0 ", 41 | "LastFTCVersion": 1700, 42 | "LastFTCRevision": "r0 ", 43 | "FTCModel": 2, 44 | "RefridgerentAddress": 0, 45 | "DipSwitch1": 134, 46 | "DipSwitch2": 232, 47 | "DipSwitch3": 80, 48 | "DipSwitch4": 0, 49 | "HasThermostatZone1": true, 50 | "HasThermostatZone2": true, 51 | "TemperatureIncrement": 0.5, 52 | "DefrostMode": 0, 53 | "HeatPumpFrequency": 0, 54 | "MaxSetTemperature": 43.0, 55 | "MinSetTemperature": 25.0, 56 | "RoomTemperatureZone1": 21.0, 57 | "RoomTemperatureZone2": 21.0, 58 | "OutdoorTemperature": 18.0, 59 | "FlowTemperature": 50.5, 60 | "FlowTemperatureZone1": 22.0, 61 | "FlowTemperatureZone2": 21.0, 62 | "FlowTemperatureBoiler": 25.0, 63 | "ReturnTemperature": 50.5, 64 | "ReturnTemperatureZone1": 21.0, 65 | "ReturnTemperatureZone2": 21.0, 66 | "ReturnTemperatureBoiler": 25.0, 67 | "BoilerStatus": false, 68 | "BoosterHeater1Status": false, 69 | "BoosterHeater2Status": false, 70 | "BoosterHeater2PlusStatus": false, 71 | "ImmersionHeaterStatus": false, 72 | "WaterPump1Status": true, 73 | "WaterPump2Status": false, 74 | "WaterPump3Status": false, 75 | "ValveStatus3Way": true, 76 | "ValveStatus2Way": false, 77 | "WaterPump4Status": true, 78 | "ValveStatus2Way2a": false, 79 | "ValveStatus2Way2b": false, 80 | "TankWaterTemperature": 51.5, 81 | "UnitStatus": 0, 82 | "HeatingFunctionEnabled": true, 83 | "ServerTimerEnabled": false, 84 | "ThermostatStatusZone1": false, 85 | "ThermostatStatusZone2": false, 86 | "ThermostatTypeZone1": 1, 87 | "ThermostatTypeZone2": 1, 88 | "EffectiveFlags": 0, 89 | "LastEffectiveFlags": 0, 90 | "Power": true, 91 | "EcoHotWater": false, 92 | "OperationMode": 1, 93 | "OperationModeZone1": 2, 94 | "OperationModeZone2": 2, 95 | "SetTemperatureZone1": 20.5, 96 | "SetTemperatureZone2": 20.5, 97 | "SetTankWaterTemperature": 52.0, 98 | "TargetHCTemperatureZone1": 24.0, 99 | "TargetHCTemperatureZone2": 24.0, 100 | "ForcedHotWaterMode": false, 101 | "HolidayMode": false, 102 | "ProhibitHotWater": false, 103 | "ProhibitHeatingZone1": false, 104 | "ProhibitHeatingZone2": false, 105 | "ProhibitCoolingZone1": false, 106 | "ProhibitCoolingZone2": false, 107 | "ServerTimerDesired": false, 108 | "SecondaryZoneHeatCurve": false, 109 | "SetHeatFlowTemperatureZone1": 5.0, 110 | "SetHeatFlowTemperatureZone2": 5.0, 111 | "SetCoolFlowTemperatureZone1": 25.0, 112 | "SetCoolFlowTemperatureZone2": 22.0, 113 | "ThermostatTemperatureZone1": 0.0, 114 | "ThermostatTemperatureZone2": 0.0, 115 | "DECCReport": false, 116 | "CSVReport1min": false, 117 | "Zone2Master": false, 118 | "DailyEnergyConsumedDate": "2020-01-01T00:00:00", 119 | "DailyEnergyProducedDate": "2020-01-01T00:00:00", 120 | "CurrentEnergyConsumed": 0, 121 | "CurrentEnergyProduced": 1, 122 | "CurrentEnergyMode": null, 123 | "HeatingEnergyConsumedRate1": 0, 124 | "HeatingEnergyConsumedRate2": 0, 125 | "CoolingEnergyConsumedRate1": 0, 126 | "CoolingEnergyConsumedRate2": 0, 127 | "HotWaterEnergyConsumedRate1": 0, 128 | "HotWaterEnergyConsumedRate2": 0, 129 | "HeatingEnergyProducedRate1": 0, 130 | "HeatingEnergyProducedRate2": 0, 131 | "CoolingEnergyProducedRate1": 0, 132 | "CoolingEnergyProducedRate2": 0, 133 | "HotWaterEnergyProducedRate1": 1, 134 | "HotWaterEnergyProducedRate2": 0, 135 | "ErrorCode2Digit": 0, 136 | "SendSpecialFunctions": 0, 137 | "RequestSpecialFunctions": 0, 138 | "SpecialFunctionsState": 0, 139 | "PendingSendSpecialFunctions": 0, 140 | "PendingRequestSpecialFunctions": 0, 141 | "HasZone2": true, 142 | "HasSimplifiedZone2": false, 143 | "CanHeat": true, 144 | "CanCool": true, 145 | "HasHotWaterTank": true, 146 | "CanSetTankTemperature": true, 147 | "CanSetEcoHotWater": true, 148 | "HasEnergyConsumedMeter": true, 149 | "HasEnergyProducedMeter": true, 150 | "CanMeasureEnergyProduced": false, 151 | "CanMeasureEnergyConsumed": false, 152 | "Zone1InRoomMode": false, 153 | "Zone2InRoomMode": false, 154 | "Zone1InHeatMode": true, 155 | "Zone2InHeatMode": true, 156 | "Zone1InCoolMode": false, 157 | "Zone2InCoolMode": false, 158 | "AllowDualRoomTemperature": false, 159 | "HasEcoCuteSettings": false, 160 | "HasFTC45Settings": true, 161 | "CanEstimateEnergyUsage": true, 162 | "CanUseRoomTemperatureCooling": true, 163 | "IsFtcModelSupported": true, 164 | "MaxTankTemperature": 60.0, 165 | "IdleZone1": true, 166 | "IdleZone2": true, 167 | "MinPcycle": 1, 168 | "MaxPcycle": 1, 169 | "MaxOutdoorUnits": 255, 170 | "MaxIndoorUnits": 255, 171 | "MaxTemperatureControlUnits": 0, 172 | "DeviceID": 1, 173 | "MacAddress": "de:ad:be:ee:ee:ef", 174 | "SerialNumber": "1", 175 | "TimeZoneID": 119, 176 | "DiagnosticMode": 0, 177 | "DiagnosticEndDate": null, 178 | "ExpectedCommand": 1, 179 | "Owner": null, 180 | "DetectedCountry": null, 181 | "AdaptorType": -1, 182 | "FirmwareDeployment": null, 183 | "FirmwareUpdateAborted": false, 184 | "LinkedDevice": null, 185 | "WifiSignalStrength": -82, 186 | "WifiAdapterStatus": "NORMAL", 187 | "Position": "Unknown", 188 | "PCycle": 1, 189 | "RecordNumMax": 1, 190 | "LastTimeStamp": "2020-01-01T12:00:00", 191 | "ErrorCode": 8000, 192 | "HasError": false, 193 | "LastReset": "2020-01-01T12:00:00.00", 194 | "FlashWrites": 1, 195 | "Scene": null, 196 | "TemperatureIncrementOverride": 0, 197 | "SSLExpirationDate": "2040-01-01T00:00:00", 198 | "SPTimeout": 1, 199 | "Passcode": null, 200 | "ServerCommunicationDisabled": false, 201 | "ConsecutiveUploadErrors": 0, 202 | "DoNotRespondAfter": null, 203 | "OwnerRoleAccessLevel": 1, 204 | "OwnerCountry": 24, 205 | "HideEnergyReport": false, 206 | "Rate1StartTime": null, 207 | "Rate2StartTime": null, 208 | "ProtocolVersion": 0, 209 | "UnitVersion": 0, 210 | "FirmwareAppVersion": 14000, 211 | "FirmwareWebVersion": 0, 212 | "FirmwareWlanVersion": 0, 213 | "EffectivePCycle": 1, 214 | "HasErrorMessages": false, 215 | "Offline": false, 216 | "Units": [] 217 | }, 218 | "DiagnosticMode": 0, 219 | "DiagnosticEndDate": null, 220 | "Location": 1, 221 | "DetectedCountry": null, 222 | "Registrations": 1, 223 | "LocalIPAddress": null, 224 | "TimeZone": 1, 225 | "RegistReason": "STARTUP", 226 | "ExpectedCommand": 1, 227 | "RegistRetry": 1, 228 | "DateCreated": "2020-01-01T12:00:00.000", 229 | "FirmwareDeployment": null, 230 | "FirmwareUpdateAborted": false, 231 | "Permissions": { 232 | "CanSetForcedHotWater": true, 233 | "CanSetOperationMode": true, 234 | "CanSetPower": true, 235 | "CanSetTankWaterTemperature": true, 236 | "CanSetEcoHotWater": false, 237 | "CanSetFlowTemperature": true, 238 | "CanSetTemperatureIncrementOverride": true 239 | } 240 | } -------------------------------------------------------------------------------- /tests/samples/atw_2zone_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "EffectiveFlags": 0, 3 | "LocalIPAddress": null, 4 | "SetTemperatureZone1": 19.5, 5 | "SetTemperatureZone2": 18, 6 | "RoomTemperatureZone1": 20.5, 7 | "RoomTemperatureZone2": 19.5, 8 | "OperationMode": 2, 9 | "OperationModeZone1": 0, 10 | "OperationModeZone2": 0, 11 | "WeatherObservations": [], 12 | "ErrorMessage": null, 13 | "ErrorCode": 8000, 14 | "SetHeatFlowTemperatureZone1": 25, 15 | "SetHeatFlowTemperatureZone2": 25, 16 | "SetCoolFlowTemperatureZone1": 20, 17 | "SetCoolFlowTemperatureZone2": 20, 18 | "HCControlType": 1, 19 | "TankWaterTemperature": 49.5, 20 | "SetTankWaterTemperature": 50, 21 | "ForcedHotWaterMode": false, 22 | "UnitStatus": 0, 23 | "OutdoorTemperature": 5, 24 | "EcoHotWater": false, 25 | "Zone1Name": "Downstairs", 26 | "Zone2Name": "Upstairs", 27 | "HolidayMode": false, 28 | "ProhibitZone1": false, 29 | "ProhibitZone2": false, 30 | "ProhibitHotWater": false, 31 | "TemperatureIncrementOverride": 0, 32 | "IdleZone1": false, 33 | "IdleZone2": false, 34 | "DeviceID": 1, 35 | "DeviceType": 1, 36 | "LastCommunication": "2020-01-01T00:00:00.000", 37 | "NextCommunication": "2020-01-01T00:00:00.000", 38 | "Power": true, 39 | "HasPendingCommand": false, 40 | "Offline": false, 41 | "Scene": null, 42 | "SceneOwner": null 43 | } -------------------------------------------------------------------------------- /tests/samples/atw_2zone_listdevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 1, 3 | "DeviceName": "Home", 4 | "BuildingID": 2, 5 | "BuildingName": null, 6 | "FloorID": 1, 7 | "FloorName": null, 8 | "AreaID": null, 9 | "AreaName": null, 10 | "ImageID": 1, 11 | "InstallationDate": null, 12 | "LastServiceDate": null, 13 | "Presets": [{ 14 | "Power": true, 15 | "EcoHotWater": false, 16 | "OperationModeZone1": 0, 17 | "OperationModeZone2": 0, 18 | "SetTankWaterTemperature": 53.0, 19 | "SetTemperatureZone1": 20.0, 20 | "SetTemperatureZone2": 20.0, 21 | "ForcedHotWaterMode": false, 22 | "SetHeatFlowTemperatureZone1": 25.0, 23 | "SetHeatFlowTemperatureZone2": 25.0, 24 | "SetCoolFlowTemperatureZone1": 20.0, 25 | "SetCoolFlowTemperatureZone2": 20.0, 26 | "ID": 1, 27 | "Client": 1, 28 | "DeviceLocation": 1, 29 | "Number": 1, 30 | "Configuration": "truefalse00532020false25252020", 31 | "NumberDescription": "20°C" 32 | }, { 33 | "Power": true, 34 | "EcoHotWater": false, 35 | "OperationModeZone1": 0, 36 | "OperationModeZone2": 0, 37 | "SetTankWaterTemperature": 45.0, 38 | "SetTemperatureZone1": 19.5, 39 | "SetTemperatureZone2": 19.5, 40 | "ForcedHotWaterMode": false, 41 | "SetHeatFlowTemperatureZone1": 25.0, 42 | "SetHeatFlowTemperatureZone2": 25.0, 43 | "SetCoolFlowTemperatureZone1": 20.0, 44 | "SetCoolFlowTemperatureZone2": 20.0, 45 | "ID": 2, 46 | "Client": 1, 47 | "DeviceLocation": 1, 48 | "Number": 2, 49 | "Configuration": "truefalse004519.519.5false25252020", 50 | "NumberDescription": "19.5°C" 51 | }], 52 | "OwnerID": null, 53 | "OwnerName": null, 54 | "OwnerEmail": null, 55 | "AccessLevel": 4, 56 | "DirectAccess": false, 57 | "EndDate": "2500-01-01T00:00:00", 58 | "Zone1Name": "Downstairs", 59 | "Zone2Name": "Upstairs", 60 | "MinTemperature": 0, 61 | "MaxTemperature": 40, 62 | "HideVaneControls": false, 63 | "HideDryModeControl": false, 64 | "HideRoomTemperature": false, 65 | "HideSupplyTemperature": false, 66 | "HideOutdoorTemperature": false, 67 | "BuildingCountry": null, 68 | "OwnerCountry": null, 69 | "AdaptorType": 1, 70 | "LinkedDevice": null, 71 | "Type": 1, 72 | "MacAddress": "de:ad:be:ef:22:22", 73 | "SerialNumber": "22222222222", 74 | "Device": { 75 | "ListHistory24Formatters": [], 76 | "DeviceType": 1, 77 | "FTCVersion": 1500, 78 | "FTCRevision": "r0 ", 79 | "LastFTCVersion": 1500, 80 | "LastFTCRevision": "r0 ", 81 | "FTCModel": 2, 82 | "RefridgerentAddress": 0, 83 | "DipSwitch1": 206, 84 | "DipSwitch2": 192, 85 | "DipSwitch3": 112, 86 | "DipSwitch4": 0, 87 | "DipSwitch5": 0, 88 | "DipSwitch6": 0, 89 | "HasThermostatZone1": true, 90 | "HasThermostatZone2": true, 91 | "TemperatureIncrement": 0.5, 92 | "DefrostMode": 0, 93 | "HeatPumpFrequency": 30, 94 | "MaxSetTemperature": 50.0, 95 | "MinSetTemperature": 30.0, 96 | "RoomTemperatureZone1": 20.0, 97 | "RoomTemperatureZone2": 19.5, 98 | "OutdoorTemperature": 4.0, 99 | "FlowTemperature": 36.0, 100 | "FlowTemperatureZone1": 25.0, 101 | "FlowTemperatureZone2": 25.0, 102 | "FlowTemperatureBoiler": 25.0, 103 | "ReturnTemperature": 30.0, 104 | "ReturnTemperatureZone1": 25.0, 105 | "ReturnTemperatureZone2": 25.0, 106 | "ReturnTemperatureBoiler": 25.0, 107 | "BoilerStatus": false, 108 | "BoosterHeater1Status": false, 109 | "BoosterHeater2Status": false, 110 | "BoosterHeater2PlusStatus": false, 111 | "ImmersionHeaterStatus": false, 112 | "WaterPump1Status": true, 113 | "WaterPump2Status": true, 114 | "WaterPump3Status": false, 115 | "ValveStatus3Way": false, 116 | "ValveStatus2Way": false, 117 | "WaterPump4Status": false, 118 | "ValveStatus2Way2a": true, 119 | "ValveStatus2Way2b": true, 120 | "TankWaterTemperature": 50.0, 121 | "UnitStatus": 0, 122 | "HeatingFunctionEnabled": true, 123 | "ServerTimerEnabled": false, 124 | "ThermostatStatusZone1": false, 125 | "ThermostatStatusZone2": false, 126 | "ThermostatTypeZone1": 1, 127 | "ThermostatTypeZone2": 1, 128 | "MixingTankWaterTemperature": 0.0, 129 | "CondensingTemperature": 0.0, 130 | "EffectiveFlags": 0, 131 | "LastEffectiveFlags": 0, 132 | "Power": true, 133 | "EcoHotWater": false, 134 | "OperationMode": 2, 135 | "OperationModeZone1": 0, 136 | "OperationModeZone2": 0, 137 | "SetTemperatureZone1": 19.5, 138 | "SetTemperatureZone2": 19.5, 139 | "SetTankWaterTemperature": 50.0, 140 | "TargetHCTemperatureZone1": 19.5, 141 | "TargetHCTemperatureZone2": 19.5, 142 | "ForcedHotWaterMode": false, 143 | "HolidayMode": false, 144 | "ProhibitHotWater": false, 145 | "ProhibitHeatingZone1": false, 146 | "ProhibitHeatingZone2": false, 147 | "ProhibitCoolingZone1": false, 148 | "ProhibitCoolingZone2": false, 149 | "ServerTimerDesired": false, 150 | "SecondaryZoneHeatCurve": false, 151 | "SetHeatFlowTemperatureZone1": 25.0, 152 | "SetHeatFlowTemperatureZone2": 25.0, 153 | "SetCoolFlowTemperatureZone1": 20.0, 154 | "SetCoolFlowTemperatureZone2": 20.0, 155 | "ThermostatTemperatureZone1": 0.0, 156 | "ThermostatTemperatureZone2": 0.0, 157 | "DECCReport": false, 158 | "CSVReport1min": false, 159 | "Zone2Master": false, 160 | "DailyEnergyConsumedDate": "2020-01-01T00:00:00", 161 | "DailyEnergyProducedDate": "2020-01-01T00:00:00", 162 | "CurrentEnergyConsumed": 1, 163 | "CurrentEnergyProduced": 5, 164 | "CurrentEnergyMode": null, 165 | "HeatingEnergyConsumedRate1": 1, 166 | "HeatingEnergyConsumedRate2": 0, 167 | "CoolingEnergyConsumedRate1": 0, 168 | "CoolingEnergyConsumedRate2": 0, 169 | "HotWaterEnergyConsumedRate1": 0, 170 | "HotWaterEnergyConsumedRate2": 0, 171 | "HeatingEnergyProducedRate1": 5, 172 | "HeatingEnergyProducedRate2": 0, 173 | "CoolingEnergyProducedRate1": 0, 174 | "CoolingEnergyProducedRate2": 0, 175 | "HotWaterEnergyProducedRate1": 0, 176 | "HotWaterEnergyProducedRate2": 0, 177 | "ErrorCode2Digit": 0, 178 | "SendSpecialFunctions": 0, 179 | "RequestSpecialFunctions": 0, 180 | "SpecialFunctionsState": 0, 181 | "PendingSendSpecialFunctions": 0, 182 | "PendingRequestSpecialFunctions": 0, 183 | "HasZone2": true, 184 | "HasSimplifiedZone2": true, 185 | "CanHeat": true, 186 | "CanCool": false, 187 | "HasHotWaterTank": true, 188 | "CanSetTankTemperature": true, 189 | "CanSetEcoHotWater": true, 190 | "HasEnergyConsumedMeter": true, 191 | "HasEnergyProducedMeter": true, 192 | "CanMeasureEnergyProduced": false, 193 | "CanMeasureEnergyConsumed": false, 194 | "Zone1InRoomMode": true, 195 | "Zone2InRoomMode": true, 196 | "Zone1InHeatMode": true, 197 | "Zone2InHeatMode": true, 198 | "Zone1InCoolMode": false, 199 | "Zone2InCoolMode": false, 200 | "AllowDualRoomTemperature": true, 201 | "IsGeodan": false, 202 | "HasEcoCuteSettings": false, 203 | "HasFTC45Settings": true, 204 | "HasFTC6Settings": false, 205 | "CanEstimateEnergyUsage": true, 206 | "CanUseRoomTemperatureCooling": false, 207 | "IsFtcModelSupported": true, 208 | "MaxTankTemperature": 60.0, 209 | "IdleZone1": false, 210 | "IdleZone2": false, 211 | "MinPcycle": 1, 212 | "MaxPcycle": 1, 213 | "MaxOutdoorUnits": 255, 214 | "MaxIndoorUnits": 255, 215 | "MaxTemperatureControlUnits": 0, 216 | "DeviceID": 1, 217 | "MacAddress": "de:ad:be:ef:22:22", 218 | "SerialNumber": "22222222", 219 | "TimeZoneID": 118, 220 | "DiagnosticMode": 0, 221 | "DiagnosticEndDate": null, 222 | "ExpectedCommand": 1, 223 | "Owner": null, 224 | "DetectedCountry": null, 225 | "AdaptorType": 1, 226 | "FirmwareDeployment": null, 227 | "FirmwareUpdateAborted": false, 228 | "LinkedDevice": null, 229 | "WifiSignalStrength": -37, 230 | "WifiAdapterStatus": "NORMAL", 231 | "Position": "unregistered", 232 | "PCycle": 1, 233 | "RecordNumMax": 1, 234 | "LastTimeStamp": "2020-01-01T00:00:00", 235 | "ErrorCode": 8000, 236 | "HasError": false, 237 | "LastReset": "2020-01-01T00:00:00.000", 238 | "FlashWrites": 2, 239 | "Scene": null, 240 | "TemperatureIncrementOverride": 0, 241 | "SSLExpirationDate": null, 242 | "SPTimeout": null, 243 | "Passcode": null, 244 | "ServerCommunicationDisabled": false, 245 | "ConsecutiveUploadErrors": 0, 246 | "DoNotRespondAfter": null, 247 | "OwnerRoleAccessLevel": 1, 248 | "OwnerCountry": 1, 249 | "HideEnergyReport": false, 250 | "ExceptionHash": null, 251 | "ExceptionDate": null, 252 | "ExceptionCount": null, 253 | "Rate1StartTime": null, 254 | "Rate2StartTime": null, 255 | "ProtocolVersion": 768, 256 | "UnitVersion": 256, 257 | "FirmwareAppVersion": 4000, 258 | "FirmwareWebVersion": 2000, 259 | "FirmwareWlanVersion": 3005009, 260 | "EffectivePCycle": 1, 261 | "HasErrorMessages": false, 262 | "Offline": false, 263 | "Units": [] 264 | }, 265 | "DiagnosticMode": 0, 266 | "DiagnosticEndDate": null, 267 | "Location": 1, 268 | "DetectedCountry": null, 269 | "Registrations": 1, 270 | "LocalIPAddress": null, 271 | "TimeZone": 1, 272 | "RegistReason": null, 273 | "ExpectedCommand": 1, 274 | "RegistRetry": 0, 275 | "DateCreated": "2020-01-01T00:00:00.000", 276 | "FirmwareDeployment": null, 277 | "FirmwareUpdateAborted": false, 278 | "Permissions": { 279 | "CanSetForcedHotWater": true, 280 | "CanSetOperationMode": true, 281 | "CanSetPower": true, 282 | "CanSetTankWaterTemperature": true, 283 | "CanSetEcoHotWater": false, 284 | "CanSetFlowTemperature": true, 285 | "CanSetTemperatureIncrementOverride": true 286 | } 287 | } -------------------------------------------------------------------------------- /tests/samples/erv_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "EffectiveFlags": 0, 3 | "LocalIPAddress": null, 4 | "RoomTemperature": 29.0, 5 | "SupplyTemperature": null, 6 | "OutdoorTemperature": 28.0, 7 | "RoomCO2Level": 0, 8 | "NightPurgeMode": false, 9 | "CoreMaintenanceRequired": false, 10 | "FilterMaintenanceRequired": false, 11 | "SetTemperature": null, 12 | "SetFanSpeed": 3, 13 | "OperationMode": 7, 14 | "VentilationMode": 0, 15 | "Name": null, 16 | "NumberOfFanSpeeds": 4, 17 | "WeatherObservations": [], 18 | "ErrorMessage": null, 19 | "ErrorCode": 8000, 20 | "DefaultHeatingSetTemperature": 21.0, 21 | "DefaultCoolingSetTemperature": 22.0, 22 | "TemperatureIncrementOverride": 0, 23 | "HideRoomTemperature": false, 24 | "HideSupplyTemperature": false, 25 | "HideOutdoorTemperature": false, 26 | "DeviceID": 111111, 27 | "DeviceType": 3, 28 | "LastCommunication": "2020-07-07T06:44:11.027", 29 | "NextCommunication": "2020-07-07T06:45:11.027", 30 | "Power": false, 31 | "HasPendingCommand": false, 32 | "Offline": false, 33 | "Scene": null, 34 | "SceneOwner": null 35 | } 36 | -------------------------------------------------------------------------------- /tests/samples/erv_listdevice.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceID": 0, 3 | "DeviceName": "", 4 | "BuildingID": 0, 5 | "BuildingName": null, 6 | "FloorID": null, 7 | "FloorName": null, 8 | "AreaID": null, 9 | "AreaName": null, 10 | "ImageID": -10003, 11 | "InstallationDate": null, 12 | "LastServiceDate": null, 13 | "Presets": [], 14 | "OwnerID": null, 15 | "OwnerName": null, 16 | "OwnerEmail": null, 17 | "AccessLevel": 4, 18 | "DirectAccess": false, 19 | "EndDate": "2500-01-01T00:00:00", 20 | "Zone1Name": null, 21 | "Zone2Name": null, 22 | "MinTemperature": 0, 23 | "MaxTemperature": 40, 24 | "HideVaneControls": false, 25 | "HideDryModeControl": false, 26 | "HideRoomTemperature": false, 27 | "HideSupplyTemperature": false, 28 | "HideOutdoorTemperature": false, 29 | "BuildingCountry": null, 30 | "OwnerCountry": null, 31 | "AdaptorType": -1, 32 | "LinkedDevice": null, 33 | "Type": 3, 34 | "MacAddress": "cc:cc:cc:cc:cc:cc", 35 | "SerialNumber": "1111111111", 36 | "Device": { 37 | "ListHistory24Formatters": [], 38 | "DeviceType": 3, 39 | "HasTemperatureControlUnit": false, 40 | "HasCoolOperationMode": true, 41 | "HasHeatOperationMode": true, 42 | "HasAutoOperationMode": false, 43 | "HasBypassVentilationMode": true, 44 | "HasAutoVentilationMode": true, 45 | "HasRoomTemperature": true, 46 | "HasSupplyTemperature": false, 47 | "HasOutdoorTemperature": true, 48 | "HasCO2Sensor": false, 49 | "NumberOfFanSpeeds": 4, 50 | "HasHalfDegreeIncrements": false, 51 | "TemperatureIncrementOverride": 0, 52 | "TemperatureIncrement": 1.0, 53 | "MinTempCoolDry": 0.0, 54 | "MaxTempCoolDry": 0.0, 55 | "MinTempHeat": 0.0, 56 | "MaxTempHeat": 0.0, 57 | "MinTempAutomatic": 0.0, 58 | "MaxTempAutomatic": 0.0, 59 | "SetSupplyTemperatureMode": false, 60 | "HasAutomaticFanSpeed": false, 61 | "CoreMaintenanceRequired": false, 62 | "FilterMaintenanceRequired": false, 63 | "Power": false, 64 | "RoomTemperature": 29.0, 65 | "SupplyTemperature": null, 66 | "OutdoorTemperature": 29.0, 67 | "RoomCO2Level": 0, 68 | "NightPurgeMode": false, 69 | "ThermostatOn": false, 70 | "SetTemperature": null, 71 | "ActualSupplyFanSpeed": 3, 72 | "ActualExhaustFanSpeed": 3, 73 | "SetFanSpeed": 3, 74 | "AutomaticFanSpeed": false, 75 | "OperationMode": 7, 76 | "ActualOperationMode": 0, 77 | "VentilationMode": 0, 78 | "ActualVentilationMode": 0, 79 | "EffectiveFlags": 0, 80 | "LastEffectiveFlags": 0, 81 | "DefaultCoolingSetTemperature": 22.0, 82 | "DefaultHeatingSetTemperature": 21.0, 83 | "HasEnergyConsumedMeter": true, 84 | "CurrentEnergyConsumed": 100, 85 | "CurrentEnergyAssignment": 0, 86 | "CoolingDisabled": false, 87 | "MaxOutdoorUnits": 0, 88 | "MaxIndoorUnits": 1, 89 | "MaxTemperatureControlUnits": 0, 90 | "DeviceID": 0, 91 | "MacAddress": "cc:cc:cc:cc:cc:cc", 92 | "SerialNumber": "1111111111111", 93 | "TimeZoneID": 0, 94 | "DiagnosticMode": 0, 95 | "DiagnosticEndDate": null, 96 | "ExpectedCommand": 1, 97 | "Owner": null, 98 | "DetectedCountry": null, 99 | "AdaptorType": -1, 100 | "FirmwareDeployment": null, 101 | "FirmwareUpdateAborted": false, 102 | "LinkedDevice": null, 103 | "WifiSignalStrength": -65, 104 | "WifiAdapterStatus": "NORMAL", 105 | "Position": "Unknown", 106 | "PCycle": 1, 107 | "RecordNumMax": 0, 108 | "LastTimeStamp": "2020-06-27T20:57:00", 109 | "ErrorCode": 8000, 110 | "HasError": false, 111 | "LastReset": "2020-06-18T13:40:32.67", 112 | "FlashWrites": 0, 113 | "Scene": null, 114 | "SSLExpirationDate": "2037-12-31T00:00:00", 115 | "SPTimeout": null, 116 | "Passcode": null, 117 | "ServerCommunicationDisabled": false, 118 | "ConsecutiveUploadErrors": 0, 119 | "DoNotRespondAfter": null, 120 | "OwnerRoleAccessLevel": 1, 121 | "OwnerCountry": 0, 122 | "HideEnergyReport": false, 123 | "ExceptionHash": null, 124 | "ExceptionDate": null, 125 | "ExceptionCount": null, 126 | "Rate1StartTime": null, 127 | "Rate2StartTime": null, 128 | "ProtocolVersion": 0, 129 | "UnitVersion": 0, 130 | "FirmwareAppVersion": 14000, 131 | "FirmwareWebVersion": 0, 132 | "FirmwareWlanVersion": 0, 133 | "EffectivePCycle": 1, 134 | "HasErrorMessages": false, 135 | "HasZone2": false, 136 | "Offline": false, 137 | "MinPcycle": 1, 138 | "MaxPcycle": 1, 139 | "Units": [] 140 | }, 141 | "DiagnosticMode": 0, 142 | "DiagnosticEndDate": null, 143 | "Location": 0, 144 | "DetectedCountry": null, 145 | "Registrations": 20, 146 | "LocalIPAddress": null, 147 | "TimeZone": 0, 148 | "RegistReason": "CONFIG", 149 | "ExpectedCommand": 1, 150 | "RegistRetry": 0, 151 | "DateCreated": "2020-06-18T13:40:32.703", 152 | "FirmwareDeployment": null, 153 | "FirmwareUpdateAborted": false, 154 | "Permissions": { 155 | "CanSetOperationMode": true, 156 | "CanSetFanSpeed": true, 157 | "CanSetVentilationMode": true, 158 | "CanSetPower": true, 159 | "CanSetTemperatureIncrementOverride": true 160 | } 161 | } -------------------------------------------------------------------------------- /tests/test_ata_properties.py: -------------------------------------------------------------------------------- 1 | """ATA tests.""" 2 | import json 3 | import os 4 | 5 | import pytest 6 | from asynctest import CoroutineMock, Mock, patch 7 | from aiohttp.web import HTTPForbidden 8 | from pymelcloud import DEVICE_TYPE_ATA 9 | from pymelcloud.const import ACCESS_LEVEL 10 | from pymelcloud.ata_device import ( 11 | OPERATION_MODE_HEAT, 12 | OPERATION_MODE_DRY, 13 | OPERATION_MODE_COOL, 14 | OPERATION_MODE_FAN_ONLY, 15 | OPERATION_MODE_HEAT_COOL, 16 | V_VANE_POSITION_AUTO, 17 | V_VANE_POSITION_1, 18 | V_VANE_POSITION_2, 19 | V_VANE_POSITION_3, 20 | V_VANE_POSITION_4, 21 | V_VANE_POSITION_5, 22 | V_VANE_POSITION_SWING, 23 | V_VANE_POSITION_UNDEFINED, 24 | H_VANE_POSITION_AUTO, 25 | H_VANE_POSITION_1, 26 | H_VANE_POSITION_2, 27 | H_VANE_POSITION_3, 28 | H_VANE_POSITION_4, 29 | H_VANE_POSITION_5, 30 | H_VANE_POSITION_SPLIT, 31 | H_VANE_POSITION_SWING, 32 | H_VANE_POSITION_UNDEFINED, 33 | AtaDevice, 34 | ) 35 | 36 | 37 | def _build_device(device_conf_name: str, device_state_name: str) -> AtaDevice: 38 | test_dir = os.path.join(os.path.dirname(__file__), "samples") 39 | with open(os.path.join(test_dir, device_conf_name), "r") as json_file: 40 | device_conf = json.load(json_file) 41 | 42 | with open(os.path.join(test_dir, device_state_name), "r") as json_file: 43 | device_state = json.load(json_file) 44 | 45 | with patch("pymelcloud.client.Client") as _client: 46 | _client.update_confs = CoroutineMock() 47 | _client.device_confs.__iter__ = Mock(return_value=[device_conf].__iter__()) 48 | _client.fetch_device_units = CoroutineMock(return_value=[]) 49 | _client.fetch_device_state = CoroutineMock(return_value=device_state) 50 | _client.fetch_energy_report = CoroutineMock(return_value=None) 51 | client = _client 52 | 53 | return AtaDevice(device_conf, client) 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_ata(): 58 | device = _build_device("ata_listdevice.json", "ata_get.json") 59 | 60 | assert device.name == "" 61 | assert device.device_type == DEVICE_TYPE_ATA 62 | assert device.access_level == ACCESS_LEVEL["OWNER"] 63 | assert device.temperature_increment == 0.5 64 | assert device.has_energy_consumed_meter is False 65 | assert device.room_temperature is None 66 | 67 | assert device.operation_modes == [ 68 | OPERATION_MODE_HEAT, 69 | OPERATION_MODE_DRY, 70 | OPERATION_MODE_COOL, 71 | OPERATION_MODE_FAN_ONLY, 72 | OPERATION_MODE_HEAT_COOL, 73 | ] 74 | assert device.fan_speed is None 75 | assert device.fan_speeds is None 76 | 77 | await device.update() 78 | 79 | assert device.room_temperature == 28.0 80 | assert device.target_temperature == 22.0 81 | 82 | assert device.operation_mode == OPERATION_MODE_COOL 83 | assert device.fan_speed == "3" 84 | assert device.actual_fan_speed == "0" 85 | assert device.fan_speeds == ["auto", "1", "2", "3", "4", "5"] 86 | 87 | assert device.vane_vertical == V_VANE_POSITION_AUTO 88 | assert device.vane_horizontal == H_VANE_POSITION_3 89 | 90 | assert device.wifi_signal == -51 91 | assert device.has_error is False 92 | assert device.error_code == 8000 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_ata_guest(): 97 | device = _build_device("ata_guest_listdevices.json", "ata_guest_get.json") 98 | device._client.fetch_device_units = CoroutineMock(side_effect=HTTPForbidden) 99 | assert device.device_type == DEVICE_TYPE_ATA 100 | assert device.access_level == ACCESS_LEVEL["GUEST"] 101 | await device.update() 102 | -------------------------------------------------------------------------------- /tests/test_atw_properties.py: -------------------------------------------------------------------------------- 1 | """Ecodan tests.""" 2 | import pytest 3 | from pymelcloud import DEVICE_TYPE_ATW 4 | from pymelcloud.atw_device import ( 5 | OPERATION_MODE_AUTO, 6 | OPERATION_MODE_FORCE_HOT_WATER, 7 | STATUS_HEAT_WATER, 8 | STATUS_HEAT_ZONES, 9 | STATUS_UNKNOWN, 10 | ZONE_OPERATION_MODE_COOL_FLOW, 11 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 12 | ZONE_OPERATION_MODE_CURVE, 13 | ZONE_OPERATION_MODE_HEAT_FLOW, 14 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 15 | ZONE_STATUS_HEAT, 16 | ZONE_STATUS_IDLE, 17 | ZONE_STATUS_UNKNOWN, 18 | AtwDevice, 19 | ) 20 | from .util import build_device 21 | 22 | 23 | def _build_device(device_conf_name: str, device_state_name: str) -> AtwDevice: 24 | device_conf, client = build_device(device_conf_name, device_state_name) 25 | return AtwDevice(device_conf, client) 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_1zone(): 30 | device = _build_device("atw_1zone_listdevice.json", "atw_1zone_get.json") 31 | 32 | assert device.name == "Heater and Water" 33 | assert device.device_type == DEVICE_TYPE_ATW 34 | assert device.temperature_increment == 0.5 35 | 36 | assert device.operation_mode is None 37 | assert device.operation_modes == [ 38 | OPERATION_MODE_AUTO, 39 | OPERATION_MODE_FORCE_HOT_WATER, 40 | ] 41 | assert device.tank_temperature is None 42 | assert device.status is STATUS_UNKNOWN 43 | assert device.target_tank_temperature is None 44 | assert device.target_tank_temperature_min == 40 45 | assert device.target_tank_temperature_max == 60 46 | assert device.flow_temperature_boiler == 25 47 | assert device.return_temperature_boiler == 25 48 | assert device.mixing_tank_temperature == 0 49 | assert device.holiday_mode is None 50 | assert device.wifi_signal == -73 51 | assert device.has_error is False 52 | assert device.error_code is None 53 | 54 | zones = device.zones 55 | 56 | assert len(zones) == 1 57 | assert zones[0].name == "Zone 1" 58 | assert zones[0].zone_index == 1 59 | assert zones[0].room_temperature is None 60 | assert zones[0].target_temperature is None 61 | assert zones[0].flow_temperature == 57.5 62 | assert zones[0].return_temperature == 53.0 63 | assert zones[0].target_flow_temperature is None 64 | assert zones[0].operation_mode is None 65 | assert zones[0].operation_modes == [ 66 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 67 | ZONE_OPERATION_MODE_HEAT_FLOW, 68 | ZONE_OPERATION_MODE_CURVE, 69 | ] 70 | assert zones[0].status == ZONE_STATUS_UNKNOWN 71 | 72 | await device.update() 73 | 74 | assert device.operation_mode == OPERATION_MODE_AUTO 75 | assert device.status == STATUS_HEAT_ZONES 76 | assert device.tank_temperature == 52.0 77 | assert device.target_tank_temperature == 50.0 78 | assert device.flow_temperature_boiler == 25 79 | assert device.return_temperature_boiler == 25 80 | assert device.mixing_tank_temperature == 0 81 | assert device.holiday_mode is False 82 | assert device.wifi_signal == -73 83 | assert device.has_error is False 84 | assert device.error_code == 8000 85 | 86 | assert zones[0].room_temperature == 27.0 87 | assert zones[0].target_temperature == 30 88 | assert zones[0].target_flow_temperature == 60.0 89 | assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT_FLOW 90 | assert zones[0].operation_modes == [ 91 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 92 | ZONE_OPERATION_MODE_HEAT_FLOW, 93 | ZONE_OPERATION_MODE_CURVE, 94 | ] 95 | assert zones[0].status == ZONE_STATUS_HEAT 96 | 97 | 98 | @pytest.mark.asyncio 99 | async def test_2zone(): 100 | device = _build_device("atw_2zone_listdevice.json", "atw_2zone_get.json") 101 | 102 | assert device.name == "Home" 103 | assert device.device_type == DEVICE_TYPE_ATW 104 | assert device.temperature_increment == 0.5 105 | 106 | assert device.operation_mode is None 107 | assert device.operation_modes == [ 108 | OPERATION_MODE_AUTO, 109 | OPERATION_MODE_FORCE_HOT_WATER, 110 | ] 111 | assert device.tank_temperature is None 112 | assert device.status is STATUS_UNKNOWN 113 | assert device.target_tank_temperature is None 114 | assert device.target_tank_temperature_min == 40 115 | assert device.target_tank_temperature_max == 60 116 | assert device.flow_temperature_boiler == 25 117 | assert device.return_temperature_boiler == 25 118 | assert device.mixing_tank_temperature == 0 119 | assert device.holiday_mode is None 120 | assert device.wifi_signal == -37 121 | assert device.has_error is False 122 | assert device.error_code is None 123 | 124 | zones = device.zones 125 | 126 | assert len(zones) == 2 127 | assert zones[0].name == "Downstairs" 128 | assert zones[0].zone_index == 1 129 | assert zones[0].room_temperature is None 130 | assert zones[0].target_temperature is None 131 | assert zones[0].flow_temperature == 36.0 132 | assert zones[0].return_temperature == 30.0 133 | assert zones[0].target_flow_temperature is None 134 | assert zones[0].operation_mode is None 135 | assert zones[0].operation_modes == [ 136 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 137 | ZONE_OPERATION_MODE_HEAT_FLOW, 138 | ZONE_OPERATION_MODE_CURVE, 139 | ] 140 | assert zones[0].status == ZONE_STATUS_UNKNOWN 141 | 142 | assert zones[1].name == "Upstairs" 143 | assert zones[1].zone_index == 2 144 | assert zones[1].room_temperature is None 145 | assert zones[1].target_temperature is None 146 | assert zones[1].flow_temperature == 36.0 147 | assert zones[1].return_temperature == 30.0 148 | assert zones[1].target_flow_temperature is None 149 | assert zones[1].operation_mode is None 150 | assert zones[1].operation_modes == [ 151 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 152 | ZONE_OPERATION_MODE_HEAT_FLOW, 153 | ZONE_OPERATION_MODE_CURVE, 154 | ] 155 | assert zones[1].status == ZONE_STATUS_UNKNOWN 156 | 157 | await device.update() 158 | 159 | assert device.operation_mode == OPERATION_MODE_AUTO 160 | assert device.status == STATUS_HEAT_ZONES 161 | assert device.tank_temperature == 49.5 162 | assert device.target_tank_temperature == 50.0 163 | assert device.flow_temperature_boiler == 25 164 | assert device.return_temperature_boiler == 25 165 | assert device.mixing_tank_temperature == 0 166 | assert device.holiday_mode is False 167 | assert device.wifi_signal == -37 168 | assert device.has_error is False 169 | assert device.error_code == 8000 170 | 171 | assert zones[0].room_temperature == 20.5 172 | assert zones[0].target_temperature == 19.5 173 | assert zones[0].target_flow_temperature == 25.0 174 | assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT_THERMOSTAT 175 | assert zones[0].operation_modes == [ 176 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 177 | ZONE_OPERATION_MODE_HEAT_FLOW, 178 | ZONE_OPERATION_MODE_CURVE, 179 | ] 180 | assert zones[0].status == ZONE_STATUS_HEAT 181 | 182 | assert zones[1].room_temperature == 19.5 183 | assert zones[1].target_temperature == 18 184 | assert zones[1].target_flow_temperature == 25.0 185 | assert zones[1].operation_mode == ZONE_OPERATION_MODE_HEAT_THERMOSTAT 186 | assert zones[1].operation_modes == [ 187 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 188 | ZONE_OPERATION_MODE_HEAT_FLOW, 189 | ZONE_OPERATION_MODE_CURVE, 190 | ] 191 | assert zones[1].status == ZONE_STATUS_HEAT 192 | 193 | 194 | @pytest.mark.asyncio 195 | async def test_2zone_cancool(): 196 | device = _build_device( 197 | "atw_2zone_cancool_listdevice.json", "atw_2zone_cancool_get.json" 198 | ) 199 | 200 | assert device.name == "Mitsubishi" 201 | assert device.device_type == DEVICE_TYPE_ATW 202 | assert device.temperature_increment == 0.5 203 | 204 | assert device.operation_mode is None 205 | assert device.operation_modes == [ 206 | OPERATION_MODE_AUTO, 207 | OPERATION_MODE_FORCE_HOT_WATER, 208 | ] 209 | assert device.tank_temperature is None 210 | assert device.status is STATUS_UNKNOWN 211 | assert device.target_tank_temperature is None 212 | assert device.target_tank_temperature_min == 40 213 | assert device.target_tank_temperature_max == 60 214 | assert device.flow_temperature_boiler == 25 215 | assert device.return_temperature_boiler == 25 216 | assert device.mixing_tank_temperature is None 217 | assert device.holiday_mode is None 218 | assert device.wifi_signal == -82 219 | assert device.has_error is False 220 | assert device.error_code is None 221 | 222 | zones = device.zones 223 | 224 | assert len(zones) == 2 225 | assert zones[0].name == "Zone 1" 226 | assert zones[0].zone_index == 1 227 | assert zones[0].room_temperature is None 228 | assert zones[0].target_temperature is None 229 | assert zones[0].flow_temperature == 50.5 230 | assert zones[0].return_temperature == 50.5 231 | assert zones[0].target_flow_temperature is None 232 | assert zones[0].operation_mode is None 233 | assert zones[0].operation_modes == [ 234 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 235 | ZONE_OPERATION_MODE_HEAT_FLOW, 236 | ZONE_OPERATION_MODE_CURVE, 237 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 238 | ZONE_OPERATION_MODE_COOL_FLOW, 239 | ] 240 | assert zones[0].status == ZONE_STATUS_UNKNOWN 241 | 242 | assert zones[1].name == "Zone 2" 243 | assert zones[1].zone_index == 2 244 | assert zones[1].room_temperature is None 245 | assert zones[1].target_temperature is None 246 | assert zones[1].flow_temperature == 50.5 247 | assert zones[1].return_temperature == 50.5 248 | assert zones[1].target_flow_temperature is None 249 | assert zones[1].operation_mode is None 250 | assert zones[1].operation_modes == [ 251 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 252 | ZONE_OPERATION_MODE_HEAT_FLOW, 253 | ZONE_OPERATION_MODE_CURVE, 254 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 255 | ZONE_OPERATION_MODE_COOL_FLOW, 256 | ] 257 | assert zones[1].status == ZONE_STATUS_UNKNOWN 258 | 259 | await device.update() 260 | 261 | assert device.operation_mode == OPERATION_MODE_AUTO 262 | assert device.status == STATUS_HEAT_WATER 263 | assert device.tank_temperature == 47.5 264 | assert device.target_tank_temperature == 52.0 265 | assert device.flow_temperature_boiler == 25 266 | assert device.return_temperature_boiler == 25 267 | assert device.mixing_tank_temperature is None 268 | assert device.holiday_mode is False 269 | assert device.wifi_signal == -82 270 | assert device.has_error is False 271 | assert device.error_code == 8000 272 | 273 | assert zones[0].room_temperature == 21.5 274 | assert zones[0].target_temperature == 20.5 275 | assert zones[0].target_flow_temperature == 5.0 276 | assert zones[0].operation_mode == ZONE_OPERATION_MODE_CURVE 277 | assert zones[0].operation_modes == [ 278 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 279 | ZONE_OPERATION_MODE_HEAT_FLOW, 280 | ZONE_OPERATION_MODE_CURVE, 281 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 282 | ZONE_OPERATION_MODE_COOL_FLOW, 283 | ] 284 | assert zones[0].status == ZONE_STATUS_IDLE 285 | 286 | assert zones[1].room_temperature == 21.0 287 | assert zones[1].target_temperature == 21.0 288 | assert zones[1].target_flow_temperature == 5.0 289 | assert zones[1].operation_mode == ZONE_OPERATION_MODE_CURVE 290 | assert zones[1].operation_modes == [ 291 | ZONE_OPERATION_MODE_HEAT_THERMOSTAT, 292 | ZONE_OPERATION_MODE_HEAT_FLOW, 293 | ZONE_OPERATION_MODE_CURVE, 294 | ZONE_OPERATION_MODE_COOL_THERMOSTAT, 295 | ZONE_OPERATION_MODE_COOL_FLOW, 296 | ] 297 | assert zones[1].status == ZONE_STATUS_IDLE 298 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | """Device tests.""" 2 | from typing import Any, Dict, Optional 3 | 4 | import pytest 5 | from pymelcloud.ata_device import AtaDevice 6 | from .util import build_device 7 | 8 | 9 | def _build_device(device_conf_name: str, device_state_name: str, energy_report: Optional[Dict[Any, Any]]=None) -> AtaDevice: 10 | device_conf, client = build_device(device_conf_name, device_state_name, energy_report) 11 | return AtaDevice(device_conf, client) 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_round_temperature(): 16 | device = _build_device("ata_listdevice.json", "ata_get.json") 17 | device._device_conf.get("Device")["TemperatureIncrement"] = 0.5 18 | 19 | assert device.round_temperature(23.99999) == 24.0 20 | assert device.round_temperature(24.0) == 24.0 21 | assert device.round_temperature(24.00001) == 24.0 22 | assert device.round_temperature(24.24999) == 24.0 23 | assert device.round_temperature(24.25) == 24.5 24 | assert device.round_temperature(24.25001) == 24.5 25 | assert device.round_temperature(24.5) == 24.5 26 | assert device.round_temperature(24.74999) == 24.5 27 | assert device.round_temperature(24.75) == 25.0 28 | assert device.round_temperature(24.75001) == 25.0 29 | 30 | device._device_conf.get("Device")["TemperatureIncrement"] = 1 31 | 32 | assert device.round_temperature(23.99999) == 24.0 33 | assert device.round_temperature(24.0) == 24.0 34 | assert device.round_temperature(24.00001) == 24.0 35 | assert device.round_temperature(24.49999) == 24.0 36 | assert device.round_temperature(24.5) == 25.0 37 | assert device.round_temperature(24.50001) == 25.0 38 | assert device.round_temperature(25.0) == 25.0 39 | assert device.round_temperature(25.00001) == 25.0 40 | assert device.round_temperature(25.49999) == 25.0 41 | assert device.round_temperature(25.5) == 26.0 42 | 43 | @pytest.mark.asyncio 44 | async def test_energy_report_none_if_no_report(): 45 | device = _build_device("ata_listdevice.json", "ata_get.json") 46 | 47 | await device.update() 48 | 49 | assert device.daily_energy_consumed is None 50 | 51 | def test_energy_report_before_update(): 52 | device = _build_device("ata_listdevice.json", "ata_get.json") 53 | 54 | assert device.daily_energy_consumed is None 55 | 56 | @pytest.mark.asyncio 57 | async def test_round_temperature(): 58 | device = _build_device( 59 | "ata_listdevice.json", 60 | "ata_get.json", 61 | { 62 | "Heating": [0.0, 0.0, 1.0], 63 | "Cooling": [0.0, 0.1, 10.0], 64 | "Dry": [0.2, 0.0, 100.0], 65 | "Fan": [0.3, 1000.0], 66 | }, 67 | ) 68 | 69 | await device.update() 70 | 71 | assert device.daily_energy_consumed == 1111.0 72 | -------------------------------------------------------------------------------- /tests/test_erv_properties.py: -------------------------------------------------------------------------------- 1 | """ERV tests.""" 2 | import json 3 | import os 4 | 5 | import pytest 6 | from asynctest import CoroutineMock, Mock, patch 7 | from pymelcloud import DEVICE_TYPE_ERV 8 | from pymelcloud.erv_device import ( 9 | VENTILATION_MODE_AUTO, 10 | VENTILATION_MODE_BYPASS, 11 | VENTILATION_MODE_RECOVERY, 12 | ErvDevice, 13 | ) 14 | 15 | 16 | def _build_device(device_conf_name: str, device_state_name: str) -> ErvDevice: 17 | test_dir = os.path.join(os.path.dirname(__file__), "samples") 18 | with open(os.path.join(test_dir, device_conf_name), "r") as json_file: 19 | device_conf = json.load(json_file) 20 | 21 | with open(os.path.join(test_dir, device_state_name), "r") as json_file: 22 | device_state = json.load(json_file) 23 | 24 | with patch("pymelcloud.client.Client") as _client: 25 | _client.update_confs = CoroutineMock() 26 | _client.device_confs.__iter__ = Mock(return_value=[device_conf].__iter__()) 27 | _client.fetch_device_units = CoroutineMock(return_value=[]) 28 | _client.fetch_device_state = CoroutineMock(return_value=device_state) 29 | _client.fetch_energy_report = CoroutineMock(return_value=None) 30 | client = _client 31 | 32 | return ErvDevice(device_conf, client) 33 | 34 | @pytest.mark.asyncio 35 | async def test_erv(): 36 | device = _build_device("erv_listdevice.json", "erv_get.json") 37 | 38 | assert device.name == "" 39 | assert device.device_type == DEVICE_TYPE_ERV 40 | assert device.temperature_increment == 1.0 41 | assert device.has_energy_consumed_meter is True 42 | assert device.total_energy_consumed == 0.1 43 | assert device.room_temperature is None 44 | assert device.outside_temperature is None 45 | assert device.room_co2_level is None 46 | 47 | assert device.ventilation_mode is None 48 | assert device.ventilation_modes == [ 49 | VENTILATION_MODE_RECOVERY, 50 | VENTILATION_MODE_BYPASS, 51 | VENTILATION_MODE_AUTO, 52 | ] 53 | assert device.actual_ventilation_mode is None 54 | assert device.fan_speed is None 55 | assert device.fan_speeds is None 56 | assert device.actual_supply_fan_speed is None 57 | assert device.actual_exhaust_fan_speed is None 58 | assert device.core_maintenance_required is False 59 | assert device.filter_maintenance_required is False 60 | assert device.night_purge_mode is False 61 | 62 | await device.update() 63 | 64 | assert device.room_temperature == 29.0 65 | assert device.outside_temperature == 28.0 66 | assert device.room_co2_level is None 67 | 68 | assert device.ventilation_mode == VENTILATION_MODE_RECOVERY 69 | assert device.actual_ventilation_mode == VENTILATION_MODE_RECOVERY 70 | assert device.fan_speed == "3" 71 | assert device.fan_speeds == ["1", "2", "3", "4"] 72 | assert device.actual_supply_fan_speed == "3" 73 | assert device.actual_exhaust_fan_speed == "3" 74 | assert device.core_maintenance_required is False 75 | assert device.filter_maintenance_required is False 76 | assert device.night_purge_mode is False 77 | 78 | assert device.wifi_signal == -65 79 | assert device.has_error is False 80 | assert device.error_code == 8000 81 | assert str(device.last_seen) == '2020-07-07 06:44:11.027000+00:00' 82 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Any, Dict, Optional 4 | 5 | from asynctest import CoroutineMock, Mock, patch 6 | 7 | def build_device(device_conf_name: str, device_state_name: str, energy_report: Optional[Dict[Any, Any]]=None): 8 | test_dir = os.path.join(os.path.dirname(__file__), "samples") 9 | with open(os.path.join(test_dir, device_conf_name), "r") as json_file: 10 | device_conf = json.load(json_file) 11 | 12 | with open(os.path.join(test_dir, device_state_name), "r") as json_file: 13 | device_state = json.load(json_file) 14 | 15 | with patch("pymelcloud.client.Client") as _client: 16 | _client.update_confs = CoroutineMock() 17 | _client.device_confs.__iter__ = Mock(return_value=[device_conf].__iter__()) 18 | _client.fetch_device_units = CoroutineMock(return_value=[]) 19 | _client.fetch_device_state = CoroutineMock(return_value=device_state) 20 | _client.fetch_energy_report = CoroutineMock(return_value=energy_report) 21 | client = _client 22 | 23 | return device_conf, client 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py36,py37,py38,py39,flake8,typing 3 | 4 | [testenv] 5 | deps= 6 | -r requirements.txt 7 | pytest 8 | pytest-asyncio 9 | pytest-cov 10 | commands= 11 | py.test --cov --cov-config=tox.ini 12 | 13 | [testenv:flake8] 14 | deps=flake8 15 | commands=flake8 pymelcloud 16 | 17 | [flake8] 18 | exclude = .git,.tox,__pycache__,tests/* 19 | max-line-length = 88 20 | 21 | [testenv:lint] 22 | deps = pre-commit 23 | skip_install = true 24 | commands = pre-commit run --all-files 25 | 26 | [testenv:typing] 27 | deps=mypy 28 | commands=mypy --ignore-missing-imports pymelcloud 29 | 30 | [isort] 31 | multi_line_output=3 32 | include_trailing_comma=True 33 | force_grid_wrap=0 34 | use_parentheses=True 35 | line_length=88 36 | known_first_party=pymelcloud 37 | known_third_party= 38 | aiohttp 39 | 40 | [coverage:run] 41 | source = pymelcloud 42 | branch = True 43 | omit = 44 | 45 | [coverage:report] 46 | --------------------------------------------------------------------------------