├── .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 | [](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 |
--------------------------------------------------------------------------------