Skip to content

Commit b7384a7

Browse files
author
Vilppu Vuorinen
committed
Add Atw zone op mode control and fine grained modes
1 parent 2dcfd5f commit b7384a7

3 files changed

Lines changed: 139 additions & 70 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Add Atw zone operation mode control.
10+
11+
### Removed
12+
- Remove heat/cool simplification from Atw zone operation modes.
813

914
## [2.7.0] - 2020-05-01
1015
### Added

pymelcloud/atw_device.py

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,26 @@
4343
_ZONE_INT_MODE_COOL_THERMOSTAT = 3
4444
_ZONE_INT_MODE_COOL_FLOW = 4
4545

46-
ZONE_OPERATION_MODE_HEAT = "heat"
47-
ZONE_OPERATION_MODE_COOL = "cool"
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"
4851
ZONE_OPERATION_MODE_UNKNOWN = "unknown"
4952
_ZONE_OPERATION_MODE_LOOKUP = {
50-
_ZONE_INT_MODE_HEAT_THERMOSTAT: ZONE_OPERATION_MODE_HEAT,
51-
_ZONE_INT_MODE_HEAT_FLOW: ZONE_OPERATION_MODE_HEAT,
52-
_ZONE_INT_MODE_CURVE: ZONE_OPERATION_MODE_HEAT,
53-
_ZONE_INT_MODE_COOL_THERMOSTAT: ZONE_OPERATION_MODE_COOL,
54-
_ZONE_INT_MODE_COOL_FLOW: ZONE_OPERATION_MODE_COOL,
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()
5561
}
5662

57-
ZONE_STATUS_HEAT = ZONE_OPERATION_MODE_HEAT
63+
ZONE_STATUS_HEAT = "heat"
5864
ZONE_STATUS_IDLE = "idle"
59-
ZONE_STATUS_COOL = ZONE_OPERATION_MODE_COOL
65+
ZONE_STATUS_COOL = "cool"
6066
ZONE_STATUS_UNKNOWN = "unknown"
6167

6268

@@ -110,9 +116,16 @@ def status(self) -> str:
110116
return ZONE_STATUS_IDLE
111117

112118
op_mode = self.operation_mode
113-
if op_mode == ZONE_OPERATION_MODE_HEAT:
119+
if op_mode in [
120+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
121+
ZONE_OPERATION_MODE_HEAT_FLOW,
122+
ZONE_OPERATION_MODE_CURVE,
123+
]:
114124
return ZONE_STATUS_HEAT
115-
if op_mode == ZONE_OPERATION_MODE_COOL:
125+
if op_mode in [
126+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
127+
ZONE_OPERATION_MODE_COOL_FLOW,
128+
]:
116129
return ZONE_STATUS_COOL
117130

118131
return ZONE_STATUS_UNKNOWN
@@ -166,9 +179,13 @@ def target_flow_temperature(self) -> Optional[float]:
166179
if op_mode is None:
167180
return None
168181

169-
if op_mode == ZONE_OPERATION_MODE_HEAT:
170-
return self.target_heat_flow_temperature
171-
return self.target_cool_flow_temperature
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
172189

173190
@property
174191
def target_heat_flow_temperature(self) -> Optional[float]:
@@ -194,10 +211,13 @@ async def set_target_flow_temperature(self, target_flow_temperature):
194211
if op_mode is None:
195212
return None
196213

197-
if op_mode == ZONE_OPERATION_MODE_HEAT:
198-
await self.set_target_heat_flow_temperature(target_flow_temperature)
199-
else:
214+
if op_mode in [
215+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
216+
ZONE_OPERATION_MODE_COOL_FLOW,
217+
]:
200218
await self.set_target_cool_flow_temperature(target_flow_temperature)
219+
else:
220+
await self.set_target_heat_flow_temperature(target_flow_temperature)
201221

202222
async def set_target_heat_flow_temperature(self, target_flow_temperature):
203223
"""Set target heat flow temperature of this zone."""
@@ -217,13 +237,7 @@ async def set_target_cool_flow_temperature(self, target_flow_temperature):
217237

