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