plugwise 1.5.1a3__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plugwise/__init__.py +10 -10
- plugwise/common.py +68 -60
- plugwise/constants.py +47 -34
- plugwise/data.py +95 -67
- plugwise/helper.py +199 -168
- plugwise/legacy/data.py +23 -23
- plugwise/legacy/helper.py +57 -58
- plugwise/legacy/smile.py +18 -16
- plugwise/smile.py +24 -21
- plugwise/util.py +7 -9
- {plugwise-1.5.1a3.dist-info → plugwise-1.6.0.dist-info}/METADATA +1 -1
- plugwise-1.6.0.dist-info/RECORD +17 -0
- {plugwise-1.5.1a3.dist-info → plugwise-1.6.0.dist-info}/WHEEL +1 -1
- plugwise-1.5.1a3.dist-info/RECORD +0 -17
- {plugwise-1.5.1a3.dist-info → plugwise-1.6.0.dist-info}/LICENSE +0 -0
- {plugwise-1.5.1a3.dist-info → plugwise-1.6.0.dist-info}/top_level.txt +0 -0
plugwise/helper.py
CHANGED
@@ -24,6 +24,7 @@ from plugwise.constants import (
|
|
24
24
|
LIMITS,
|
25
25
|
LOCATIONS,
|
26
26
|
LOGGER,
|
27
|
+
MODULE_LOCATOR,
|
27
28
|
NONE,
|
28
29
|
OFF,
|
29
30
|
P1_MEASUREMENTS,
|
@@ -31,11 +32,12 @@ from plugwise.constants import (
|
|
31
32
|
THERMOSTAT_CLASSES,
|
32
33
|
TOGGLES,
|
33
34
|
UOM,
|
35
|
+
ZONE_MEASUREMENTS,
|
34
36
|
ActuatorData,
|
35
37
|
ActuatorDataType,
|
36
38
|
ActuatorType,
|
37
|
-
DeviceData,
|
38
39
|
GatewayData,
|
40
|
+
GwEntityData,
|
39
41
|
SensorType,
|
40
42
|
ThermoLoc,
|
41
43
|
ToggleNameType,
|
@@ -222,6 +224,7 @@ class SmileHelper(SmileCommon):
|
|
222
224
|
self._is_thermostat: bool
|
223
225
|
self._last_active: dict[str, str | None]
|
224
226
|
self._last_modified: dict[str, str] = {}
|
227
|
+
self._loc_data: dict[str, ThermoLoc]
|
225
228
|
self._notifications: dict[str, dict[str, str]] = {}
|
226
229
|
self._on_off_device: bool
|
227
230
|
self._opentherm_device: bool
|
@@ -248,8 +251,7 @@ class SmileHelper(SmileCommon):
|
|
248
251
|
|
249
252
|
self.gateway_id: str
|
250
253
|
self.gw_data: GatewayData = {}
|
251
|
-
self.
|
252
|
-
self.loc_data: dict[str, ThermoLoc]
|
254
|
+
self.gw_entities: dict[str, GwEntityData] = {}
|
253
255
|
self.smile_fw_version: Version | None
|
254
256
|
self.smile_hw_version: str | None
|
255
257
|
self.smile_mac_address: str | None
|
@@ -259,6 +261,7 @@ class SmileHelper(SmileCommon):
|
|
259
261
|
self.smile_type: str
|
260
262
|
self.smile_zigbee_mac_address: str | None
|
261
263
|
self.therms_with_offset_func: list[str] = []
|
264
|
+
self._zones: dict[str, GwEntityData] = {}
|
262
265
|
SmileCommon.__init__(self)
|
263
266
|
|
264
267
|
def _all_appliances(self) -> None:
|
@@ -299,7 +302,8 @@ class SmileHelper(SmileCommon):
|
|
299
302
|
if appl.pwclass in THERMOSTAT_CLASSES and appl.location is None:
|
300
303
|
continue
|
301
304
|
|
302
|
-
appl.
|
305
|
+
appl.available = None
|
306
|
+
appl.entity_id = appliance.attrib["id"]
|
303
307
|
appl.name = appliance.find("name").text
|
304
308
|
appl.model = None
|
305
309
|
appl.model_id = None
|
@@ -313,18 +317,18 @@ class SmileHelper(SmileCommon):
|
|
313
317
|
if not (appl := self._appliance_info_finder(appl, appliance)):
|
314
318
|
continue
|
315
319
|
|
316
|
-
# P1: for gateway and smartmeter switch
|
320
|
+
# P1: for gateway and smartmeter switch entity_id - part 1
|
317
321
|
# This is done to avoid breakage in HA Core
|
318
322
|
if appl.pwclass == "gateway" and self.smile_type == "power":
|
319
|
-
appl.
|
323
|
+
appl.entity_id = appl.location
|
320
324
|
|
321
|
-
self.
|
325
|
+
self._create_gw_entities(appl)
|
322
326
|
|
323
327
|
# For P1 collect the connected SmartMeter info
|
324
328
|
if self.smile_type == "power":
|
325
329
|
self._p1_smartmeter_info_finder(appl)
|
326
|
-
# P1: for gateway and smartmeter switch
|
327
|
-
for item in self.
|
330
|
+
# P1: for gateway and smartmeter switch entity_id - part 2
|
331
|
+
for item in self.gw_entities:
|
328
332
|
if item != self.gateway_id:
|
329
333
|
self.gateway_id = item
|
330
334
|
# Leave for-loop to avoid a 2nd device_id switch
|
@@ -332,13 +336,13 @@ class SmileHelper(SmileCommon):
|
|
332
336
|
|
333
337
|
# Place the gateway and optional heater_central devices as 1st and 2nd
|
334
338
|
for dev_class in ("heater_central", "gateway"):
|
335
|
-
for
|
336
|
-
if
|
337
|
-
|
338
|
-
self.
|
339
|
-
cleared_dict = self.
|
340
|
-
add_to_front = {
|
341
|
-
self.
|
339
|
+
for entity_id, entity in dict(self.gw_entities).items():
|
340
|
+
if entity["dev_class"] == dev_class:
|
341
|
+
tmp_entity = entity
|
342
|
+
self.gw_entities.pop(entity_id)
|
343
|
+
cleared_dict = self.gw_entities
|
344
|
+
add_to_front = {entity_id: tmp_entity}
|
345
|
+
self.gw_entities = {**add_to_front, **cleared_dict}
|
342
346
|
|
343
347
|
def _all_locations(self) -> None:
|
344
348
|
"""Collect all locations."""
|
@@ -350,20 +354,19 @@ class SmileHelper(SmileCommon):
|
|
350
354
|
if loc.name == "Home":
|
351
355
|
self._home_location = loc.loc_id
|
352
356
|
|
353
|
-
self.
|
357
|
+
self._loc_data[loc.loc_id] = {"name": loc.name}
|
354
358
|
|
355
359
|
def _p1_smartmeter_info_finder(self, appl: Munch) -> None:
|
356
360
|
"""Collect P1 DSMR SmartMeter info."""
|
357
|
-
loc_id = next(iter(self.
|
361
|
+
loc_id = next(iter(self._loc_data.keys()))
|
358
362
|
location = self._domain_objects.find(f'./location[@id="{loc_id}"]')
|
359
|
-
locator =
|
360
|
-
|
361
|
-
module_data = self._get_module_data(location, locator, mod_type)
|
363
|
+
locator = MODULE_LOCATOR
|
364
|
+
module_data = self._get_module_data(location, locator)
|
362
365
|
if not module_data["contents"]:
|
363
366
|
LOGGER.error("No module data found for SmartMeter") # pragma: no cover
|
364
367
|
return None # pragma: no cover
|
365
368
|
|
366
|
-
appl.
|
369
|
+
appl.entity_id = self.gateway_id
|
367
370
|
appl.firmware = module_data["firmware_version"]
|
368
371
|
appl.hardware = module_data["hardware_version"]
|
369
372
|
appl.location = loc_id
|
@@ -375,7 +378,7 @@ class SmileHelper(SmileCommon):
|
|
375
378
|
appl.vendor_name = module_data["vendor_name"]
|
376
379
|
appl.zigbee_mac = None
|
377
380
|
|
378
|
-
self.
|
381
|
+
self._create_gw_entities(appl)
|
379
382
|
|
380
383
|
def _appliance_info_finder(self, appl: Munch, appliance: etree) -> Munch:
|
381
384
|
"""Collect info for all appliances found."""
|
@@ -391,18 +394,18 @@ class SmileHelper(SmileCommon):
|
|
391
394
|
self._appl_heater_central_info(appl, appliance, False) # False means non-legacy device
|
392
395
|
self._appl_dhw_mode_info(appl, appliance)
|
393
396
|
# Skip orphaned heater_central (Core Issue #104433)
|
394
|
-
if appl.
|
397
|
+
if appl.entity_id != self._heater_id:
|
395
398
|
return Munch()
|
396
399
|
return appl
|
397
400
|
case _ as s if s.endswith("_plug"):
|
398
401
|
# Collect info from plug-types (Plug, Aqara Smart Plug)
|
399
|
-
locator =
|
400
|
-
|
401
|
-
module_data = self._get_module_data(appliance, locator, mod_type)
|
402
|
+
locator = MODULE_LOCATOR
|
403
|
+
module_data = self._get_module_data(appliance, locator)
|
402
404
|
# A plug without module-data is orphaned/ no present
|
403
405
|
if not module_data["contents"]:
|
404
406
|
return Munch()
|
405
407
|
|
408
|
+
appl.available = module_data["reachable"]
|
406
409
|
appl.firmware = module_data["firmware_version"]
|
407
410
|
appl.hardware = module_data["hardware_version"]
|
408
411
|
appl.model_id = module_data["vendor_model"]
|
@@ -479,24 +482,40 @@ class SmileHelper(SmileCommon):
|
|
479
482
|
|
480
483
|
return therm_list
|
481
484
|
|
482
|
-
def
|
483
|
-
"""Helper-function for smile.py:
|
485
|
+
def _get_zone_data(self, loc_id: str) -> GwEntityData:
|
486
|
+
"""Helper-function for smile.py: _get_entity_data().
|
484
487
|
|
485
|
-
Collect the
|
488
|
+
Collect the location-data based on location id.
|
486
489
|
"""
|
487
|
-
data:
|
490
|
+
data: GwEntityData = {"sensors": {}}
|
491
|
+
zone = self._zones[loc_id]
|
492
|
+
measurements = ZONE_MEASUREMENTS
|
493
|
+
if (
|
494
|
+
location := self._domain_objects.find(f'./location[@id="{loc_id}"]')
|
495
|
+
) is not None:
|
496
|
+
self._appliance_measurements(location, data, measurements)
|
497
|
+
self._get_actuator_functionalities(location, zone, data)
|
498
|
+
|
499
|
+
return data
|
500
|
+
|
501
|
+
def _get_measurement_data(self, entity_id: str) -> GwEntityData:
|
502
|
+
"""Helper-function for smile.py: _get_entity_data().
|
503
|
+
|
504
|
+
Collect the appliance-data based on entity_id.
|
505
|
+
"""
|
506
|
+
data: GwEntityData = {"binary_sensors": {}, "sensors": {}, "switches": {}}
|
488
507
|
# Get P1 smartmeter data from LOCATIONS
|
489
|
-
|
508
|
+
entity = self.gw_entities[entity_id]
|
490
509
|
# !! DON'T CHANGE below two if-lines, will break stuff !!
|
491
510
|
if self.smile_type == "power":
|
492
|
-
if
|
493
|
-
data.update(self._power_data_from_location(
|
511
|
+
if entity["dev_class"] == "smartmeter":
|
512
|
+
data.update(self._power_data_from_location(entity["location"]))
|
494
513
|
|
495
514
|
return data
|
496
515
|
|
497
516
|
# Get non-P1 data from APPLIANCES
|
498
517
|
measurements = DEVICE_MEASUREMENTS
|
499
|
-
if self._is_thermostat and
|
518
|
+
if self._is_thermostat and entity_id == self._heater_id:
|
500
519
|
measurements = HEATER_CENTRAL_MEASUREMENTS
|
501
520
|
# Show the allowed dhw_modes (Loria only)
|
502
521
|
if self._dhw_allowed_modes:
|
@@ -504,7 +523,7 @@ class SmileHelper(SmileCommon):
|
|
504
523
|
# Counting of this item is done in _appliance_measurements()
|
505
524
|
|
506
525
|
if (
|
507
|
-
appliance := self._domain_objects.find(f'./appliance[@id="{
|
526
|
+
appliance := self._domain_objects.find(f'./appliance[@id="{entity_id}"]')
|
508
527
|
) is not None:
|
509
528
|
self._appliance_measurements(appliance, data, measurements)
|
510
529
|
self._get_lock_state(appliance, data)
|
@@ -513,24 +532,11 @@ class SmileHelper(SmileCommon):
|
|
513
532
|
self._get_toggle_state(appliance, toggle, name, data)
|
514
533
|
|
515
534
|
if appliance.find("type").text in ACTUATOR_CLASSES:
|
516
|
-
self._get_actuator_functionalities(appliance,
|
535
|
+
self._get_actuator_functionalities(appliance, entity, data)
|
517
536
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
if dev_id == self.gateway_id and self.smile(ADAM):
|
522
|
-
self._get_regulation_mode(appliance, data)
|
523
|
-
self._get_gateway_mode(appliance, data)
|
524
|
-
|
525
|
-
# Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS - under Home
|
526
|
-
# The outdoor_temperature present in APPLIANCES is a local sensor connected to the active device
|
527
|
-
if self._is_thermostat and dev_id == self.gateway_id:
|
528
|
-
outdoor_temperature = self._object_value(
|
529
|
-
self._home_location, "outdoor_temperature"
|
530
|
-
)
|
531
|
-
if outdoor_temperature is not None:
|
532
|
-
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
533
|
-
self._count += 1
|
537
|
+
self._get_regulation_mode(appliance, entity_id, data)
|
538
|
+
self._get_gateway_mode(appliance, entity_id, data)
|
539
|
+
self._get_gateway_outdoor_temp(entity_id, data)
|
534
540
|
|
535
541
|
if "c_heating_state" in data:
|
536
542
|
self._process_c_heating_state(data)
|
@@ -538,45 +544,19 @@ class SmileHelper(SmileCommon):
|
|
538
544
|
data.pop("c_heating_state")
|
539
545
|
self._count -= 1
|
540
546
|
|
541
|
-
if self._is_thermostat and self.smile(ANNA)
|
542
|
-
|
543
|
-
if "elga_status_code" in data:
|
544
|
-
if data["thermostat_supports_cooling"]:
|
545
|
-
# Techneco Elga has cooling-capability
|
546
|
-
self._cooling_present = True
|
547
|
-
data["model"] = "Generic heater/cooler"
|
548
|
-
self._cooling_enabled = data["elga_status_code"] in (8, 9)
|
549
|
-
data["binary_sensors"]["cooling_state"] = self._cooling_active = (
|
550
|
-
data["elga_status_code"] == 8
|
551
|
-
)
|
552
|
-
# Elga has no cooling-switch
|
553
|
-
if "cooling_ena_switch" in data["switches"]:
|
554
|
-
data["switches"].pop("cooling_ena_switch")
|
555
|
-
self._count -= 1
|
556
|
-
|
557
|
-
data.pop("elga_status_code", None)
|
558
|
-
self._count -= 1
|
559
|
-
|
560
|
-
# Loria/Thermastage: cooling-related is based on cooling_state
|
561
|
-
# and modulation_level
|
562
|
-
elif self._cooling_present and "cooling_state" in data["binary_sensors"]:
|
563
|
-
self._cooling_enabled = data["binary_sensors"]["cooling_state"]
|
564
|
-
self._cooling_active = data["sensors"]["modulation_level"] == 100
|
565
|
-
# For Loria the above does not work (pw-beta issue #301)
|
566
|
-
if "cooling_ena_switch" in data["switches"]:
|
567
|
-
self._cooling_enabled = data["switches"]["cooling_ena_switch"]
|
568
|
-
self._cooling_active = data["binary_sensors"]["cooling_state"]
|
547
|
+
if self._is_thermostat and self.smile(ANNA):
|
548
|
+
self._update_anna_cooling(entity_id, data)
|
569
549
|
|
570
550
|
self._cleanup_data(data)
|
571
551
|
|
572
552
|
return data
|
573
553
|
|
574
|
-
def _power_data_from_location(self, loc_id: str) ->
|
575
|
-
"""Helper-function for smile.py:
|
554
|
+
def _power_data_from_location(self, loc_id: str) -> GwEntityData:
|
555
|
+
"""Helper-function for smile.py: _get_entity_data().
|
576
556
|
|
577
557
|
Collect the power-data based on Location ID, from LOCATIONS.
|
578
558
|
"""
|
579
|
-
|
559
|
+
data: GwEntityData = {"sensors": {}}
|
580
560
|
loc = Munch()
|
581
561
|
log_list: list[str] = ["point_log", "cumulative_log", "interval_log"]
|
582
562
|
t_string = "tariff"
|
@@ -585,15 +565,15 @@ class SmileHelper(SmileCommon):
|
|
585
565
|
loc.logs = search.find(f'./location[@id="{loc_id}"]/logs')
|
586
566
|
for loc.measurement, loc.attrs in P1_MEASUREMENTS.items():
|
587
567
|
for loc.log_type in log_list:
|
588
|
-
self._collect_power_values(
|
568
|
+
self._collect_power_values(data, loc, t_string)
|
589
569
|
|
590
|
-
self._count += len(
|
591
|
-
return
|
570
|
+
self._count += len(data["sensors"])
|
571
|
+
return data
|
592
572
|
|
593
573
|
def _appliance_measurements(
|
594
574
|
self,
|
595
575
|
appliance: etree,
|
596
|
-
data:
|
576
|
+
data: GwEntityData,
|
597
577
|
measurements: dict[str, DATA | UOM],
|
598
578
|
) -> None:
|
599
579
|
"""Helper-function for _get_measurement_data() - collect appliance measurement data."""
|
@@ -621,14 +601,10 @@ class SmileHelper(SmileCommon):
|
|
621
601
|
appl_i_loc.text, ENERGY_WATT_HOUR
|
622
602
|
)
|
623
603
|
|
624
|
-
self.
|
625
|
-
self._count += len(data["sensors"])
|
626
|
-
self._count += len(data["switches"])
|
627
|
-
# Don't count the above top-level dicts, only the remaining single items
|
628
|
-
self._count += len(data) - 3
|
604
|
+
self._count_data_items(data)
|
629
605
|
|
630
606
|
def _get_toggle_state(
|
631
|
-
self, xml: etree, toggle: str, name: ToggleNameType, data:
|
607
|
+
self, xml: etree, toggle: str, name: ToggleNameType, data: GwEntityData
|
632
608
|
) -> None:
|
633
609
|
"""Helper-function for _get_measurement_data().
|
634
610
|
|
@@ -657,14 +633,16 @@ class SmileHelper(SmileCommon):
|
|
657
633
|
)
|
658
634
|
|
659
635
|
def _get_actuator_functionalities(
|
660
|
-
self, xml: etree,
|
636
|
+
self, xml: etree, entity: GwEntityData, data: GwEntityData
|
661
637
|
) -> None:
|
662
638
|
"""Helper-function for _get_measurement_data()."""
|
663
639
|
for item in ACTIVE_ACTUATORS:
|
664
640
|
# Skip max_dhw_temperature, not initially valid,
|
665
|
-
# skip thermostat for
|
641
|
+
# skip thermostat for all but zones with thermostats
|
666
642
|
if item == "max_dhw_temperature" or (
|
667
|
-
item == "thermostat" and
|
643
|
+
item == "thermostat" and (
|
644
|
+
entity["dev_class"] != "climate" if self.smile(ADAM) else entity["dev_class"] != "thermostat"
|
645
|
+
)
|
668
646
|
):
|
669
647
|
continue
|
670
648
|
|
@@ -710,52 +688,52 @@ class SmileHelper(SmileCommon):
|
|
710
688
|
act_item = cast(ActuatorType, item)
|
711
689
|
data[act_item] = temp_dict
|
712
690
|
|
713
|
-
def
|
691
|
+
def _get_regulation_mode(
|
692
|
+
self, appliance: etree, entity_id: str, data: GwEntityData
|
693
|
+
) -> None:
|
714
694
|
"""Helper-function for _get_measurement_data().
|
715
695
|
|
716
|
-
|
696
|
+
Adam: collect the gateway regulation_mode.
|
717
697
|
"""
|
718
|
-
if self.smile(ADAM):
|
719
|
-
|
720
|
-
locator = "./logs/interval_log/electricity_interval_meter"
|
721
|
-
mod_type = "electricity_interval_meter"
|
722
|
-
module_data = self._get_module_data(appliance, locator, mod_type)
|
723
|
-
if not module_data["contents"]:
|
724
|
-
# Try collecting for a wireless thermostat
|
725
|
-
locator = "./logs/point_log[type='thermostat']/thermostat"
|
726
|
-
mod_type = "thermostat"
|
727
|
-
module_data = self._get_module_data(appliance, locator, mod_type)
|
728
|
-
if not module_data["contents"]:
|
729
|
-
LOGGER.error("No module data found for Plug or wireless thermostat") # pragma: no cover
|
730
|
-
return None # pragma: no cover
|
731
|
-
|
732
|
-
if module_data["reachable"] is not None:
|
733
|
-
data["available"] = module_data["reachable"]
|
734
|
-
self._count += 1
|
735
|
-
|
736
|
-
def _get_regulation_mode(self, appliance: etree, data: DeviceData) -> None:
|
737
|
-
"""Helper-function for _get_measurement_data().
|
698
|
+
if not (self.smile(ADAM) and entity_id == self.gateway_id):
|
699
|
+
return
|
738
700
|
|
739
|
-
Collect the gateway regulation_mode.
|
740
|
-
"""
|
741
701
|
locator = "./actuator_functionalities/regulation_mode_control_functionality"
|
742
702
|
if (search := appliance.find(locator)) is not None:
|
743
703
|
data["select_regulation_mode"] = search.find("mode").text
|
744
704
|
self._count += 1
|
745
705
|
self._cooling_enabled = data["select_regulation_mode"] == "cooling"
|
746
706
|
|
747
|
-
def _get_gateway_mode(
|
707
|
+
def _get_gateway_mode(
|
708
|
+
self, appliance: etree, entity_id: str, data: GwEntityData
|
709
|
+
) -> None:
|
748
710
|
"""Helper-function for _get_measurement_data().
|
749
711
|
|
750
|
-
|
712
|
+
Adam: collect the gateway mode.
|
751
713
|
"""
|
714
|
+
if not (self.smile(ADAM) and entity_id == self.gateway_id):
|
715
|
+
return
|
716
|
+
|
752
717
|
locator = "./actuator_functionalities/gateway_mode_control_functionality"
|
753
718
|
if (search := appliance.find(locator)) is not None:
|
754
719
|
data["select_gateway_mode"] = search.find("mode").text
|
755
720
|
self._count += 1
|
756
721
|
|
722
|
+
def _get_gateway_outdoor_temp(self, entity_id: str, data: GwEntityData) -> None:
|
723
|
+
"""Adam & Anna: the Smile outdoor_temperature is present in DOMAIN_OBJECTS and LOCATIONS.
|
724
|
+
|
725
|
+
Available under the Home location.
|
726
|
+
"""
|
727
|
+
if self._is_thermostat and entity_id == self.gateway_id:
|
728
|
+
outdoor_temperature = self._object_value(
|
729
|
+
self._home_location, "outdoor_temperature"
|
730
|
+
)
|
731
|
+
if outdoor_temperature is not None:
|
732
|
+
data.update({"sensors": {"outdoor_temperature": outdoor_temperature}})
|
733
|
+
self._count += 1
|
734
|
+
|
757
735
|
def _object_value(self, obj_id: str, measurement: str) -> float | int | None:
|
758
|
-
"""Helper-function for smile.py:
|
736
|
+
"""Helper-function for smile.py: _get_entity_data().
|
759
737
|
|
760
738
|
Obtain the value/state for the given object from a location in DOMAIN_OBJECTS
|
761
739
|
"""
|
@@ -767,35 +745,80 @@ class SmileHelper(SmileCommon):
|
|
767
745
|
|
768
746
|
return val
|
769
747
|
|
770
|
-
def _process_c_heating_state(self, data:
|
748
|
+
def _process_c_heating_state(self, data: GwEntityData) -> None:
|
771
749
|
"""Helper-function for _get_measurement_data().
|
772
750
|
|
773
751
|
Process the central_heating_state value.
|
774
752
|
"""
|
753
|
+
# Adam or Anna + OnOff device
|
775
754
|
if self._on_off_device:
|
776
|
-
|
777
|
-
# Solution for Core issue #81839
|
778
|
-
if self.smile(ANNA):
|
779
|
-
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
780
|
-
|
781
|
-
# Adam + OnOff cooling: use central_heating_state to show heating/cooling_state
|
782
|
-
if self.smile(ADAM):
|
783
|
-
if "heating_state" not in data["binary_sensors"]:
|
784
|
-
self._count += 1
|
785
|
-
data["binary_sensors"]["heating_state"] = False
|
786
|
-
if "cooling_state" not in data["binary_sensors"]:
|
787
|
-
self._count += 1
|
788
|
-
data["binary_sensors"]["cooling_state"] = False
|
789
|
-
if self._cooling_enabled:
|
790
|
-
data["binary_sensors"]["cooling_state"] = data["c_heating_state"]
|
791
|
-
else:
|
792
|
-
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
755
|
+
self._process_on_off_device_c_heating_state(data)
|
793
756
|
|
794
757
|
# Anna + Elga: use central_heating_state to show heating_state
|
795
758
|
if self._elga:
|
796
759
|
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
797
760
|
|
798
|
-
def
|
761
|
+
def _process_on_off_device_c_heating_state(self, data: GwEntityData) -> None:
|
762
|
+
"""Adam or Anna + OnOff device - use central_heating_state to show heating/cooling_state.
|
763
|
+
|
764
|
+
Solution for Core issue #81839.
|
765
|
+
"""
|
766
|
+
if self.smile(ANNA):
|
767
|
+
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
768
|
+
|
769
|
+
if self.smile(ADAM):
|
770
|
+
if "heating_state" not in data["binary_sensors"]:
|
771
|
+
self._count += 1
|
772
|
+
data["binary_sensors"]["heating_state"] = False
|
773
|
+
if "cooling_state" not in data["binary_sensors"]:
|
774
|
+
self._count += 1
|
775
|
+
data["binary_sensors"]["cooling_state"] = False
|
776
|
+
if self._cooling_enabled:
|
777
|
+
data["binary_sensors"]["cooling_state"] = data["c_heating_state"]
|
778
|
+
else:
|
779
|
+
data["binary_sensors"]["heating_state"] = data["c_heating_state"]
|
780
|
+
|
781
|
+
def _update_anna_cooling(self, entity_id: str, data: GwEntityData) -> None:
|
782
|
+
"""Update the Anna heater_central device for cooling.
|
783
|
+
|
784
|
+
Support added for Techneco Elga and Thercon Loria/Thermastage.
|
785
|
+
"""
|
786
|
+
if entity_id != self._heater_id:
|
787
|
+
return
|
788
|
+
|
789
|
+
if "elga_status_code" in data:
|
790
|
+
self._update_elga_cooling(data)
|
791
|
+
elif self._cooling_present and "cooling_state" in data["binary_sensors"]:
|
792
|
+
self._update_loria_cooling(data)
|
793
|
+
|
794
|
+
def _update_elga_cooling(self, data: GwEntityData) -> None:
|
795
|
+
"""# Anna+Elga: base cooling_state on the elga-status-code."""
|
796
|
+
if data["thermostat_supports_cooling"]:
|
797
|
+
# Techneco Elga has cooling-capability
|
798
|
+
self._cooling_present = True
|
799
|
+
data["model"] = "Generic heater/cooler"
|
800
|
+
self._cooling_enabled = data["elga_status_code"] in (8, 9)
|
801
|
+
data["binary_sensors"]["cooling_state"] = self._cooling_active = (
|
802
|
+
data["elga_status_code"] == 8
|
803
|
+
)
|
804
|
+
# Elga has no cooling-switch
|
805
|
+
if "cooling_ena_switch" in data["switches"]:
|
806
|
+
data["switches"].pop("cooling_ena_switch")
|
807
|
+
self._count -= 1
|
808
|
+
|
809
|
+
data.pop("elga_status_code", None)
|
810
|
+
self._count -= 1
|
811
|
+
|
812
|
+
def _update_loria_cooling(self, data: GwEntityData) -> None:
|
813
|
+
"""Loria/Thermastage: base cooling-related on cooling_state and modulation_level."""
|
814
|
+
self._cooling_enabled = data["binary_sensors"]["cooling_state"]
|
815
|
+
self._cooling_active = data["sensors"]["modulation_level"] == 100
|
816
|
+
# For Loria the above does not work (pw-beta issue #301)
|
817
|
+
if "cooling_ena_switch" in data["switches"]:
|
818
|
+
self._cooling_enabled = data["switches"]["cooling_ena_switch"]
|
819
|
+
self._cooling_active = data["binary_sensors"]["cooling_state"]
|
820
|
+
|
821
|
+
def _cleanup_data(self, data: GwEntityData) -> None:
|
799
822
|
"""Helper-function for _get_measurement_data().
|
800
823
|
|
801
824
|
Clean up the data dict.
|
@@ -818,7 +841,7 @@ class SmileHelper(SmileCommon):
|
|
818
841
|
self._count -= 1
|
819
842
|
|
820
843
|
def _scan_thermostats(self) -> None:
|
821
|
-
"""Helper-function for smile.py:
|
844
|
+
"""Helper-function for smile.py: get_all_entities().
|
822
845
|
|
823
846
|
Update locations with thermostat ranking results and use
|
824
847
|
the result to update the device_class of secondary thermostats.
|
@@ -826,22 +849,26 @@ class SmileHelper(SmileCommon):
|
|
826
849
|
self._thermo_locs = self._match_locations()
|
827
850
|
|
828
851
|
thermo_matching: dict[str, int] = {
|
829
|
-
"thermostat":
|
852
|
+
"thermostat": 2,
|
830
853
|
"zone_thermometer": 2,
|
831
854
|
"zone_thermostat": 2,
|
832
855
|
"thermostatic_radiator_valve": 1,
|
833
856
|
}
|
834
857
|
|
835
858
|
for loc_id in self._thermo_locs:
|
836
|
-
for
|
837
|
-
self._rank_thermostat(thermo_matching, loc_id,
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
859
|
+
for entity_id, entity in self.gw_entities.items():
|
860
|
+
self._rank_thermostat(thermo_matching, loc_id, entity_id, entity)
|
861
|
+
|
862
|
+
for loc_id, loc_data in list(self._thermo_locs.items()):
|
863
|
+
if loc_data["primary_prio"] != 0:
|
864
|
+
self._zones[loc_id] = {
|
865
|
+
"dev_class": "climate",
|
866
|
+
"model": "ThermoZone",
|
867
|
+
"name": loc_data["name"],
|
868
|
+
"thermostats": {"primary": loc_data["primary"], "secondary": loc_data["secondary"]},
|
869
|
+
"vendor": "Plugwise",
|
870
|
+
}
|
871
|
+
self._count += 3
|
845
872
|
|
846
873
|
def _match_locations(self) -> dict[str, ThermoLoc]:
|
847
874
|
"""Helper-function for _scan_thermostats().
|
@@ -849,11 +876,11 @@ class SmileHelper(SmileCommon):
|
|
849
876
|
Match appliances with locations.
|
850
877
|
"""
|
851
878
|
matched_locations: dict[str, ThermoLoc] = {}
|
852
|
-
for location_id, location_details in self.
|
853
|
-
for appliance_details in self.
|
879
|
+
for location_id, location_details in self._loc_data.items():
|
880
|
+
for appliance_details in self.gw_entities.values():
|
854
881
|
if appliance_details["location"] == location_id:
|
855
882
|
location_details.update(
|
856
|
-
{"primary":
|
883
|
+
{"primary": [], "primary_prio": 0, "secondary": []}
|
857
884
|
)
|
858
885
|
matched_locations[location_id] = location_details
|
859
886
|
|
@@ -864,30 +891,34 @@ class SmileHelper(SmileCommon):
|
|
864
891
|
thermo_matching: dict[str, int],
|
865
892
|
loc_id: str,
|
866
893
|
appliance_id: str,
|
867
|
-
appliance_details:
|
894
|
+
appliance_details: GwEntityData,
|
868
895
|
) -> None:
|
869
896
|
"""Helper-function for _scan_thermostats().
|
870
897
|
|
871
898
|
Rank the thermostat based on appliance_details: primary or secondary.
|
899
|
+
Note: there can be several primary and secondary thermostats.
|
872
900
|
"""
|
873
901
|
appl_class = appliance_details["dev_class"]
|
874
902
|
appl_d_loc = appliance_details["location"]
|
903
|
+
thermo_loc = self._thermo_locs[loc_id]
|
875
904
|
if loc_id == appl_d_loc and appl_class in thermo_matching:
|
905
|
+
if thermo_matching[appl_class] == thermo_loc["primary_prio"]:
|
906
|
+
thermo_loc["primary"].append(appliance_id)
|
876
907
|
# Pre-elect new primary
|
877
|
-
|
908
|
+
elif (thermo_rank := thermo_matching[appl_class]) > thermo_loc["primary_prio"]:
|
909
|
+
thermo_loc["primary_prio"] = thermo_rank
|
878
910
|
# Demote former primary
|
879
|
-
if (tl_primary:=
|
880
|
-
|
911
|
+
if (tl_primary := thermo_loc["primary"]):
|
912
|
+
thermo_loc["secondary"] += tl_primary
|
913
|
+
thermo_loc["primary"] = []
|
881
914
|
|
882
915
|
# Crown primary
|
883
|
-
|
884
|
-
self._thermo_locs[loc_id]["primary"] = appliance_id
|
885
|
-
|
916
|
+
thermo_loc["primary"].append(appliance_id)
|
886
917
|
else:
|
887
|
-
|
918
|
+
thermo_loc["secondary"].append(appliance_id)
|
888
919
|
|
889
920
|
def _control_state(self, loc_id: str) -> str | bool:
|
890
|
-
"""Helper-function for
|
921
|
+
"""Helper-function for _get_adam_data().
|
891
922
|
|
892
923
|
Adam: find the thermostat control_state of a location, from DOMAIN_OBJECTS.
|
893
924
|
Represents the heating/cooling demand-state of the local primary thermostat.
|
@@ -902,7 +933,7 @@ class SmileHelper(SmileCommon):
|
|
902
933
|
return False
|
903
934
|
|
904
935
|
def _heating_valves(self) -> int | bool:
|
905
|
-
"""Helper-function for smile.py:
|
936
|
+
"""Helper-function for smile.py: _get_adam_data().
|
906
937
|
|
907
938
|
Collect amount of open valves indicating active direct heating.
|
908
939
|
For cases where the heat is provided from an external shared source (city heating).
|
@@ -987,7 +1018,7 @@ class SmileHelper(SmileCommon):
|
|
987
1018
|
return schedule_ids
|
988
1019
|
|
989
1020
|
def _schedules(self, location: str) -> tuple[list[str], str]:
|
990
|
-
"""Helper-function for smile.py:
|
1021
|
+
"""Helper-function for smile.py: _climate_data().
|
991
1022
|
|
992
1023
|
Obtain the available schedules/schedules. Adam: a schedule can be connected to more than one location.
|
993
1024
|
NEW: when a location_id is present then the schedule is active. Valid for both Adam and non-legacy Anna.
|