218238
@property
219239
def operation_mode(self) -> Optional[str]:
220-
"""Return current operation mode.
221-
222-
This value is not backed by "OperationMode" property of the zone. MELCloud
223-
uses "OperationMode" for the temperature control mode ("Room", "Flow",
224-
"Curve"). Instead this property indicates whether the device is set to heat
225-
or cool.
226-
"""
240+
"""Return current operation mode."""
227241
state = self._device_state()
228242
if state is None:
229243
return None
@@ -235,16 +249,20 @@ def operation_mode(self) -> Optional[str]:
235249

236250
@property
237251
def operation_modes(self) -> List[str]:
238-
"""Return list of available operation modes.
239-
240-
This value is not backed by "OperationMode" property of the zone. MELCloud
241-
uses "OperationMode" for the temperature control mode ("Room", "Flow",
242-
"Curve"). Instead this property indicates whether the device is set to heat
243-
or cool.
244-
"""
245-
modes = [ZONE_OPERATION_MODE_HEAT]
246-
if self._device_conf().get("Device", {}).get("CanCool", False):
247-
modes.append(ZONE_OPERATION_MODE_COOL)
252+
"""Return list of available operation modes."""
253+
modes = []
254+
device = self._device_conf().get("Device", {})
255+
if device.get("CanHeat", False):
256+
modes += [
257+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
258+
ZONE_OPERATION_MODE_HEAT_FLOW,
259+
ZONE_OPERATION_MODE_CURVE,
260+
]
261+
if device.get("CanCool", False):
262+
modes += [
263+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
264+
ZONE_OPERATION_MODE_COOL_FLOW,
265+
]
248266
return modes
249267

250268
async def set_operation_mode(self, mode: str):
@@ -253,7 +271,16 @@ async def set_operation_mode(self, mode: str):
253271
if state is None:
254272
return
255273

256-
raise ValueError("Not implemented")
274+
int_mode = _REVERSE_ZONE_OPERATION_MODE_LOOKUP.get(mode)
275+
if int_mode is None:
276+
raise ValueError(f"Invalid mode '{mode}'")
277+
278+
if self.zone_index == 1:
279+
prop = PROPERTY_ZONE_1_OPERATION_MODE
280+
else:
281+
prop = PROPERTY_ZONE_2_OPERATION_MODE
282+
283+
await self._device.set({prop: int_mode})
257284

258285

259286
class AtwDevice(Device):
@@ -265,36 +292,34 @@ def apply_write(self, state: Dict[str, Any], key: str, value: Any):
265292

266293
if key == PROPERTY_TARGET_TANK_TEMPERATURE:
267294
state["SetTankWaterTemperature"] = value
268-
flags = flags | 0x1000000000020
295+
flags |= 0x1000000000020
269296
elif key == PROPERTY_OPERATION_MODE:
270297
state["ForcedHotWaterMode"] = value == OPERATION_MODE_FORCE_HOT_WATER
271-
flags = flags | 0x10000
298+
flags |= 0x10000
272299
elif key == PROPERTY_ZONE_1_TARGET_TEMPERATURE:
273300
state["SetTemperatureZone1"] = value
274-
flags = flags | 0x200000080
301+
flags |= 0x200000080
275302
elif key == PROPERTY_ZONE_2_TARGET_TEMPERATURE:
276303
state["SetTemperatureZone2"] = value
277-
flags = flags | 0x800000200
304+
flags |= 0x800000200
278305
elif key == PROPERTY_ZONE_1_TARGET_HEAT_FLOW_TEMPERATURE:
279306
state["SetHeatFlowTemperatureZone1"] = value
280-
flags = flags | 0x1000000000000
307+
flags |= 0x1000000000000
281308
elif key == PROPERTY_ZONE_1_TARGET_COOL_FLOW_TEMPERATURE:
282309
state["SetCoolFlowTemperatureZone1"] = value
283-
flags = flags | 0x1000000000000
310+
flags |= 0x1000000000000
284311
elif key == PROPERTY_ZONE_2_TARGET_HEAT_FLOW_TEMPERATURE:
285312
state["SetHeatFlowTemperatureZone2"] = value
286-
flags = flags | 0x1000000000000
313+
flags |= 0x1000000000000
287314
elif key == PROPERTY_ZONE_2_TARGET_COOL_FLOW_TEMPERATURE:
288315
state["SetCoolFlowTemperatureZone2"] = value
289-
flags = flags | 0x1000000000000
316+
flags |= 0x1000000000000
290317
elif key == PROPERTY_ZONE_1_OPERATION_MODE:
291-
# Captures required to implement
292-
# 0x08
293-
pass
318+
state["OperationModeZone1"] = value
319+
flags |= 0x08
294320
elif key == PROPERTY_ZONE_2_OPERATION_MODE:
295-
# Captures required to implement
296-
# 0x10
297-
pass
321+
state["OperationModeZone2"] = value
322+
flags |= 0x10
298323
else:
299324
raise ValueError(f"Cannot set {key}, invalid property")
300325

tests/test_atw_properties.py

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
STATUS_HEAT_WATER,
1212
STATUS_HEAT_ZONES,
1313
STATUS_UNKNOWN,
14-
ZONE_OPERATION_MODE_COOL,
15-
ZONE_OPERATION_MODE_HEAT,
14+
ZONE_OPERATION_MODE_COOL_FLOW,
15+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
16+
ZONE_OPERATION_MODE_CURVE,
17+
ZONE_OPERATION_MODE_HEAT_FLOW,
18+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
1619
ZONE_STATUS_HEAT,
1720
ZONE_STATUS_IDLE,
1821
ZONE_STATUS_UNKNOWN,
@@ -69,7 +72,11 @@ async def test_1zone():
6972
assert zones[0].return_temperature == 25.0
7073
assert zones[0].target_flow_temperature is None
7174
assert zones[0].operation_mode is None
72-
assert zones[0].operation_modes == [ZONE_OPERATION_MODE_HEAT]
75+
assert zones[0].operation_modes == [
76+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
77+
ZONE_OPERATION_MODE_HEAT_FLOW,
78+
ZONE_OPERATION_MODE_CURVE,
79+
]
7380
assert zones[0].status == ZONE_STATUS_UNKNOWN
7481

7582
await device.update()
@@ -83,8 +90,12 @@ async def test_1zone():
8390
assert zones[0].room_temperature == 27.0
8491
assert zones[0].target_temperature == 30
8592
assert zones[0].target_flow_temperature == 60.0
86-
assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT
87-
assert zones[0].operation_modes == [ZONE_OPERATION_MODE_HEAT]
93+
assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT_FLOW
94+
assert zones[0].operation_modes == [
95+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
96+
ZONE_OPERATION_MODE_HEAT_FLOW,
97+
ZONE_OPERATION_MODE_CURVE,
98+
]
8899
assert zones[0].status == ZONE_STATUS_HEAT
89100

90101

@@ -119,7 +130,11 @@ async def test_2zone():
119130
assert zones[0].return_temperature == 25.0
120131
assert zones[0].target_flow_temperature is None
121132
assert zones[0].operation_mode is None
122-
assert zones[0].operation_modes == [ZONE_OPERATION_MODE_HEAT]
133+
assert zones[0].operation_modes == [
134+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
135+
ZONE_OPERATION_MODE_HEAT_FLOW,
136+
ZONE_OPERATION_MODE_CURVE,
137+
]
123138
assert zones[0].status == ZONE_STATUS_UNKNOWN
124139

125140
assert zones[1].name == "Upstairs"
@@ -130,7 +145,11 @@ async def test_2zone():
130145
assert zones[1].return_temperature == 25.0
131146
assert zones[1].target_flow_temperature is None
132147
assert zones[1].operation_mode is None
133-
assert zones[1].operation_modes == [ZONE_OPERATION_MODE_HEAT]
148+
assert zones[1].operation_modes == [
149+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
150+
ZONE_OPERATION_MODE_HEAT_FLOW,
151+
ZONE_OPERATION_MODE_CURVE,
152+
]
134153
assert zones[1].status == ZONE_STATUS_UNKNOWN
135154

136155
await device.update()
@@ -144,15 +163,23 @@ async def test_2zone():
144163
assert zones[0].room_temperature == 20.5
145164
assert zones[0].target_temperature == 19.5
146165
assert zones[0].target_flow_temperature == 25.0
147-
assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT
148-
assert zones[0].operation_modes == [ZONE_OPERATION_MODE_HEAT]
166+
assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT_THERMOSTAT
167+
assert zones[0].operation_modes == [
168+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
169+
ZONE_OPERATION_MODE_HEAT_FLOW,
170+
ZONE_OPERATION_MODE_CURVE,
171+
]
149172
assert zones[0].status == ZONE_STATUS_HEAT
150173

151174
assert zones[1].room_temperature == 19.5
152175
assert zones[1].target_temperature == 18
153176
assert zones[1].target_flow_temperature == 25.0
154-
assert zones[1].operation_mode == ZONE_OPERATION_MODE_HEAT
155-
assert zones[1].operation_modes == [ZONE_OPERATION_MODE_HEAT]
177+
assert zones[1].operation_mode == ZONE_OPERATION_MODE_HEAT_THERMOSTAT
178+
assert zones[1].operation_modes == [
179+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
180+
ZONE_OPERATION_MODE_HEAT_FLOW,
181+
ZONE_OPERATION_MODE_CURVE,
182+
]
156183
assert zones[1].status == ZONE_STATUS_HEAT
157184

158185

@@ -190,8 +217,11 @@ async def test_2zone_cancool():
190217
assert zones[0].target_flow_temperature is None
191218
assert zones[0].operation_mode is None
192219
assert zones[0].operation_modes == [
193-
ZONE_OPERATION_MODE_HEAT,
194-
ZONE_OPERATION_MODE_COOL,
220+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
221+
ZONE_OPERATION_MODE_HEAT_FLOW,
222+
ZONE_OPERATION_MODE_CURVE,
223+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
224+
ZONE_OPERATION_MODE_COOL_FLOW,
195225
]
196226
assert zones[0].status == ZONE_STATUS_UNKNOWN
197227

@@ -204,8 +234,11 @@ async def test_2zone_cancool():
204234
assert zones[1].target_flow_temperature is None
205235
assert zones[1].operation_mode is None
206236
assert zones[1].operation_modes == [
207-
ZONE_OPERATION_MODE_HEAT,
208-
ZONE_OPERATION_MODE_COOL,
237+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
238+
ZONE_OPERATION_MODE_HEAT_FLOW,
239+
ZONE_OPERATION_MODE_CURVE,
240+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
241+
ZONE_OPERATION_MODE_COOL_FLOW,
209242
]
210243
assert zones[1].status == ZONE_STATUS_UNKNOWN
211244

@@ -220,19 +253,25 @@ async def test_2zone_cancool():
220253
assert zones[0].room_temperature == 21.5
221254
assert zones[0].target_temperature == 20.5
222255
assert zones[0].target_flow_temperature == 5.0
223-
assert zones[0].operation_mode == ZONE_OPERATION_MODE_HEAT
256+
assert zones[0].operation_mode == ZONE_OPERATION_MODE_CURVE
224257
assert zones[0].operation_modes == [
225-
ZONE_OPERATION_MODE_HEAT,
226-
ZONE_OPERATION_MODE_COOL,
258+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
259+
ZONE_OPERATION_MODE_HEAT_FLOW,
260+
ZONE_OPERATION_MODE_CURVE,
261+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
262+
ZONE_OPERATION_MODE_COOL_FLOW,
227263
]
228264
assert zones[0].status == ZONE_STATUS_IDLE
229265

230266
assert zones[1].room_temperature == 21.0
231267
assert zones[1].target_temperature == 21.0
232268
assert zones[1].target_flow_temperature == 5.0
233-
assert zones[1].operation_mode == ZONE_OPERATION_MODE_HEAT
269+
assert zones[1].operation_mode == ZONE_OPERATION_MODE_CURVE
234270
assert zones[1].operation_modes == [
235-
ZONE_OPERATION_MODE_HEAT,
236-
ZONE_OPERATION_MODE_COOL,
271+
ZONE_OPERATION_MODE_HEAT_THERMOSTAT,
272+
ZONE_OPERATION_MODE_HEAT_FLOW,
273+
ZONE_OPERATION_MODE_CURVE,
274+
ZONE_OPERATION_MODE_COOL_THERMOSTAT,
275+
ZONE_OPERATION_MODE_COOL_FLOW,
237276
]
238277
assert zones[1].status == ZONE_STATUS_IDLE

0 commit comments

Comments
 (0)