@@ -4,24 +4,47 @@
|
|
4 |
|
5 |
from typing import Any
|
6 |
|
|
|
|
|
|
|
7 |
from homeassistant.config_entries import ConfigEntry
|
8 |
-
from homeassistant.const import Platform
|
9 |
-
from homeassistant.core import
|
|
|
|
|
|
|
|
|
10 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
11 |
|
12 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
14 |
|
15 |
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
16 |
|
17 |
|
18 |
async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
19 |
-
"""Set up Plugwise
|
20 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
21 |
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
await coordinator.async_config_entry_first_refresh()
|
24 |
-
|
|
|
25 |
|
26 |
entry.runtime_data = coordinator
|
27 |
|
@@ -36,16 +59,44 @@
|
|
36 |
sw_version=str(coordinator.api.smile_version),
|
37 |
) # required for adding the entity-less P1 Gateway
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
return True
|
42 |
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
45 |
-
"""Unload
|
46 |
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
47 |
|
48 |
-
|
49 |
@callback
|
50 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
51 |
"""Migrate Plugwise entity entries.
|
@@ -72,15 +123,14 @@
|
|
72 |
# No migration needed
|
73 |
return None
|
74 |
|
75 |
-
|
76 |
-
def migrate_sensor_entities(
|
77 |
hass: HomeAssistant,
|
78 |
coordinator: PlugwiseDataUpdateCoordinator,
|
79 |
) -> None:
|
80 |
"""Migrate Sensors if needed."""
|
81 |
ent_reg = er.async_get(hass)
|
82 |
|
83 |
-
#
|
84 |
# to opentherm_outdoor_air_temperature sensor
|
85 |
for device_id, device in coordinator.data.devices.items():
|
86 |
if device["dev_class"] != "heater_central":
|
@@ -91,10 +141,27 @@
|
|
91 |
Platform.SENSOR, DOMAIN, old_unique_id
|
92 |
):
|
93 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
94 |
-
LOGGER
|
95 |
-
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
|
96 |
-
entity_id,
|
97 |
-
old_unique_id,
|
98 |
-
new_unique_id,
|
99 |
-
)
|
100 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
from typing import Any
|
6 |
|
7 |
+
from plugwise.exceptions import PlugwiseError
|
8 |
+
import voluptuous as vol # pw-beta delete_notification
|
9 |
+
|
10 |
from homeassistant.config_entries import ConfigEntry
|
11 |
+
from homeassistant.const import CONF_TIMEOUT, Platform
|
12 |
+
from homeassistant.core import (
|
13 |
+
HomeAssistant,
|
14 |
+
ServiceCall, # pw-beta delete_notification
|
15 |
+
callback,
|
16 |
+
)
|
17 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
18 |
|
19 |
+
from .const import (
|
20 |
+
CONF_REFRESH_INTERVAL, # pw-beta options
|
21 |
+
DOMAIN,
|
22 |
+
LOGGER,
|
23 |
+
PLATFORMS,
|
24 |
+
SERVICE_DELETE, # pw-beta delete_notifications
|
25 |
+
)
|
26 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
27 |
|
28 |
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
29 |
|
30 |
|
31 |
async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
32 |
+
"""Set up Plugwise from a config entry."""
|
33 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
34 |
|
35 |
+
cooldown = 1.5 # pw-beta frontend refresh-interval
|
36 |
+
if (
|
37 |
+
custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
|
38 |
+
) is not None: # pragma: no cover
|
39 |
+
cooldown = custom_refresh
|
40 |
+
LOGGER.debug("DUC cooldown interval: %s", cooldown)
|
41 |
+
|
42 |
+
coordinator = PlugwiseDataUpdateCoordinator(
|
43 |
+
hass, cooldown
|
44 |
+
) # pw-beta - cooldown, update_interval as extra
|
45 |
await coordinator.async_config_entry_first_refresh()
|
46 |
+
|
47 |
+
await async_migrate_sensor_entities(hass, coordinator)
|
48 |
|
49 |
entry.runtime_data = coordinator
|
50 |
|
|
|
59 |
sw_version=str(coordinator.api.smile_version),
|
60 |
) # required for adding the entity-less P1 Gateway
|
61 |
|
62 |
+
async def delete_notification(
|
63 |
+
call: ServiceCall,
|
64 |
+
) -> None: # pragma: no cover # pw-beta: HA service - delete_notification
|
65 |
+
"""Service: delete the Plugwise Notification."""
|
66 |
+
LOGGER.debug(
|
67 |
+
"Service delete PW Notification called for %s",
|
68 |
+
coordinator.api.smile_name,
|
69 |
+
)
|
70 |
+
try:
|
71 |
+
await coordinator.api.delete_notification()
|
72 |
+
LOGGER.debug("PW Notification deleted")
|
73 |
+
except PlugwiseError:
|
74 |
+
LOGGER.debug(
|
75 |
+
"Failed to delete the Plugwise Notification for %s",
|
76 |
+
coordinator.api.smile_name,
|
77 |
+
)
|
78 |
+
|
79 |
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
80 |
|
81 |
+
entry.async_on_unload(entry.add_update_listener(update_listener)) # pw-beta options_flow
|
82 |
+
for component in PLATFORMS: # pw-beta delete_notification
|
83 |
+
if component == Platform.BINARY_SENSOR:
|
84 |
+
hass.services.async_register(
|
85 |
+
DOMAIN, SERVICE_DELETE, delete_notification, schema=vol.Schema({})
|
86 |
+
)
|
87 |
+
|
88 |
return True
|
89 |
|
90 |
+
async def update_listener(
|
91 |
+
hass: HomeAssistant, entry: PlugwiseConfigEntry
|
92 |
+
) -> None: # pragma: no cover # pw-beta
|
93 |
+
"""Handle options update."""
|
94 |
+
await hass.config_entries.async_reload(entry.entry_id)
|
95 |
|
96 |
async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
|
97 |
+
"""Unload Plugwise."""
|
98 |
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
99 |
|
|
|
100 |
@callback
|
101 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
102 |
"""Migrate Plugwise entity entries.
|
|
|
123 |
# No migration needed
|
124 |
return None
|
125 |
|
126 |
+
async def async_migrate_sensor_entities(
|
|
|
127 |
hass: HomeAssistant,
|
128 |
coordinator: PlugwiseDataUpdateCoordinator,
|
129 |
) -> None:
|
130 |
"""Migrate Sensors if needed."""
|
131 |
ent_reg = er.async_get(hass)
|
132 |
|
133 |
+
# Migrate opentherm_outdoor_temperature
|
134 |
# to opentherm_outdoor_air_temperature sensor
|
135 |
for device_id, device in coordinator.data.devices.items():
|
136 |
if device["dev_class"] != "heater_central":
|
|
|
141 |
Platform.SENSOR, DOMAIN, old_unique_id
|
142 |
):
|
143 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
144 |
+
# Upstream remove LOGGER debug
|
|
|
|
|
|
|
|
|
|
|
145 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
146 |
+
|
147 |
+
# pw-beta only - revert adding CONF_TIMEOUT to config_entry in v0.53.3
|
148 |
+
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
149 |
+
"""Migrate back to v1.1 config entry."""
|
150 |
+
if entry.version > 1:
|
151 |
+
# This means the user has downgraded from a future version
|
152 |
+
return False
|
153 |
+
|
154 |
+
if entry.version == 1 and entry.minor_version == 2:
|
155 |
+
new_data = {**entry.data}
|
156 |
+
new_data.pop(CONF_TIMEOUT)
|
157 |
+
hass.config_entries.async_update_entry(
|
158 |
+
entry, data=new_data, minor_version=1, version=1
|
159 |
+
)
|
160 |
+
|
161 |
+
LOGGER.debug(
|
162 |
+
"Migration to version %s.%s successful",
|
163 |
+
entry.version,
|
164 |
+
entry.minor_version,
|
165 |
+
)
|
166 |
+
|
167 |
+
return True
|
@@ -8,6 +8,9 @@
|
|
8 |
|
9 |
from plugwise.constants import BinarySensorType
|
10 |
|
|
|
|
|
|
|
11 |
from homeassistant.components.binary_sensor import (
|
12 |
BinarySensorDeviceClass,
|
13 |
BinarySensorEntity,
|
@@ -18,11 +21,27 @@
|
|
18 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
19 |
|
20 |
from . import PlugwiseConfigEntry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
22 |
from .entity import PlugwiseEntity
|
23 |
|
24 |
-
SEVERITIES = ["other", "info", "warning", "error"]
|
25 |
-
|
26 |
# Coordinator is used to centralize the data updates
|
27 |
PARALLEL_UPDATES = 0
|
28 |
|
@@ -34,50 +53,51 @@
|
|
34 |
key: BinarySensorType
|
35 |
|
36 |
|
37 |
-
|
|
|
38 |
PlugwiseBinarySensorEntityDescription(
|
39 |
-
key=
|
40 |
device_class=BinarySensorDeviceClass.BATTERY,
|
41 |
entity_category=EntityCategory.DIAGNOSTIC,
|
42 |
),
|
43 |
PlugwiseBinarySensorEntityDescription(
|
44 |
-
key=
|
45 |
-
translation_key=
|
46 |
entity_category=EntityCategory.DIAGNOSTIC,
|
47 |
),
|
48 |
PlugwiseBinarySensorEntityDescription(
|
49 |
-
key=
|
50 |
-
translation_key=
|
51 |
entity_category=EntityCategory.DIAGNOSTIC,
|
52 |
),
|
53 |
PlugwiseBinarySensorEntityDescription(
|
54 |
-
key=
|
55 |
-
translation_key=
|
56 |
entity_category=EntityCategory.DIAGNOSTIC,
|
57 |
),
|
58 |
PlugwiseBinarySensorEntityDescription(
|
59 |
-
key=
|
60 |
-
translation_key=
|
61 |
entity_category=EntityCategory.DIAGNOSTIC,
|
62 |
),
|
63 |
PlugwiseBinarySensorEntityDescription(
|
64 |
-
key=
|
65 |
-
translation_key=
|
66 |
entity_category=EntityCategory.DIAGNOSTIC,
|
67 |
),
|
68 |
PlugwiseBinarySensorEntityDescription(
|
69 |
-
key=
|
70 |
-
translation_key=
|
71 |
entity_category=EntityCategory.DIAGNOSTIC,
|
72 |
),
|
73 |
PlugwiseBinarySensorEntityDescription(
|
74 |
-
key=
|
75 |
-
translation_key=
|
76 |
entity_category=EntityCategory.DIAGNOSTIC,
|
77 |
),
|
78 |
PlugwiseBinarySensorEntityDescription(
|
79 |
-
key=
|
80 |
-
translation_key=
|
81 |
entity_category=EntityCategory.DIAGNOSTIC,
|
82 |
),
|
83 |
)
|
@@ -88,7 +108,7 @@
|
|
88 |
entry: PlugwiseConfigEntry,
|
89 |
async_add_entities: AddEntitiesCallback,
|
90 |
) -> None:
|
91 |
-
"""Set up
|
92 |
coordinator = entry.runtime_data
|
93 |
|
94 |
@callback
|
@@ -97,24 +117,40 @@
|
|
97 |
if not coordinator.new_devices:
|
98 |
return
|
99 |
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
)
|
107 |
-
|
108 |
-
for description in BINARY_SENSORS
|
109 |
-
if description.key in binary_sensors
|
110 |
-
)
|
111 |
|
112 |
_add_entities()
|
113 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
114 |
|
115 |
|
116 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
117 |
-
"""
|
118 |
|
119 |
entity_description: PlugwiseBinarySensorEntityDescription
|
120 |
|
@@ -128,25 +164,45 @@
|
|
128 |
super().__init__(coordinator, device_id)
|
129 |
self.entity_description = description
|
130 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
131 |
|
132 |
@property
|
133 |
def is_on(self) -> bool:
|
134 |
"""Return true if the binary sensor is on."""
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
|
137 |
@property
|
138 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
139 |
"""Return entity specific state attributes."""
|
140 |
-
if self.entity_description.key !=
|
141 |
return None
|
142 |
|
143 |
-
|
144 |
-
|
145 |
-
|
|
|
|
|
|
|
146 |
for msg_type, msg in details.items():
|
147 |
msg_type = msg_type.lower()
|
148 |
if msg_type not in SEVERITIES:
|
149 |
-
msg_type = "other"
|
|
|
|
|
|
|
|
|
|
|
150 |
attrs[f"{msg_type}_msg"].append(msg)
|
151 |
|
|
|
|
|
|
|
|
|
152 |
return attrs
|
|
|
8 |
|
9 |
from plugwise.constants import BinarySensorType
|
10 |
|
11 |
+
from homeassistant.components import (
|
12 |
+
persistent_notification, # pw-beta Plugwise notifications
|
13 |
+
)
|
14 |
from homeassistant.components.binary_sensor import (
|
15 |
BinarySensorDeviceClass,
|
16 |
BinarySensorEntity,
|
|
|
21 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
22 |
|
23 |
from . import PlugwiseConfigEntry
|
24 |
+
from .const import (
|
25 |
+
BATTERY_STATE,
|
26 |
+
BINARY_SENSORS,
|
27 |
+
COMPRESSOR_STATE,
|
28 |
+
COOLING_ENABLED,
|
29 |
+
COOLING_STATE,
|
30 |
+
DHW_STATE,
|
31 |
+
DOMAIN,
|
32 |
+
FLAME_STATE,
|
33 |
+
HEATING_STATE,
|
34 |
+
LOGGER, # pw-beta
|
35 |
+
NOTIFICATIONS,
|
36 |
+
PLUGWISE_NOTIFICATION,
|
37 |
+
SECONDARY_BOILER_STATE,
|
38 |
+
SEVERITIES,
|
39 |
+
)
|
40 |
+
|
41 |
+
# Upstream
|
42 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
43 |
from .entity import PlugwiseEntity
|
44 |
|
|
|
|
|
45 |
# Coordinator is used to centralize the data updates
|
46 |
PARALLEL_UPDATES = 0
|
47 |
|
|
|
53 |
key: BinarySensorType
|
54 |
|
55 |
|
56 |
+
# Upstream PLUGWISE_BINARY_SENSORS
|
57 |
+
PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
|
58 |
PlugwiseBinarySensorEntityDescription(
|
59 |
+
key=BATTERY_STATE,
|
60 |
device_class=BinarySensorDeviceClass.BATTERY,
|
61 |
entity_category=EntityCategory.DIAGNOSTIC,
|
62 |
),
|
63 |
PlugwiseBinarySensorEntityDescription(
|
64 |
+
key=COMPRESSOR_STATE,
|
65 |
+
translation_key=COMPRESSOR_STATE,
|
66 |
entity_category=EntityCategory.DIAGNOSTIC,
|
67 |
),
|
68 |
PlugwiseBinarySensorEntityDescription(
|
69 |
+
key=COOLING_ENABLED,
|
70 |
+
translation_key=COOLING_ENABLED,
|
71 |
entity_category=EntityCategory.DIAGNOSTIC,
|
72 |
),
|
73 |
PlugwiseBinarySensorEntityDescription(
|
74 |
+
key=DHW_STATE,
|
75 |
+
translation_key=DHW_STATE,
|
76 |
entity_category=EntityCategory.DIAGNOSTIC,
|
77 |
),
|
78 |
PlugwiseBinarySensorEntityDescription(
|
79 |
+
key=FLAME_STATE,
|
80 |
+
translation_key=FLAME_STATE,
|
81 |
entity_category=EntityCategory.DIAGNOSTIC,
|
82 |
),
|
83 |
PlugwiseBinarySensorEntityDescription(
|
84 |
+
key=HEATING_STATE,
|
85 |
+
translation_key=HEATING_STATE,
|
86 |
entity_category=EntityCategory.DIAGNOSTIC,
|
87 |
),
|
88 |
PlugwiseBinarySensorEntityDescription(
|
89 |
+
key=COOLING_STATE,
|
90 |
+
translation_key=COOLING_STATE,
|
91 |
entity_category=EntityCategory.DIAGNOSTIC,
|
92 |
),
|
93 |
PlugwiseBinarySensorEntityDescription(
|
94 |
+
key=SECONDARY_BOILER_STATE,
|
95 |
+
translation_key=SECONDARY_BOILER_STATE,
|
96 |
entity_category=EntityCategory.DIAGNOSTIC,
|
97 |
),
|
98 |
PlugwiseBinarySensorEntityDescription(
|
99 |
+
key=PLUGWISE_NOTIFICATION,
|
100 |
+
translation_key=PLUGWISE_NOTIFICATION,
|
101 |
entity_category=EntityCategory.DIAGNOSTIC,
|
102 |
),
|
103 |
)
|
|
|
108 |
entry: PlugwiseConfigEntry,
|
109 |
async_add_entities: AddEntitiesCallback,
|
110 |
) -> None:
|
111 |
+
"""Set up Plugwise binary_sensors from a config entry."""
|
112 |
coordinator = entry.runtime_data
|
113 |
|
114 |
@callback
|
|
|
117 |
if not coordinator.new_devices:
|
118 |
return
|
119 |
|
120 |
+
# Upstream consts to HA
|
121 |
+
# async_add_entities(
|
122 |
+
# PlugwiseBinarySensorEntity(coordinator, device_id, description)
|
123 |
+
# for device_id in coordinator.new_devices
|
124 |
+
# if (
|
125 |
+
# binary_sensors := coordinator.data.devices[device_id].get(
|
126 |
+
# BINARY_SENSORS
|
127 |
+
# )
|
128 |
+
# )
|
129 |
+
# for description in PLUGWISE_BINARY_SENSORS
|
130 |
+
# if description.key in binary_sensors
|
131 |
+
# )
|
132 |
+
|
133 |
+
# pw-beta alternative for debugging
|
134 |
+
entities: list[PlugwiseBinarySensorEntity] = []
|
135 |
+
for device_id in coordinator.new_devices:
|
136 |
+
device = coordinator.data.devices[device_id]
|
137 |
+
if not (binary_sensors := device.get(BINARY_SENSORS)):
|
138 |
+
continue
|
139 |
+
for description in PLUGWISE_BINARY_SENSORS:
|
140 |
+
if description.key not in binary_sensors:
|
141 |
+
continue
|
142 |
+
entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
|
143 |
+
LOGGER.debug(
|
144 |
+
"Add %s %s binary sensor", device["name"], description.translation_key
|
145 |
)
|
146 |
+
async_add_entities(entities)
|
|
|
|
|
|
|
147 |
|
148 |
_add_entities()
|
149 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
150 |
|
151 |
|
152 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
153 |
+
"""Set up Plugwise binary_sensors from a config entry."""
|
154 |
|
155 |
entity_description: PlugwiseBinarySensorEntityDescription
|
156 |
|
|
|
164 |
super().__init__(coordinator, device_id)
|
165 |
self.entity_description = description
|
166 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
167 |
+
self._notification: dict[str, str] = {} # pw-beta
|
168 |
|
169 |
@property
|
170 |
def is_on(self) -> bool:
|
171 |
"""Return true if the binary sensor is on."""
|
172 |
+
# pw-beta: show Plugwise notifications as HA persistent notifications
|
173 |
+
if self._notification:
|
174 |
+
for notify_id, message in self._notification.items():
|
175 |
+
persistent_notification.async_create(
|
176 |
+
self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
|
177 |
+
)
|
178 |
+
|
179 |
+
return self.device[BINARY_SENSORS][self.entity_description.key]
|
180 |
|
181 |
@property
|
182 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
183 |
"""Return entity specific state attributes."""
|
184 |
+
if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
|
185 |
return None
|
186 |
|
187 |
+
# pw-beta adjustment with attrs is to only represent severities *with* content
|
188 |
+
# not all severities including those without content as empty lists
|
189 |
+
attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
|
190 |
+
self._notification = {} # pw-beta
|
191 |
+
if notify := self.coordinator.data.gateway[NOTIFICATIONS]:
|
192 |
+
for notify_id, details in notify.items(): # pw-beta uses notify_id
|
193 |
for msg_type, msg in details.items():
|
194 |
msg_type = msg_type.lower()
|
195 |
if msg_type not in SEVERITIES:
|
196 |
+
msg_type = "other" # pragma: no cover
|
197 |
+
|
198 |
+
if (
|
199 |
+
f"{msg_type}_msg" not in attrs
|
200 |
+
): # pw-beta Re-evaluate against Core
|
201 |
+
attrs[f"{msg_type}_msg"] = []
|
202 |
attrs[f"{msg_type}_msg"].append(msg)
|
203 |
|
204 |
+
self._notification[
|
205 |
+
notify_id
|
206 |
+
] = f"{msg_type.title()}: {msg}" # pw-beta
|
207 |
+
|
208 |
return attrs
|
@@ -8,7 +8,11 @@
|
|
8 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
9 |
|
10 |
from . import PlugwiseConfigEntry
|
11 |
-
from .const import
|
|
|
|
|
|
|
|
|
12 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
13 |
from .entity import PlugwiseEntity
|
14 |
from .util import plugwise_command
|
@@ -21,15 +25,22 @@
|
|
21 |
entry: PlugwiseConfigEntry,
|
22 |
async_add_entities: AddEntitiesCallback,
|
23 |
) -> None:
|
24 |
-
"""Set up
|
25 |
coordinator = entry.runtime_data
|
26 |
|
27 |
gateway = coordinator.data.gateway
|
28 |
-
async_add_entities(
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
|
35 |
class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
|
|
|
8 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
9 |
|
10 |
from . import PlugwiseConfigEntry
|
11 |
+
from .const import (
|
12 |
+
GATEWAY_ID,
|
13 |
+
LOGGER, # pw-betea
|
14 |
+
REBOOT,
|
15 |
+
)
|
16 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
17 |
from .entity import PlugwiseEntity
|
18 |
from .util import plugwise_command
|
|
|
25 |
entry: PlugwiseConfigEntry,
|
26 |
async_add_entities: AddEntitiesCallback,
|
27 |
) -> None:
|
28 |
+
"""Set up Plugwise buttons from a config entry."""
|
29 |
coordinator = entry.runtime_data
|
30 |
|
31 |
gateway = coordinator.data.gateway
|
32 |
+
# async_add_entities(
|
33 |
+
# PlugwiseButtonEntity(coordinator, device_id)
|
34 |
+
# for device_id in coordinator.data.devices
|
35 |
+
# if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
|
36 |
+
# )
|
37 |
+
# pw-beta alternative for debugging
|
38 |
+
entities: list[PlugwiseButtonEntity] = []
|
39 |
+
for device_id, device in coordinator.data.devices.items():
|
40 |
+
if device_id == gateway[GATEWAY_ID] and REBOOT in gateway:
|
41 |
+
entities.append(PlugwiseButtonEntity(coordinator, device_id))
|
42 |
+
LOGGER.debug("Add %s reboot button", device["name"])
|
43 |
+
async_add_entities(entities)
|
44 |
|
45 |
|
46 |
class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
|
@@ -8,18 +8,50 @@
|
|
8 |
ATTR_HVAC_MODE,
|
9 |
ATTR_TARGET_TEMP_HIGH,
|
10 |
ATTR_TARGET_TEMP_LOW,
|
|
|
|
|
11 |
ClimateEntity,
|
12 |
ClimateEntityFeature,
|
13 |
HVACAction,
|
14 |
HVACMode,
|
15 |
)
|
16 |
-
from homeassistant.const import
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
from homeassistant.core import HomeAssistant, callback
|
18 |
-
from homeassistant.exceptions import
|
19 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
20 |
|
21 |
from . import PlugwiseConfigEntry
|
22 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
24 |
from .entity import PlugwiseEntity
|
25 |
from .util import plugwise_command
|
@@ -32,28 +64,39 @@
|
|
32 |
entry: PlugwiseConfigEntry,
|
33 |
async_add_entities: AddEntitiesCallback,
|
34 |
) -> None:
|
35 |
-
"""Set up
|
36 |
coordinator = entry.runtime_data
|
|
|
|
|
|
|
37 |
|
38 |
@callback
|
39 |
def _add_entities() -> None:
|
40 |
-
"""Add Entities."""
|
41 |
if not coordinator.new_devices:
|
42 |
return
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
_add_entities()
|
59 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
@@ -62,33 +105,48 @@
|
|
62 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
63 |
"""Representation of a Plugwise thermostat."""
|
64 |
|
|
|
65 |
_attr_name = None
|
66 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
67 |
_attr_translation_key = DOMAIN
|
|
|
68 |
|
69 |
-
_previous_mode: str =
|
|
|
70 |
|
71 |
def __init__(
|
72 |
self,
|
73 |
coordinator: PlugwiseDataUpdateCoordinator,
|
74 |
device_id: str,
|
|
|
75 |
) -> None:
|
76 |
"""Set up the Plugwise API."""
|
77 |
super().__init__(coordinator, device_id)
|
78 |
-
self._attr_unique_id = f"{device_id}-climate"
|
79 |
|
80 |
self._devices = coordinator.data.devices
|
81 |
self._gateway = coordinator.data.gateway
|
82 |
-
gateway_id: str = self._gateway[
|
83 |
self._gateway_data = self._devices[gateway_id]
|
|
|
84 |
|
85 |
self._location = device_id
|
86 |
-
if (location := self.device.get(
|
87 |
self._location = location
|
88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
# Determine supported features
|
90 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
91 |
-
if
|
|
|
|
|
|
|
92 |
self._attr_supported_features = (
|
93 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
94 |
)
|
@@ -96,16 +154,9 @@
|
|
96 |
self._attr_supported_features |= (
|
97 |
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
98 |
)
|
99 |
-
if presets := self.device
|
100 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
101 |
-
|
102 |
-
|
103 |
-
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
104 |
-
self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
|
105 |
-
# Ensure we don't drop below 0.1
|
106 |
-
self._attr_target_temperature_step = max(
|
107 |
-
self.device["thermostat"]["resolution"], 0.1
|
108 |
-
)
|
109 |
|
110 |
def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
|
111 |
"""Return the previous action-mode when the regulation-mode is not heating or cooling.
|
@@ -114,17 +165,17 @@
|
|
114 |
"""
|
115 |
# When no cooling available, _previous_mode is always heating
|
116 |
if (
|
117 |
-
|
118 |
-
and
|
119 |
):
|
120 |
-
mode = self._gateway_data[
|
121 |
-
if mode in (
|
122 |
self._previous_mode = mode
|
123 |
|
124 |
@property
|
125 |
def current_temperature(self) -> float:
|
126 |
"""Return the current temperature."""
|
127 |
-
return self.device[
|
128 |
|
129 |
@property
|
130 |
def target_temperature(self) -> float:
|
@@ -133,7 +184,7 @@
|
|
133 |
Connected to the HVACMode combination of AUTO-HEAT.
|
134 |
"""
|
135 |
|
136 |
-
return self.device[
|
137 |
|
138 |
@property
|
139 |
def target_temperature_high(self) -> float:
|
@@ -141,7 +192,7 @@
|
|
141 |
|
142 |
Connected to the HVACMode combination of AUTO-HEAT_COOL.
|
143 |
"""
|
144 |
-
return self.device[
|
145 |
|
146 |
@property
|
147 |
def target_temperature_low(self) -> float:
|
@@ -149,32 +200,39 @@
|
|
149 |
|
150 |
Connected to the HVACMode combination AUTO-HEAT_COOL.
|
151 |
"""
|
152 |
-
return self.device[
|
153 |
|
154 |
@property
|
155 |
def hvac_mode(self) -> HVACMode:
|
156 |
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
157 |
if (
|
158 |
-
mode := self.device
|
159 |
-
) is None or mode not in self.hvac_modes:
|
160 |
-
return HVACMode.HEAT
|
|
|
|
|
|
|
|
|
161 |
return HVACMode(mode)
|
162 |
|
163 |
@property
|
164 |
def hvac_modes(self) -> list[HVACMode]:
|
165 |
"""Return a list of available HVACModes."""
|
166 |
hvac_modes: list[HVACMode] = []
|
167 |
-
if
|
|
|
|
|
|
|
168 |
hvac_modes.append(HVACMode.OFF)
|
169 |
|
170 |
-
if
|
171 |
hvac_modes.append(HVACMode.AUTO)
|
172 |
|
173 |
-
if self._gateway[
|
174 |
-
if
|
175 |
-
if self._gateway_data[
|
176 |
hvac_modes.append(HVACMode.COOL)
|
177 |
-
if self._gateway_data[
|
178 |
hvac_modes.append(HVACMode.HEAT)
|
179 |
else:
|
180 |
hvac_modes.append(HVACMode.HEAT_COOL)
|
@@ -184,11 +242,12 @@
|
|
184 |
return hvac_modes
|
185 |
|
186 |
@property
|
187 |
-
def hvac_action(self) -> HVACAction:
|
188 |
"""Return the current running hvac operation if supported."""
|
189 |
# Keep track of the previous action-mode
|
190 |
self._previous_action_mode(self.coordinator)
|
191 |
-
|
|
|
192 |
return HVACAction(action)
|
193 |
|
194 |
return HVACAction.IDLE
|
@@ -196,18 +255,20 @@
|
|
196 |
@property
|
197 |
def preset_mode(self) -> str | None:
|
198 |
"""Return the current preset mode."""
|
199 |
-
return self.device
|
200 |
|
201 |
@plugwise_command
|
202 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
203 |
"""Set new target temperature."""
|
204 |
data: dict[str, Any] = {}
|
205 |
if ATTR_TEMPERATURE in kwargs:
|
206 |
-
data[
|
207 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
208 |
-
data[
|
209 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
210 |
-
data[
|
|
|
|
|
211 |
|
212 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
213 |
await self.async_set_hvac_mode(mode)
|
@@ -218,28 +279,33 @@
|
|
218 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
219 |
"""Set the hvac mode."""
|
220 |
if hvac_mode not in self.hvac_modes:
|
221 |
-
|
222 |
-
raise ServiceValidationError(
|
223 |
-
translation_domain=DOMAIN,
|
224 |
-
translation_key="unsupported_hvac_mode_requested",
|
225 |
-
translation_placeholders={
|
226 |
-
"hvac_mode": hvac_mode,
|
227 |
-
"hvac_modes": hvac_modes,
|
228 |
-
},
|
229 |
-
)
|
230 |
|
231 |
if hvac_mode == self.hvac_mode:
|
232 |
return
|
233 |
|
234 |
-
if hvac_mode =
|
235 |
-
await self.coordinator.api.set_regulation_mode(hvac_mode)
|
236 |
-
else:
|
237 |
await self.coordinator.api.set_schedule_state(
|
238 |
self._location,
|
239 |
-
|
240 |
)
|
241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
242 |
await self.coordinator.api.set_regulation_mode(self._previous_mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
@plugwise_command
|
245 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
|
|
8 |
ATTR_HVAC_MODE,
|
9 |
ATTR_TARGET_TEMP_HIGH,
|
10 |
ATTR_TARGET_TEMP_LOW,
|
11 |
+
PRESET_AWAY, # pw-beta homekit emulation
|
12 |
+
PRESET_HOME, # pw-beta homekit emulation
|
13 |
ClimateEntity,
|
14 |
ClimateEntityFeature,
|
15 |
HVACAction,
|
16 |
HVACMode,
|
17 |
)
|
18 |
+
from homeassistant.const import (
|
19 |
+
ATTR_NAME,
|
20 |
+
ATTR_TEMPERATURE,
|
21 |
+
STATE_OFF,
|
22 |
+
STATE_ON,
|
23 |
+
UnitOfTemperature,
|
24 |
+
)
|
25 |
from homeassistant.core import HomeAssistant, callback
|
26 |
+
from homeassistant.exceptions import HomeAssistantError
|
27 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
28 |
|
29 |
from . import PlugwiseConfigEntry
|
30 |
+
from .const import (
|
31 |
+
ACTIVE_PRESET,
|
32 |
+
AVAILABLE_SCHEDULES,
|
33 |
+
CLIMATE_MODE,
|
34 |
+
CONF_HOMEKIT_EMULATION, # pw-beta homekit emulation
|
35 |
+
CONTROL_STATE,
|
36 |
+
COOLING_PRESENT,
|
37 |
+
DEV_CLASS,
|
38 |
+
DOMAIN,
|
39 |
+
GATEWAY_ID,
|
40 |
+
LOCATION,
|
41 |
+
LOGGER,
|
42 |
+
LOWER_BOUND,
|
43 |
+
MASTER_THERMOSTATS,
|
44 |
+
REGULATION_MODES,
|
45 |
+
RESOLUTION,
|
46 |
+
SELECT_REGULATION_MODE,
|
47 |
+
SENSORS,
|
48 |
+
SMILE_NAME,
|
49 |
+
TARGET_TEMP,
|
50 |
+
TARGET_TEMP_HIGH,
|
51 |
+
TARGET_TEMP_LOW,
|
52 |
+
THERMOSTAT,
|
53 |
+
UPPER_BOUND,
|
54 |
+
)
|
55 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
56 |
from .entity import PlugwiseEntity
|
57 |
from .util import plugwise_command
|
|
|
64 |
entry: PlugwiseConfigEntry,
|
65 |
async_add_entities: AddEntitiesCallback,
|
66 |
) -> None:
|
67 |
+
"""Set up Plugwise thermostats from a config entry."""
|
68 |
coordinator = entry.runtime_data
|
69 |
+
homekit_enabled: bool = entry.options.get(
|
70 |
+
CONF_HOMEKIT_EMULATION, False
|
71 |
+
) # pw-beta homekit emulation
|
72 |
|
73 |
@callback
|
74 |
def _add_entities() -> None:
|
75 |
+
"""Add Entities during init and runtime."""
|
76 |
if not coordinator.new_devices:
|
77 |
return
|
78 |
|
79 |
+
entities: list[PlugwiseClimateEntity] = []
|
80 |
+
gateway_name = coordinator.data.gateway[SMILE_NAME]
|
81 |
+
for device_id in coordinator.new_devices:
|
82 |
+
device = coordinator.data.devices[device_id]
|
83 |
+
if gateway_name == "Adam":
|
84 |
+
if device[DEV_CLASS] == "climate":
|
85 |
+
entities.append(
|
86 |
+
PlugwiseClimateEntity(
|
87 |
+
coordinator, device_id, homekit_enabled
|
88 |
+
) # pw-beta homekit emulation
|
89 |
+
)
|
90 |
+
LOGGER.debug("Add climate %s", device[ATTR_NAME])
|
91 |
+
elif device[DEV_CLASS] in MASTER_THERMOSTATS:
|
92 |
+
entities.append(
|
93 |
+
PlugwiseClimateEntity(
|
94 |
+
coordinator, device_id, homekit_enabled
|
95 |
+
) # pw-beta homekit emulation
|
96 |
+
)
|
97 |
+
LOGGER.debug("Add climate %s", device[ATTR_NAME])
|
98 |
+
|
99 |
+
async_add_entities(entities)
|
100 |
|
101 |
_add_entities()
|
102 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
105 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
106 |
"""Representation of a Plugwise thermostat."""
|
107 |
|
108 |
+
_attr_has_entity_name = True
|
109 |
_attr_name = None
|
110 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
111 |
_attr_translation_key = DOMAIN
|
112 |
+
_enable_turn_on_off_backwards_compatibility = False
|
113 |
|
114 |
+
_previous_mode: str = HVACAction.HEATING # Upstream
|
115 |
+
_homekit_mode: str | None = None # pw-beta homekit emulation + intentional unsort
|
116 |
|
117 |
def __init__(
|
118 |
self,
|
119 |
coordinator: PlugwiseDataUpdateCoordinator,
|
120 |
device_id: str,
|
121 |
+
homekit_enabled: bool, # pw-beta homekit emulation
|
122 |
) -> None:
|
123 |
"""Set up the Plugwise API."""
|
124 |
super().__init__(coordinator, device_id)
|
|
|
125 |
|
126 |
self._devices = coordinator.data.devices
|
127 |
self._gateway = coordinator.data.gateway
|
128 |
+
gateway_id: str = self._gateway[GATEWAY_ID]
|
129 |
self._gateway_data = self._devices[gateway_id]
|
130 |
+
self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
|
131 |
|
132 |
self._location = device_id
|
133 |
+
if (location := self.device.get(LOCATION)) is not None:
|
134 |
self._location = location
|
135 |
|
136 |
+
self._attr_max_temp = min(self.device[THERMOSTAT][UPPER_BOUND], 35.0)
|
137 |
+
self._attr_min_temp = self.device[THERMOSTAT][LOWER_BOUND]
|
138 |
+
# Ensure we don't drop below 0.1
|
139 |
+
self._attr_target_temperature_step = max(
|
140 |
+
self.device[THERMOSTAT][RESOLUTION], 0.1
|
141 |
+
)
|
142 |
+
self._attr_unique_id = f"{device_id}-climate"
|
143 |
+
|
144 |
# Determine supported features
|
145 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
146 |
+
if (
|
147 |
+
self._gateway[COOLING_PRESENT]
|
148 |
+
and self._gateway[SMILE_NAME] != "Adam"
|
149 |
+
):
|
150 |
self._attr_supported_features = (
|
151 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
152 |
)
|
|
|
154 |
self._attr_supported_features |= (
|
155 |
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
156 |
)
|
157 |
+
if presets := self.device["preset_modes"]: # can be NONE
|
158 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
159 |
+
self._attr_preset_modes = presets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
|
161 |
def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
|
162 |
"""Return the previous action-mode when the regulation-mode is not heating or cooling.
|
|
|
165 |
"""
|
166 |
# When no cooling available, _previous_mode is always heating
|
167 |
if (
|
168 |
+
REGULATION_MODES in self._gateway_data
|
169 |
+
and HVACAction.COOLING in self._gateway_data[REGULATION_MODES]
|
170 |
):
|
171 |
+
mode = self._gateway_data[SELECT_REGULATION_MODE]
|
172 |
+
if mode in (HVACAction.COOLING, HVACAction.HEATING):
|
173 |
self._previous_mode = mode
|
174 |
|
175 |
@property
|
176 |
def current_temperature(self) -> float:
|
177 |
"""Return the current temperature."""
|
178 |
+
return self.device[SENSORS][ATTR_TEMPERATURE]
|
179 |
|
180 |
@property
|
181 |
def target_temperature(self) -> float:
|
|
|
184 |
Connected to the HVACMode combination of AUTO-HEAT.
|
185 |
"""
|
186 |
|
187 |
+
return self.device[THERMOSTAT][TARGET_TEMP]
|
188 |
|
189 |
@property
|
190 |
def target_temperature_high(self) -> float:
|
|
|
192 |
|
193 |
Connected to the HVACMode combination of AUTO-HEAT_COOL.
|
194 |
"""
|
195 |
+
return self.device[THERMOSTAT][TARGET_TEMP_HIGH]
|
196 |
|
197 |
@property
|
198 |
def target_temperature_low(self) -> float:
|
|
|
200 |
|
201 |
Connected to the HVACMode combination AUTO-HEAT_COOL.
|
202 |
"""
|
203 |
+
return self.device[THERMOSTAT][TARGET_TEMP_LOW]
|
204 |
|
205 |
@property
|
206 |
def hvac_mode(self) -> HVACMode:
|
207 |
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
208 |
if (
|
209 |
+
mode := self.device[CLIMATE_MODE]
|
210 |
+
) is None or mode not in self.hvac_modes: # pw-beta add to Core
|
211 |
+
return HVACMode.HEAT # pragma: no cover
|
212 |
+
# pw-beta homekit emulation
|
213 |
+
if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
|
214 |
+
mode = HVACMode.OFF # pragma: no cover
|
215 |
+
|
216 |
return HVACMode(mode)
|
217 |
|
218 |
@property
|
219 |
def hvac_modes(self) -> list[HVACMode]:
|
220 |
"""Return a list of available HVACModes."""
|
221 |
hvac_modes: list[HVACMode] = []
|
222 |
+
if (
|
223 |
+
self._homekit_enabled # pw-beta homekit emulation
|
224 |
+
or REGULATION_MODES in self._gateway_data
|
225 |
+
):
|
226 |
hvac_modes.append(HVACMode.OFF)
|
227 |
|
228 |
+
if AVAILABLE_SCHEDULES in self.device:
|
229 |
hvac_modes.append(HVACMode.AUTO)
|
230 |
|
231 |
+
if self._gateway[COOLING_PRESENT]:
|
232 |
+
if REGULATION_MODES in self._gateway_data:
|
233 |
+
if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.COOLING:
|
234 |
hvac_modes.append(HVACMode.COOL)
|
235 |
+
if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.HEATING:
|
236 |
hvac_modes.append(HVACMode.HEAT)
|
237 |
else:
|
238 |
hvac_modes.append(HVACMode.HEAT_COOL)
|
|
|
242 |
return hvac_modes
|
243 |
|
244 |
@property
|
245 |
+
def hvac_action(self) -> HVACAction: # pw-beta add to Core
|
246 |
"""Return the current running hvac operation if supported."""
|
247 |
# Keep track of the previous action-mode
|
248 |
self._previous_action_mode(self.coordinator)
|
249 |
+
|
250 |
+
if (action := self.device.get(CONTROL_STATE)) is not None:
|
251 |
return HVACAction(action)
|
252 |
|
253 |
return HVACAction.IDLE
|
|
|
255 |
@property
|
256 |
def preset_mode(self) -> str | None:
|
257 |
"""Return the current preset mode."""
|
258 |
+
return self.device[ACTIVE_PRESET]
|
259 |
|
260 |
@plugwise_command
|
261 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
262 |
"""Set new target temperature."""
|
263 |
data: dict[str, Any] = {}
|
264 |
if ATTR_TEMPERATURE in kwargs:
|
265 |
+
data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
|
266 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
267 |
+
data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
268 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
269 |
+
data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
270 |
+
|
271 |
+
# Upstream removed input-valid check
|
272 |
|
273 |
if mode := kwargs.get(ATTR_HVAC_MODE):
|
274 |
await self.async_set_hvac_mode(mode)
|
|
|
279 |
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
280 |
"""Set the hvac mode."""
|
281 |
if hvac_mode not in self.hvac_modes:
|
282 |
+
raise HomeAssistantError("Unsupported hvac_mode")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
|
284 |
if hvac_mode == self.hvac_mode:
|
285 |
return
|
286 |
|
287 |
+
if hvac_mode != HVACMode.OFF:
|
|
|
|
|
288 |
await self.coordinator.api.set_schedule_state(
|
289 |
self._location,
|
290 |
+
STATE_ON if hvac_mode == HVACMode.AUTO else STATE_OFF,
|
291 |
)
|
292 |
+
|
293 |
+
if (
|
294 |
+
not self._homekit_enabled
|
295 |
+
): # pw-beta: feature request - mimic HomeKit behavior
|
296 |
+
if hvac_mode == HVACMode.OFF:
|
297 |
+
await self.coordinator.api.set_regulation_mode(hvac_mode)
|
298 |
+
elif self.hvac_mode == HVACMode.OFF:
|
299 |
await self.coordinator.api.set_regulation_mode(self._previous_mode)
|
300 |
+
else:
|
301 |
+
self._homekit_mode = hvac_mode # pragma: no cover
|
302 |
+
if self._homekit_mode == HVACMode.OFF: # pragma: no cover
|
303 |
+
await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
|
304 |
+
if (
|
305 |
+
self._homekit_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]
|
306 |
+
and self.device[ACTIVE_PRESET] == PRESET_AWAY
|
307 |
+
): # pragma: no cover
|
308 |
+
await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
|
309 |
|
310 |
@plugwise_command
|
311 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
@@ -2,6 +2,7 @@
|
|
2 |
|
3 |
from __future__ import annotations
|
4 |
|
|
|
5 |
import logging
|
6 |
from typing import Any, Self
|
7 |
|
@@ -16,7 +17,16 @@
|
|
16 |
)
|
17 |
import voluptuous as vol
|
18 |
|
19 |
-
from homeassistant.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
from homeassistant.const import (
|
21 |
ATTR_CONFIGURATION_URL,
|
22 |
CONF_BASE,
|
@@ -24,26 +34,46 @@
|
|
24 |
CONF_NAME,
|
25 |
CONF_PASSWORD,
|
26 |
CONF_PORT,
|
|
|
27 |
CONF_USERNAME,
|
28 |
)
|
29 |
-
|
|
|
|
|
|
|
30 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
31 |
-
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
32 |
|
33 |
from .const import (
|
|
|
|
|
|
|
34 |
DEFAULT_PORT,
|
|
|
35 |
DEFAULT_USERNAME,
|
36 |
DOMAIN,
|
37 |
FLOW_SMILE,
|
38 |
FLOW_STRETCH,
|
|
|
39 |
SMILE,
|
|
|
|
|
40 |
STRETCH,
|
41 |
STRETCH_USERNAME,
|
|
|
|
|
|
|
42 |
ZEROCONF_MAP,
|
43 |
)
|
44 |
|
|
|
|
|
|
|
|
|
|
|
45 |
_LOGGER = logging.getLogger(__name__)
|
46 |
|
|
|
|
|
47 |
SMILE_RECONF_SCHEMA = vol.Schema(
|
48 |
{
|
49 |
vol.Required(CONF_HOST): str,
|
@@ -51,14 +81,13 @@
|
|
51 |
)
|
52 |
|
53 |
|
54 |
-
def smile_user_schema(
|
55 |
"""Generate base schema for gateways."""
|
56 |
-
|
57 |
-
|
58 |
-
if not discovery_info:
|
59 |
-
schema = schema.extend(
|
60 |
{
|
61 |
vol.Required(CONF_HOST): str,
|
|
|
62 |
# Port under investigation for removal (hence not added in #132878)
|
63 |
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
64 |
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
@@ -67,7 +96,19 @@
|
|
67 |
}
|
68 |
)
|
69 |
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
|
73 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
@@ -105,7 +146,7 @@
|
|
105 |
errors[CONF_BASE] = "response_error"
|
106 |
except UnsupportedDeviceError:
|
107 |
errors[CONF_BASE] = "unsupported"
|
108 |
-
except Exception:
|
109 |
_LOGGER.exception(
|
110 |
"Unknown exception while verifying connection with your Plugwise Smile"
|
111 |
)
|
@@ -117,6 +158,7 @@
|
|
117 |
"""Handle a config flow for Plugwise Smile."""
|
118 |
|
119 |
VERSION = 1
|
|
|
120 |
|
121 |
discovery_info: ZeroconfServiceInfo | None = None
|
122 |
product: str = "Unknown Smile"
|
@@ -128,17 +170,21 @@
|
|
128 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
129 |
self.discovery_info = discovery_info
|
130 |
_properties = discovery_info.properties
|
131 |
-
|
|
|
132 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
|
|
|
|
|
|
133 |
if config_entry := await self.async_set_unique_id(unique_id):
|
134 |
try:
|
135 |
await validate_input(
|
136 |
self.hass,
|
137 |
{
|
138 |
CONF_HOST: discovery_info.host,
|
|
|
139 |
CONF_PORT: discovery_info.port,
|
140 |
CONF_USERNAME: config_entry.data[CONF_USERNAME],
|
141 |
-
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
142 |
},
|
143 |
)
|
144 |
except Exception: # noqa: BLE001
|
@@ -151,16 +197,10 @@
|
|
151 |
}
|
152 |
)
|
153 |
|
154 |
-
if DEFAULT_USERNAME not in unique_id:
|
155 |
-
self._username = STRETCH_USERNAME
|
156 |
-
self.product = _product = _properties.get("product", "Unknown Smile")
|
157 |
-
_version = _properties.get("version", "n/a")
|
158 |
-
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
159 |
-
|
160 |
# This is an Anna, but we already have config entries.
|
161 |
# Assuming that the user has already configured Adam, aborting discovery.
|
162 |
-
if self._async_current_entries() and _product ==
|
163 |
-
return self.async_abort(reason=
|
164 |
|
165 |
# If we have discovered an Adam or Anna, both might be on the network.
|
166 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
@@ -168,12 +208,13 @@
|
|
168 |
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
169 |
return self.async_abort(reason="anna_with_adam")
|
170 |
|
|
|
171 |
self.context.update(
|
172 |
{
|
173 |
-
|
174 |
ATTR_CONFIGURATION_URL: (
|
175 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
176 |
-
)
|
177 |
}
|
178 |
)
|
179 |
return await self.async_step_user()
|
@@ -181,15 +222,16 @@
|
|
181 |
def is_matching(self, other_flow: Self) -> bool:
|
182 |
"""Return True if other_flow is matching this flow."""
|
183 |
# This is an Anna, and there is already an Adam flow in progress
|
184 |
-
if self.product ==
|
185 |
return True
|
186 |
|
187 |
# This is an Adam, and there is already an Anna flow in progress
|
188 |
-
if self.product ==
|
189 |
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
190 |
|
191 |
return False
|
192 |
|
|
|
193 |
async def async_step_user(
|
194 |
self, user_input: dict[str, Any] | None = None
|
195 |
) -> ConfigFlowResult:
|
@@ -205,18 +247,19 @@
|
|
205 |
api, errors = await verify_connection(self.hass, user_input)
|
206 |
if api:
|
207 |
await self.async_set_unique_id(
|
208 |
-
api.smile_hostname or api.gateway_id,
|
209 |
-
raise_on_progress=False,
|
210 |
)
|
211 |
self._abort_if_unique_id_configured()
|
212 |
return self.async_create_entry(title=api.smile_name, data=user_input)
|
213 |
|
|
|
214 |
return self.async_show_form(
|
215 |
step_id=SOURCE_USER,
|
216 |
-
data_schema=smile_user_schema(
|
217 |
errors=errors,
|
218 |
)
|
219 |
|
|
|
220 |
async def async_step_reconfigure(
|
221 |
self, user_input: dict[str, Any] | None = None
|
222 |
) -> ConfigFlowResult:
|
@@ -226,7 +269,7 @@
|
|
226 |
reconfigure_entry = self._get_reconfigure_entry()
|
227 |
|
228 |
if user_input:
|
229 |
-
#
|
230 |
full_input = {
|
231 |
CONF_HOST: user_input.get(CONF_HOST),
|
232 |
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
|
@@ -237,8 +280,7 @@
|
|
237 |
api, errors = await verify_connection(self.hass, full_input)
|
238 |
if api:
|
239 |
await self.async_set_unique_id(
|
240 |
-
api.smile_hostname or api.gateway_id,
|
241 |
-
raise_on_progress=False,
|
242 |
)
|
243 |
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
|
244 |
return self.async_update_reload_and_abort(
|
@@ -255,3 +297,69 @@
|
|
255 |
description_placeholders={"title": reconfigure_entry.title},
|
256 |
errors=errors,
|
257 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
from __future__ import annotations
|
4 |
|
5 |
+
from copy import deepcopy
|
6 |
import logging
|
7 |
from typing import Any, Self
|
8 |
|
|
|
17 |
)
|
18 |
import voluptuous as vol
|
19 |
|
20 |
+
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
21 |
+
from homeassistant.config_entries import (
|
22 |
+
SOURCE_USER,
|
23 |
+
ConfigEntry,
|
24 |
+
ConfigFlow,
|
25 |
+
ConfigFlowResult,
|
26 |
+
OptionsFlow,
|
27 |
+
)
|
28 |
+
|
29 |
+
# Upstream
|
30 |
from homeassistant.const import (
|
31 |
ATTR_CONFIGURATION_URL,
|
32 |
CONF_BASE,
|
|
|
34 |
CONF_NAME,
|
35 |
CONF_PASSWORD,
|
36 |
CONF_PORT,
|
37 |
+
CONF_SCAN_INTERVAL,
|
38 |
CONF_USERNAME,
|
39 |
)
|
40 |
+
|
41 |
+
# Upstream
|
42 |
+
from homeassistant.core import HomeAssistant, callback
|
43 |
+
from homeassistant.helpers import config_validation as cv
|
44 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
45 |
|
46 |
from .const import (
|
47 |
+
ANNA_WITH_ADAM,
|
48 |
+
CONF_HOMEKIT_EMULATION, # pw-beta option
|
49 |
+
CONF_REFRESH_INTERVAL, # pw-beta option
|
50 |
DEFAULT_PORT,
|
51 |
+
DEFAULT_SCAN_INTERVAL, # pw-beta option
|
52 |
DEFAULT_USERNAME,
|
53 |
DOMAIN,
|
54 |
FLOW_SMILE,
|
55 |
FLOW_STRETCH,
|
56 |
+
INIT,
|
57 |
SMILE,
|
58 |
+
SMILE_OPEN_THERM,
|
59 |
+
SMILE_THERMO,
|
60 |
STRETCH,
|
61 |
STRETCH_USERNAME,
|
62 |
+
THERMOSTAT,
|
63 |
+
TITLE_PLACEHOLDERS,
|
64 |
+
VERSION,
|
65 |
ZEROCONF_MAP,
|
66 |
)
|
67 |
|
68 |
+
# Upstream
|
69 |
+
from .coordinator import PlugwiseDataUpdateCoordinator
|
70 |
+
|
71 |
+
type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
|
72 |
+
|
73 |
_LOGGER = logging.getLogger(__name__)
|
74 |
|
75 |
+
# Upstream basically the whole file (excluding the pw-beta options)
|
76 |
+
|
77 |
SMILE_RECONF_SCHEMA = vol.Schema(
|
78 |
{
|
79 |
vol.Required(CONF_HOST): str,
|
|
|
81 |
)
|
82 |
|
83 |
|
84 |
+
def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> vol.Schema:
|
85 |
"""Generate base schema for gateways."""
|
86 |
+
if not cf_input: # no discovery- or user-input available
|
87 |
+
return vol.Schema(
|
|
|
|
|
88 |
{
|
89 |
vol.Required(CONF_HOST): str,
|
90 |
+
vol.Required(CONF_PASSWORD): str,
|
91 |
# Port under investigation for removal (hence not added in #132878)
|
92 |
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
93 |
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
|
|
96 |
}
|
97 |
)
|
98 |
|
99 |
+
if isinstance(cf_input, ZeroconfServiceInfo):
|
100 |
+
return vol.Schema({vol.Required(CONF_PASSWORD): str})
|
101 |
+
|
102 |
+
return vol.Schema(
|
103 |
+
{
|
104 |
+
vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
|
105 |
+
vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
|
106 |
+
vol.Optional(CONF_PORT, default=cf_input[CONF_PORT]): int,
|
107 |
+
vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
|
108 |
+
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
109 |
+
),
|
110 |
+
}
|
111 |
+
)
|
112 |
|
113 |
|
114 |
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
|
|
146 |
errors[CONF_BASE] = "response_error"
|
147 |
except UnsupportedDeviceError:
|
148 |
errors[CONF_BASE] = "unsupported"
|
149 |
+
except Exception: # noqa: BLE001
|
150 |
_LOGGER.exception(
|
151 |
"Unknown exception while verifying connection with your Plugwise Smile"
|
152 |
)
|
|
|
158 |
"""Handle a config flow for Plugwise Smile."""
|
159 |
|
160 |
VERSION = 1
|
161 |
+
MINOR_VERSION = 1
|
162 |
|
163 |
discovery_info: ZeroconfServiceInfo | None = None
|
164 |
product: str = "Unknown Smile"
|
|
|
170 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
171 |
self.discovery_info = discovery_info
|
172 |
_properties = discovery_info.properties
|
173 |
+
_version = _properties.get(VERSION, "n/a")
|
174 |
+
self.product = _product = _properties.get("product", "Unknown Smile")
|
175 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
176 |
+
if DEFAULT_USERNAME not in unique_id:
|
177 |
+
self._username = STRETCH_USERNAME
|
178 |
+
|
179 |
if config_entry := await self.async_set_unique_id(unique_id):
|
180 |
try:
|
181 |
await validate_input(
|
182 |
self.hass,
|
183 |
{
|
184 |
CONF_HOST: discovery_info.host,
|
185 |
+
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
186 |
CONF_PORT: discovery_info.port,
|
187 |
CONF_USERNAME: config_entry.data[CONF_USERNAME],
|
|
|
188 |
},
|
189 |
)
|
190 |
except Exception: # noqa: BLE001
|
|
|
197 |
}
|
198 |
)
|
199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
# This is an Anna, but we already have config entries.
|
201 |
# Assuming that the user has already configured Adam, aborting discovery.
|
202 |
+
if self._async_current_entries() and _product == SMILE_THERMO:
|
203 |
+
return self.async_abort(reason=ANNA_WITH_ADAM)
|
204 |
|
205 |
# If we have discovered an Adam or Anna, both might be on the network.
|
206 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
|
|
208 |
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
209 |
return self.async_abort(reason="anna_with_adam")
|
210 |
|
211 |
+
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
212 |
self.context.update(
|
213 |
{
|
214 |
+
TITLE_PLACEHOLDERS: {CONF_NAME: _name},
|
215 |
ATTR_CONFIGURATION_URL: (
|
216 |
f"http://{discovery_info.host}:{discovery_info.port}"
|
217 |
+
)
|
218 |
}
|
219 |
)
|
220 |
return await self.async_step_user()
|
|
|
222 |
def is_matching(self, other_flow: Self) -> bool:
|
223 |
"""Return True if other_flow is matching this flow."""
|
224 |
# This is an Anna, and there is already an Adam flow in progress
|
225 |
+
if self.product == SMILE_THERMO and other_flow.product == SMILE_OPEN_THERM:
|
226 |
return True
|
227 |
|
228 |
# This is an Adam, and there is already an Anna flow in progress
|
229 |
+
if self.product == SMILE_OPEN_THERM and other_flow.product == SMILE_THERMO:
|
230 |
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
231 |
|
232 |
return False
|
233 |
|
234 |
+
|
235 |
async def async_step_user(
|
236 |
self, user_input: dict[str, Any] | None = None
|
237 |
) -> ConfigFlowResult:
|
|
|
247 |
api, errors = await verify_connection(self.hass, user_input)
|
248 |
if api:
|
249 |
await self.async_set_unique_id(
|
250 |
+
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
|
|
251 |
)
|
252 |
self._abort_if_unique_id_configured()
|
253 |
return self.async_create_entry(title=api.smile_name, data=user_input)
|
254 |
|
255 |
+
configure_input = self.discovery_info or user_input
|
256 |
return self.async_show_form(
|
257 |
step_id=SOURCE_USER,
|
258 |
+
data_schema=smile_user_schema(configure_input),
|
259 |
errors=errors,
|
260 |
)
|
261 |
|
262 |
+
|
263 |
async def async_step_reconfigure(
|
264 |
self, user_input: dict[str, Any] | None = None
|
265 |
) -> ConfigFlowResult:
|
|
|
269 |
reconfigure_entry = self._get_reconfigure_entry()
|
270 |
|
271 |
if user_input:
|
272 |
+
# Redefine ingest existing username and password
|
273 |
full_input = {
|
274 |
CONF_HOST: user_input.get(CONF_HOST),
|
275 |
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
|
|
|
280 |
api, errors = await verify_connection(self.hass, full_input)
|
281 |
if api:
|
282 |
await self.async_set_unique_id(
|
283 |
+
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
|
|
284 |
)
|
285 |
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
|
286 |
return self.async_update_reload_and_abort(
|
|
|
297 |
description_placeholders={"title": reconfigure_entry.title},
|
298 |
errors=errors,
|
299 |
)
|
300 |
+
|
301 |
+
|
302 |
+
@staticmethod
|
303 |
+
@callback
|
304 |
+
def async_get_options_flow(
|
305 |
+
config_entry: PlugwiseConfigEntry,
|
306 |
+
) -> PlugwiseOptionsFlowHandler: # pw-beta options
|
307 |
+
"""Get the options flow for this handler."""
|
308 |
+
return PlugwiseOptionsFlowHandler(config_entry)
|
309 |
+
|
310 |
+
|
311 |
+
# pw-beta - change the scan-interval via CONFIGURE
|
312 |
+
# pw-beta - add homekit emulation via CONFIGURE
|
313 |
+
# pw-beta - change the frontend refresh interval via CONFIGURE
|
314 |
+
class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
|
315 |
+
"""Plugwise option flow."""
|
316 |
+
|
317 |
+
def __init__(self, config_entry: ConfigEntry) -> None:
|
318 |
+
"""Initialize options flow."""
|
319 |
+
self.options = deepcopy(dict(config_entry.options))
|
320 |
+
|
321 |
+
def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
|
322 |
+
interval = DEFAULT_SCAN_INTERVAL[coordinator.api.smile_type] # pw-beta options
|
323 |
+
schema = {
|
324 |
+
vol.Optional(
|
325 |
+
CONF_SCAN_INTERVAL,
|
326 |
+
default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
|
327 |
+
): vol.All(cv.positive_int, vol.Clamp(min=10)),
|
328 |
+
} # pw-beta
|
329 |
+
|
330 |
+
if coordinator.api.smile_type == THERMOSTAT:
|
331 |
+
schema.update({
|
332 |
+
vol.Optional(
|
333 |
+
CONF_HOMEKIT_EMULATION,
|
334 |
+
default=self.options.get(CONF_HOMEKIT_EMULATION, False),
|
335 |
+
): vol.All(cv.boolean),
|
336 |
+
vol.Optional(
|
337 |
+
CONF_REFRESH_INTERVAL,
|
338 |
+
default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
|
339 |
+
): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
|
340 |
+
}) # pw-beta
|
341 |
+
|
342 |
+
return vol.Schema(schema)
|
343 |
+
|
344 |
+
async def async_step_none(
|
345 |
+
self, user_input: dict[str, Any] | None = None
|
346 |
+
) -> ConfigFlowResult: # pragma: no cover
|
347 |
+
"""No options available."""
|
348 |
+
if user_input is not None:
|
349 |
+
# Apparently not possible to abort an options flow at the moment
|
350 |
+
return self.async_create_entry(title="", data=self.options)
|
351 |
+
return self.async_show_form(step_id="none")
|
352 |
+
|
353 |
+
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
354 |
+
"""Manage the Plugwise options."""
|
355 |
+
if not self.config_entry.data.get(CONF_HOST):
|
356 |
+
return await self.async_step_none(user_input) # pragma: no cover
|
357 |
+
|
358 |
+
if user_input is not None:
|
359 |
+
return self.async_create_entry(title="", data=user_input)
|
360 |
+
|
361 |
+
coordinator = self.config_entry.runtime_data
|
362 |
+
return self.async_show_form(
|
363 |
+
step_id=INIT,
|
364 |
+
data_schema=self._create_options_schema(coordinator)
|
365 |
+
)
|
@@ -1,30 +1,167 @@
|
|
1 |
"""Constants for Plugwise component."""
|
2 |
|
3 |
-
from __future__ import annotations
|
4 |
-
|
5 |
from datetime import timedelta
|
6 |
import logging
|
7 |
from typing import Final, Literal
|
8 |
|
9 |
from homeassistant.const import Platform
|
10 |
|
|
|
|
|
11 |
DOMAIN: Final = "plugwise"
|
12 |
|
13 |
LOGGER = logging.getLogger(__package__)
|
14 |
|
15 |
API: Final = "api"
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
19 |
GATEWAY: Final = "gateway"
|
20 |
-
GATEWAY_ID: Final = "gateway_id"
|
21 |
LOCATION: Final = "location"
|
22 |
-
|
23 |
REBOOT: Final = "reboot"
|
24 |
SMILE: Final = "smile"
|
25 |
STRETCH: Final = "stretch"
|
26 |
STRETCH_USERNAME: Final = "stretch"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
PLATFORMS: Final[list[str]] = [
|
29 |
Platform.BINARY_SENSOR,
|
30 |
Platform.BUTTON,
|
@@ -34,6 +171,18 @@
|
|
34 |
Platform.SENSOR,
|
35 |
Platform.SWITCH,
|
36 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
38 |
"smile": "Smile P1",
|
39 |
"smile_thermo": "Smile Anna",
|
@@ -59,21 +208,3 @@
|
|
59 |
"regulation_modes",
|
60 |
"available_schedules",
|
61 |
]
|
62 |
-
|
63 |
-
# Default directives
|
64 |
-
DEFAULT_MAX_TEMP: Final = 30
|
65 |
-
DEFAULT_MIN_TEMP: Final = 4
|
66 |
-
DEFAULT_PORT: Final = 80
|
67 |
-
DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
|
68 |
-
"power": timedelta(seconds=10),
|
69 |
-
"stretch": timedelta(seconds=60),
|
70 |
-
"thermostat": timedelta(seconds=60),
|
71 |
-
}
|
72 |
-
DEFAULT_USERNAME: Final = "smile"
|
73 |
-
|
74 |
-
MASTER_THERMOSTATS: Final[list[str]] = [
|
75 |
-
"thermostat",
|
76 |
-
"thermostatic_radiator_valve",
|
77 |
-
"zone_thermometer",
|
78 |
-
"zone_thermostat",
|
79 |
-
]
|
|
|
1 |
"""Constants for Plugwise component."""
|
2 |
|
|
|
|
|
3 |
from datetime import timedelta
|
4 |
import logging
|
5 |
from typing import Final, Literal
|
6 |
|
7 |
from homeassistant.const import Platform
|
8 |
|
9 |
+
# Upstream basically the whole file excluding pw-beta options
|
10 |
+
|
11 |
DOMAIN: Final = "plugwise"
|
12 |
|
13 |
LOGGER = logging.getLogger(__package__)
|
14 |
|
15 |
API: Final = "api"
|
16 |
+
COORDINATOR: Final = "coordinator"
|
17 |
+
CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
|
18 |
+
CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
|
19 |
+
CONF_MANUAL_PATH: Final = "Enter Manually"
|
20 |
GATEWAY: Final = "gateway"
|
|
|
21 |
LOCATION: Final = "location"
|
22 |
+
MAC_ADDRESS: Final = "mac_address"
|
23 |
REBOOT: Final = "reboot"
|
24 |
SMILE: Final = "smile"
|
25 |
STRETCH: Final = "stretch"
|
26 |
STRETCH_USERNAME: Final = "stretch"
|
27 |
+
UNIQUE_IDS: Final = "unique_ids"
|
28 |
+
ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
|
29 |
+
|
30 |
+
# Binary Sensor constants
|
31 |
+
BINARY_SENSORS: Final = "binary_sensors"
|
32 |
+
BATTERY_STATE: Final = "low_battery"
|
33 |
+
COMPRESSOR_STATE: Final = "compressor_state"
|
34 |
+
COOLING_ENABLED: Final = "cooling_enabled"
|
35 |
+
COOLING_STATE: Final = "cooling_state"
|
36 |
+
DHW_STATE: Final = "dhw_state"
|
37 |
+
FLAME_STATE: Final = "flame_state"
|
38 |
+
HEATING_STATE: Final = "heating_state"
|
39 |
+
NOTIFICATIONS: Final ="notifications"
|
40 |
+
PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
|
41 |
+
SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
|
42 |
+
|
43 |
+
# Climate constants
|
44 |
+
ACTIVE_PRESET: Final = "active_preset"
|
45 |
+
CLIMATE_MODE: Final = "climate_mode"
|
46 |
+
CONTROL_STATE: Final = "control_state"
|
47 |
+
COOLING_PRESENT: Final ="cooling_present"
|
48 |
+
DEV_CLASS: Final = "dev_class"
|
49 |
+
NONE : Final = "None"
|
50 |
+
TARGET_TEMP: Final = "setpoint"
|
51 |
+
TARGET_TEMP_HIGH: Final = "setpoint_high"
|
52 |
+
TARGET_TEMP_LOW: Final = "setpoint_low"
|
53 |
+
THERMOSTAT: Final = "thermostat"
|
54 |
+
|
55 |
+
# Config_flow constants
|
56 |
+
ANNA_WITH_ADAM: Final = "anna_with_adam"
|
57 |
+
CONTEXT: Final = "context"
|
58 |
+
FLOW_ID: Final = "flow_id"
|
59 |
+
FLOW_NET: Final = "Network: Smile/Stretch"
|
60 |
+
FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
|
61 |
+
FLOW_STRETCH: Final = "Stretch (Stretch)"
|
62 |
+
FLOW_TYPE: Final = "flow_type"
|
63 |
+
INIT: Final = "init"
|
64 |
+
PRODUCT: Final = "product"
|
65 |
+
SMILE_OPEN_THERM: Final = "smile_open_therm"
|
66 |
+
SMILE_THERMO: Final = "smile_thermo"
|
67 |
+
TITLE_PLACEHOLDERS: Final = "title_placeholders"
|
68 |
+
VERSION: Final = "version"
|
69 |
+
|
70 |
+
# Entity constants
|
71 |
+
AVAILABLE: Final = "available"
|
72 |
+
FIRMWARE: Final = "firmware"
|
73 |
+
GATEWAY_ID: Final = "gateway_id"
|
74 |
+
HARDWARE: Final = "hardware"
|
75 |
+
MODEL: Final = "model"
|
76 |
+
MODEL_ID: Final = "model_id"
|
77 |
+
SMILE_NAME: Final = "smile_name"
|
78 |
+
VENDOR: Final = "vendor"
|
79 |
+
|
80 |
+
# Number constants
|
81 |
+
MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
|
82 |
+
MAX_DHW_TEMP: Final = "max_dhw_temperature"
|
83 |
+
LOWER_BOUND: Final = "lower_bound"
|
84 |
+
RESOLUTION: Final = "resolution"
|
85 |
+
TEMPERATURE_OFFSET: Final = "temperature_offset"
|
86 |
+
UPPER_BOUND: Final = "upper_bound"
|
87 |
+
|
88 |
+
# Sensor constants
|
89 |
+
DHW_TEMP: Final = "dhw_temperature"
|
90 |
+
DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
|
91 |
+
EL_CONSUMED: Final = "electricity_consumed"
|
92 |
+
EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
|
93 |
+
EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
|
94 |
+
EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
|
95 |
+
EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
|
96 |
+
EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
|
97 |
+
EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
|
98 |
+
EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
|
99 |
+
EL_CONS_POINT: Final = "electricity_consumed_point"
|
100 |
+
EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
|
101 |
+
EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
|
102 |
+
EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
|
103 |
+
EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
|
104 |
+
EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
|
105 |
+
EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
|
106 |
+
EL_PRODUCED: Final = "electricity_produced"
|
107 |
+
EL_PROD_INTERVAL: Final = "electricity_produced_interval"
|
108 |
+
EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
|
109 |
+
EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
|
110 |
+
EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
|
111 |
+
EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
|
112 |
+
EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
|
113 |
+
EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
|
114 |
+
EL_PROD_POINT: Final = "electricity_produced_point"
|
115 |
+
GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
|
116 |
+
GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
|
117 |
+
INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
|
118 |
+
MOD_LEVEL: Final = "modulation_level"
|
119 |
+
NET_EL_POINT: Final = "net_electricity_point"
|
120 |
+
NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
|
121 |
+
OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
|
122 |
+
OUTDOOR_TEMP: Final = "outdoor_temperature"
|
123 |
+
RETURN_TEMP: Final = "return_temperature"
|
124 |
+
SENSORS: Final = "sensors"
|
125 |
+
TEMP_DIFF: Final = "temperature_difference"
|
126 |
+
VALVE_POS: Final = "valve_position"
|
127 |
+
VOLTAGE_PH1: Final = "voltage_phase_one"
|
128 |
+
VOLTAGE_PH2: Final = "voltage_phase_two"
|
129 |
+
VOLTAGE_PH3: Final = "voltage_phase_three"
|
130 |
+
WATER_TEMP: Final = "water_temperature"
|
131 |
+
WATER_PRESSURE: Final = "water_pressure"
|
132 |
+
|
133 |
+
# Select constants
|
134 |
+
AVAILABLE_SCHEDULES: Final = "available_schedules"
|
135 |
+
DHW_MODE: Final = "dhw_mode"
|
136 |
+
DHW_MODES: Final = "dhw_modes"
|
137 |
+
GATEWAY_MODE: Final = "gateway_mode"
|
138 |
+
GATEWAY_MODES: Final = "gateway_modes"
|
139 |
+
REGULATION_MODE: Final = "regulation_mode"
|
140 |
+
REGULATION_MODES: Final = "regulation_modes"
|
141 |
+
SELECT_DHW_MODE: Final = "select_dhw_mode"
|
142 |
+
SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
|
143 |
+
SELECT_REGULATION_MODE: Final = "select_regulation_mode"
|
144 |
+
SELECT_SCHEDULE: Final = "select_schedule"
|
145 |
+
|
146 |
+
# Switch constants
|
147 |
+
DHW_CM_SWITCH: Final = "dhw_cm_switch"
|
148 |
+
LOCK: Final = "lock"
|
149 |
+
MEMBERS: Final ="members"
|
150 |
+
RELAY: Final = "relay"
|
151 |
+
COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
|
152 |
+
SWITCHES: Final = "switches"
|
153 |
|
154 |
+
# Default directives
|
155 |
+
DEFAULT_PORT: Final[int] = 80
|
156 |
+
DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
|
157 |
+
"power": timedelta(seconds=10),
|
158 |
+
"stretch": timedelta(seconds=60),
|
159 |
+
"thermostat": timedelta(seconds=60),
|
160 |
+
}
|
161 |
+
DEFAULT_TIMEOUT: Final[int] = 30
|
162 |
+
DEFAULT_USERNAME: Final = "smile"
|
163 |
+
|
164 |
+
# --- Const for Plugwise Smile and Stretch
|
165 |
PLATFORMS: Final[list[str]] = [
|
166 |
Platform.BINARY_SENSOR,
|
167 |
Platform.BUTTON,
|
|
|
171 |
Platform.SENSOR,
|
172 |
Platform.SWITCH,
|
173 |
]
|
174 |
+
SERVICE_DELETE: Final = "delete_notification"
|
175 |
+
SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
|
176 |
+
|
177 |
+
# Climate const:
|
178 |
+
MASTER_THERMOSTATS: Final[list[str]] = [
|
179 |
+
"thermostat",
|
180 |
+
"zone_thermometer",
|
181 |
+
"zone_thermostat",
|
182 |
+
"thermostatic_radiator_valve",
|
183 |
+
]
|
184 |
+
|
185 |
+
# Config_flow const:
|
186 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
187 |
"smile": "Smile P1",
|
188 |
"smile_thermo": "Smile Anna",
|
|
|
208 |
"regulation_modes",
|
209 |
"available_schedules",
|
210 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -2,7 +2,6 @@
|
|
2 |
|
3 |
from datetime import timedelta
|
4 |
|
5 |
-
from packaging.version import Version
|
6 |
from plugwise import PlugwiseData, Smile
|
7 |
from plugwise.exceptions import (
|
8 |
ConnectionFailedError,
|
@@ -14,15 +13,22 @@
|
|
14 |
)
|
15 |
|
16 |
from homeassistant.config_entries import ConfigEntry
|
17 |
-
from homeassistant.const import
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
from homeassistant.core import HomeAssistant
|
19 |
from homeassistant.exceptions import ConfigEntryError
|
20 |
from homeassistant.helpers import device_registry as dr
|
21 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
22 |
from homeassistant.helpers.debounce import Debouncer
|
23 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
|
24 |
|
25 |
-
from .const import
|
26 |
|
27 |
|
28 |
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
@@ -32,32 +38,40 @@
|
|
32 |
|
33 |
config_entry: ConfigEntry
|
34 |
|
35 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
36 |
"""Initialize the coordinator."""
|
37 |
super().__init__(
|
38 |
hass,
|
39 |
LOGGER,
|
40 |
name=DOMAIN,
|
41 |
-
|
|
|
|
|
42 |
# Don't refresh immediately, give the device time to process
|
43 |
# the change in state before we query it.
|
44 |
request_refresh_debouncer=Debouncer(
|
45 |
hass,
|
46 |
LOGGER,
|
47 |
-
cooldown=
|
48 |
immediate=False,
|
49 |
),
|
50 |
)
|
51 |
|
52 |
self.api = Smile(
|
53 |
host=self.config_entry.data[CONF_HOST],
|
54 |
-
username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
|
55 |
password=self.config_entry.data[CONF_PASSWORD],
|
56 |
-
port=self.config_entry.data
|
|
|
57 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
58 |
)
|
59 |
self._current_devices: set[str] = set()
|
60 |
self.new_devices: set[str] = set()
|
|
|
61 |
|
62 |
async def _connect(self) -> None:
|
63 |
"""Connect to the Plugwise Smile."""
|
@@ -65,9 +79,19 @@
|
|
65 |
self._connected = isinstance(version, Version)
|
66 |
if self._connected:
|
67 |
self.api.get_all_gateway_entities()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
async def _async_update_data(self) -> PlugwiseData:
|
70 |
"""Fetch data from Plugwise."""
|
|
|
71 |
try:
|
72 |
if not self._connected:
|
73 |
await self._connect()
|
@@ -83,6 +107,7 @@
|
|
83 |
translation_key="authentication_failed",
|
84 |
) from err
|
85 |
except (InvalidXMLError, ResponseError) as err:
|
|
|
86 |
raise UpdateFailed(
|
87 |
translation_domain=DOMAIN,
|
88 |
translation_key="invalid_xml_data",
|
@@ -97,11 +122,13 @@
|
|
97 |
translation_domain=DOMAIN,
|
98 |
translation_key="unsupported_firmware",
|
99 |
) from err
|
|
|
|
|
|
|
100 |
|
101 |
-
self._async_add_remove_devices(data, self.config_entry)
|
102 |
return data
|
103 |
|
104 |
-
def
|
105 |
"""Add new Plugwise devices, remove non-existing devices."""
|
106 |
# Check for new or removed devices
|
107 |
self.new_devices = set(data.devices) - self._current_devices
|
@@ -109,20 +136,19 @@
|
|
109 |
self._current_devices = set(data.devices)
|
110 |
|
111 |
if removed_devices:
|
112 |
-
self.
|
113 |
|
114 |
-
def
|
115 |
"""Clean registries when removed devices found."""
|
116 |
device_reg = dr.async_get(self.hass)
|
117 |
device_list = dr.async_entries_for_config_entry(
|
118 |
device_reg, self.config_entry.entry_id
|
119 |
)
|
|
|
120 |
# First find the Plugwise via_device
|
121 |
-
gateway_device = device_reg.async_get_device(
|
122 |
-
|
123 |
-
|
124 |
-
assert gateway_device is not None
|
125 |
-
via_device_id = gateway_device.id
|
126 |
|
127 |
# Then remove the connected orphaned device(s)
|
128 |
for device_entry in device_list:
|
@@ -136,7 +162,7 @@
|
|
136 |
device_entry.id, remove_config_entry_id=entry.entry_id
|
137 |
)
|
138 |
LOGGER.debug(
|
139 |
-
"Removed %s device %s %s from device_registry",
|
140 |
DOMAIN,
|
141 |
device_entry.model,
|
142 |
identifier[1],
|
|
|
2 |
|
3 |
from datetime import timedelta
|
4 |
|
|
|
5 |
from plugwise import PlugwiseData, Smile
|
6 |
from plugwise.exceptions import (
|
7 |
ConnectionFailedError,
|
|
|
13 |
)
|
14 |
|
15 |
from homeassistant.config_entries import ConfigEntry
|
16 |
+
from homeassistant.const import (
|
17 |
+
CONF_HOST,
|
18 |
+
CONF_PASSWORD,
|
19 |
+
CONF_PORT,
|
20 |
+
CONF_SCAN_INTERVAL, # pw-beta options
|
21 |
+
CONF_USERNAME,
|
22 |
+
)
|
23 |
from homeassistant.core import HomeAssistant
|
24 |
from homeassistant.exceptions import ConfigEntryError
|
25 |
from homeassistant.helpers import device_registry as dr
|
26 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
27 |
from homeassistant.helpers.debounce import Debouncer
|
28 |
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
29 |
+
from packaging.version import Version
|
30 |
|
31 |
+
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, GATEWAY_ID, LOGGER
|
32 |
|
33 |
|
34 |
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
|
|
38 |
|
39 |
config_entry: ConfigEntry
|
40 |
|
41 |
+
def __init__(
|
42 |
+
self,
|
43 |
+
hass: HomeAssistant,
|
44 |
+
cooldown: float,
|
45 |
+
update_interval: timedelta = timedelta(seconds=60),
|
46 |
+
) -> None: # pw-beta cooldown
|
47 |
"""Initialize the coordinator."""
|
48 |
super().__init__(
|
49 |
hass,
|
50 |
LOGGER,
|
51 |
name=DOMAIN,
|
52 |
+
# Core directly updates from const's DEFAULT_SCAN_INTERVAL
|
53 |
+
# Upstream check correct progress for adjusting
|
54 |
+
update_interval=update_interval,
|
55 |
# Don't refresh immediately, give the device time to process
|
56 |
# the change in state before we query it.
|
57 |
request_refresh_debouncer=Debouncer(
|
58 |
hass,
|
59 |
LOGGER,
|
60 |
+
cooldown=cooldown,
|
61 |
immediate=False,
|
62 |
),
|
63 |
)
|
64 |
|
65 |
self.api = Smile(
|
66 |
host=self.config_entry.data[CONF_HOST],
|
|
|
67 |
password=self.config_entry.data[CONF_PASSWORD],
|
68 |
+
port=self.config_entry.data[CONF_PORT],
|
69 |
+
username=self.config_entry.data[CONF_USERNAME],
|
70 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
71 |
)
|
72 |
self._current_devices: set[str] = set()
|
73 |
self.new_devices: set[str] = set()
|
74 |
+
self.update_interval = update_interval
|
75 |
|
76 |
async def _connect(self) -> None:
|
77 |
"""Connect to the Plugwise Smile."""
|
|
|
79 |
self._connected = isinstance(version, Version)
|
80 |
if self._connected:
|
81 |
self.api.get_all_gateway_entities()
|
82 |
+
self.update_interval = DEFAULT_SCAN_INTERVAL.get(
|
83 |
+
self.api.smile_type, timedelta(seconds=60)
|
84 |
+
) # pw-beta options scan-interval
|
85 |
+
if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
|
86 |
+
self.update_interval = timedelta(
|
87 |
+
seconds=int(custom_time)
|
88 |
+
) # pragma: no cover # pw-beta options
|
89 |
+
|
90 |
+
LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
|
91 |
|
92 |
async def _async_update_data(self) -> PlugwiseData:
|
93 |
"""Fetch data from Plugwise."""
|
94 |
+
data = PlugwiseData(devices={}, gateway={})
|
95 |
try:
|
96 |
if not self._connected:
|
97 |
await self._connect()
|
|
|
107 |
translation_key="authentication_failed",
|
108 |
) from err
|
109 |
except (InvalidXMLError, ResponseError) as err:
|
110 |
+
# pwbeta TODO; we had {err} in the text, but not upstream, do we want this?
|
111 |
raise UpdateFailed(
|
112 |
translation_domain=DOMAIN,
|
113 |
translation_key="invalid_xml_data",
|
|
|
122 |
translation_domain=DOMAIN,
|
123 |
translation_key="unsupported_firmware",
|
124 |
) from err
|
125 |
+
else:
|
126 |
+
LOGGER.debug(f"{self.api.smile_name} data: %s", data)
|
127 |
+
await self.async_add_remove_devices(data, self.config_entry)
|
128 |
|
|
|
129 |
return data
|
130 |
|
131 |
+
async def async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
|
132 |
"""Add new Plugwise devices, remove non-existing devices."""
|
133 |
# Check for new or removed devices
|
134 |
self.new_devices = set(data.devices) - self._current_devices
|
|
|
136 |
self._current_devices = set(data.devices)
|
137 |
|
138 |
if removed_devices:
|
139 |
+
await self.async_remove_devices(data, entry)
|
140 |
|
141 |
+
async def async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
|
142 |
"""Clean registries when removed devices found."""
|
143 |
device_reg = dr.async_get(self.hass)
|
144 |
device_list = dr.async_entries_for_config_entry(
|
145 |
device_reg, self.config_entry.entry_id
|
146 |
)
|
147 |
+
|
148 |
# First find the Plugwise via_device
|
149 |
+
gateway_device = device_reg.async_get_device({(DOMAIN, data.gateway[GATEWAY_ID])})
|
150 |
+
if gateway_device is not None:
|
151 |
+
via_device_id = gateway_device.id
|
|
|
|
|
152 |
|
153 |
# Then remove the connected orphaned device(s)
|
154 |
for device_entry in device_list:
|
|
|
162 |
device_entry.id, remove_config_entry_id=entry.entry_id
|
163 |
)
|
164 |
LOGGER.debug(
|
165 |
+
"Removed %s device/zone %s %s from device_registry",
|
166 |
DOMAIN,
|
167 |
device_entry.model,
|
168 |
identifier[1],
|
@@ -12,7 +12,21 @@
|
|
12 |
)
|
13 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
14 |
|
15 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
17 |
|
18 |
|
@@ -36,30 +50,30 @@
|
|
36 |
|
37 |
data = coordinator.data.devices[device_id]
|
38 |
connections = set()
|
39 |
-
if mac := data.get(
|
40 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
41 |
-
if mac := data.get(
|
42 |
connections.add((CONNECTION_ZIGBEE, mac))
|
43 |
|
44 |
self._attr_device_info = DeviceInfo(
|
45 |
configuration_url=configuration_url,
|
46 |
identifiers={(DOMAIN, device_id)},
|
47 |
connections=connections,
|
48 |
-
manufacturer=data.get(
|
49 |
-
model=data.get(
|
50 |
-
model_id=data.get(
|
51 |
-
name=coordinator.data.gateway[
|
52 |
-
sw_version=data.get(
|
53 |
-
hw_version=data.get(
|
54 |
)
|
55 |
|
56 |
-
if device_id != coordinator.data.gateway[
|
57 |
self._attr_device_info.update(
|
58 |
{
|
59 |
-
ATTR_NAME: data.get(
|
60 |
ATTR_VIA_DEVICE: (
|
61 |
DOMAIN,
|
62 |
-
str(self.coordinator.data.gateway[
|
63 |
),
|
64 |
}
|
65 |
)
|
@@ -68,8 +82,10 @@
|
|
68 |
def available(self) -> bool:
|
69 |
"""Return if entity is available."""
|
70 |
return (
|
|
|
|
|
71 |
self._dev_id in self.coordinator.data.devices
|
72 |
-
and (
|
73 |
and super().available
|
74 |
)
|
75 |
|
|
|
12 |
)
|
13 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
14 |
|
15 |
+
from .const import (
|
16 |
+
AVAILABLE,
|
17 |
+
DOMAIN,
|
18 |
+
FIRMWARE,
|
19 |
+
GATEWAY_ID,
|
20 |
+
HARDWARE,
|
21 |
+
MAC_ADDRESS,
|
22 |
+
MODEL,
|
23 |
+
MODEL_ID,
|
24 |
+
SMILE_NAME,
|
25 |
+
VENDOR,
|
26 |
+
ZIGBEE_MAC_ADDRESS,
|
27 |
+
)
|
28 |
+
|
29 |
+
# Upstream consts
|
30 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
31 |
|
32 |
|
|
|
50 |
|
51 |
data = coordinator.data.devices[device_id]
|
52 |
connections = set()
|
53 |
+
if mac := data.get(MAC_ADDRESS):
|
54 |
connections.add((CONNECTION_NETWORK_MAC, mac))
|
55 |
+
if mac := data.get(ZIGBEE_MAC_ADDRESS):
|
56 |
connections.add((CONNECTION_ZIGBEE, mac))
|
57 |
|
58 |
self._attr_device_info = DeviceInfo(
|
59 |
configuration_url=configuration_url,
|
60 |
identifiers={(DOMAIN, device_id)},
|
61 |
connections=connections,
|
62 |
+
manufacturer=data.get(VENDOR),
|
63 |
+
model=data.get(MODEL),
|
64 |
+
model_id=data.get(MODEL_ID),
|
65 |
+
name=coordinator.data.gateway[SMILE_NAME],
|
66 |
+
sw_version=data.get(FIRMWARE),
|
67 |
+
hw_version=data.get(HARDWARE),
|
68 |
)
|
69 |
|
70 |
+
if device_id != coordinator.data.gateway[GATEWAY_ID]:
|
71 |
self._attr_device_info.update(
|
72 |
{
|
73 |
+
ATTR_NAME: data.get(ATTR_NAME),
|
74 |
ATTR_VIA_DEVICE: (
|
75 |
DOMAIN,
|
76 |
+
str(self.coordinator.data.gateway[GATEWAY_ID]),
|
77 |
),
|
78 |
}
|
79 |
)
|
|
|
82 |
def available(self) -> bool:
|
83 |
"""Return if entity is available."""
|
84 |
return (
|
85 |
+
# Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
|
86 |
+
# Upstream: do not provide their availability-status!
|
87 |
self._dev_id in self.coordinator.data.devices
|
88 |
+
and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
|
89 |
and super().available
|
90 |
)
|
91 |
|
@@ -120,5 +120,8 @@
|
|
120 |
"default": "mdi:lock"
|
121 |
}
|
122 |
}
|
|
|
|
|
|
|
123 |
}
|
124 |
}
|
|
|
120 |
"default": "mdi:lock"
|
121 |
}
|
122 |
}
|
123 |
+
},
|
124 |
+
"services": {
|
125 |
+
"delete_notification": "mdi:trash-can"
|
126 |
}
|
127 |
}
|
@@ -1,12 +1,13 @@
|
|
1 |
{
|
2 |
"domain": "plugwise",
|
3 |
-
"name": "Plugwise",
|
4 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
5 |
"config_flow": true,
|
6 |
-
"documentation": "https://
|
7 |
"integration_type": "hub",
|
8 |
"iot_class": "local_polling",
|
9 |
"loggers": ["plugwise"],
|
10 |
"requirements": ["plugwise==1.6.4"],
|
|
|
11 |
"zeroconf": ["_plugwise._tcp.local."]
|
12 |
}
|
|
|
1 |
{
|
2 |
"domain": "plugwise",
|
3 |
+
"name": "Plugwise Beta",
|
4 |
"codeowners": ["@CoMPaTech", "@bouwew"],
|
5 |
"config_flow": true,
|
6 |
+
"documentation": "https://github.com/plugwise/plugwise-beta",
|
7 |
"integration_type": "hub",
|
8 |
"iot_class": "local_polling",
|
9 |
"loggers": ["plugwise"],
|
10 |
"requirements": ["plugwise==1.6.4"],
|
11 |
+
"version": "0.55.5",
|
12 |
"zeroconf": ["_plugwise._tcp.local."]
|
13 |
}
|
@@ -15,7 +15,18 @@
|
|
15 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
16 |
|
17 |
from . import PlugwiseConfigEntry
|
18 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
20 |
from .entity import PlugwiseEntity
|
21 |
from .util import plugwise_command
|
@@ -30,24 +41,25 @@
|
|
30 |
key: NumberType
|
31 |
|
32 |
|
|
|
33 |
NUMBER_TYPES = (
|
34 |
PlugwiseNumberEntityDescription(
|
35 |
-
key=
|
36 |
-
translation_key=
|
37 |
device_class=NumberDeviceClass.TEMPERATURE,
|
38 |
entity_category=EntityCategory.CONFIG,
|
39 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
40 |
),
|
41 |
PlugwiseNumberEntityDescription(
|
42 |
-
key=
|
43 |
-
translation_key=
|
44 |
device_class=NumberDeviceClass.TEMPERATURE,
|
45 |
entity_category=EntityCategory.CONFIG,
|
46 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
47 |
),
|
48 |
PlugwiseNumberEntityDescription(
|
49 |
-
key=
|
50 |
-
translation_key=
|
51 |
device_class=NumberDeviceClass.TEMPERATURE,
|
52 |
entity_category=EntityCategory.CONFIG,
|
53 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
@@ -60,7 +72,8 @@
|
|
60 |
entry: PlugwiseConfigEntry,
|
61 |
async_add_entities: AddEntitiesCallback,
|
62 |
) -> None:
|
63 |
-
"""Set up Plugwise number platform."""
|
|
|
64 |
coordinator = entry.runtime_data
|
65 |
|
66 |
@callback
|
@@ -69,12 +82,28 @@
|
|
69 |
if not coordinator.new_devices:
|
70 |
return
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
_add_entities()
|
80 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
@@ -93,15 +122,16 @@
|
|
93 |
) -> None:
|
94 |
"""Initiate Plugwise Number."""
|
95 |
super().__init__(coordinator, device_id)
|
96 |
-
self.
|
97 |
-
self._attr_native_max_value = self.device[description.key]["upper_bound"]
|
98 |
-
self._attr_native_min_value = self.device[description.key]["lower_bound"]
|
99 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
100 |
self.device_id = device_id
|
101 |
self.entity_description = description
|
|
|
|
|
|
|
|
|
102 |
|
103 |
-
native_step = self.device[description.key][
|
104 |
-
if description.key !=
|
105 |
native_step = max(native_step, 0.5)
|
106 |
self._attr_native_step = native_step
|
107 |
|
@@ -113,6 +143,7 @@
|
|
113 |
@plugwise_command
|
114 |
async def async_set_native_value(self, value: float) -> None:
|
115 |
"""Change to the new setpoint value."""
|
116 |
-
await self.coordinator.api.set_number(
|
117 |
-
|
|
|
118 |
)
|
|
|
15 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
16 |
|
17 |
from . import PlugwiseConfigEntry
|
18 |
+
from .const import (
|
19 |
+
LOGGER,
|
20 |
+
LOWER_BOUND,
|
21 |
+
MAX_BOILER_TEMP,
|
22 |
+
MAX_DHW_TEMP,
|
23 |
+
RESOLUTION,
|
24 |
+
TEMPERATURE_OFFSET,
|
25 |
+
UPPER_BOUND,
|
26 |
+
NumberType,
|
27 |
+
)
|
28 |
+
|
29 |
+
# Upstream consts
|
30 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
31 |
from .entity import PlugwiseEntity
|
32 |
from .util import plugwise_command
|
|
|
41 |
key: NumberType
|
42 |
|
43 |
|
44 |
+
# Upstream + is there a reason we didn't rename this one prefixed?
|
45 |
NUMBER_TYPES = (
|
46 |
PlugwiseNumberEntityDescription(
|
47 |
+
key=MAX_BOILER_TEMP,
|
48 |
+
translation_key=MAX_BOILER_TEMP,
|
49 |
device_class=NumberDeviceClass.TEMPERATURE,
|
50 |
entity_category=EntityCategory.CONFIG,
|
51 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
52 |
),
|
53 |
PlugwiseNumberEntityDescription(
|
54 |
+
key=MAX_DHW_TEMP,
|
55 |
+
translation_key=MAX_DHW_TEMP,
|
56 |
device_class=NumberDeviceClass.TEMPERATURE,
|
57 |
entity_category=EntityCategory.CONFIG,
|
58 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
59 |
),
|
60 |
PlugwiseNumberEntityDescription(
|
61 |
+
key=TEMPERATURE_OFFSET,
|
62 |
+
translation_key=TEMPERATURE_OFFSET,
|
63 |
device_class=NumberDeviceClass.TEMPERATURE,
|
64 |
entity_category=EntityCategory.CONFIG,
|
65 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
72 |
entry: PlugwiseConfigEntry,
|
73 |
async_add_entities: AddEntitiesCallback,
|
74 |
) -> None:
|
75 |
+
"""Set up Plugwise number platform from a config entry."""
|
76 |
+
# Upstream above to adhere to standard used
|
77 |
coordinator = entry.runtime_data
|
78 |
|
79 |
@callback
|
|
|
82 |
if not coordinator.new_devices:
|
83 |
return
|
84 |
|
85 |
+
# Upstream consts
|
86 |
+
# async_add_entities(
|
87 |
+
# PlugwiseNumberEntity(coordinator, device_id, description)
|
88 |
+
# for device_id in coordinator.new_devices
|
89 |
+
# for description in NUMBER_TYPES
|
90 |
+
# if description.key in coordinator.data.devices[device_id]
|
91 |
+
# )
|
92 |
+
|
93 |
+
# pw-beta alternative for debugging
|
94 |
+
entities: list[PlugwiseNumberEntity] = []
|
95 |
+
for device_id in coordinator.new_devices:
|
96 |
+
device = coordinator.data.devices[device_id]
|
97 |
+
for description in NUMBER_TYPES:
|
98 |
+
if description.key in device:
|
99 |
+
entities.append(
|
100 |
+
PlugwiseNumberEntity(coordinator, device_id, description)
|
101 |
+
)
|
102 |
+
LOGGER.debug(
|
103 |
+
"Add %s %s number", device["name"], description.translation_key
|
104 |
+
)
|
105 |
+
|
106 |
+
async_add_entities(entities)
|
107 |
|
108 |
_add_entities()
|
109 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
122 |
) -> None:
|
123 |
"""Initiate Plugwise Number."""
|
124 |
super().__init__(coordinator, device_id)
|
125 |
+
self.actuator = self.device[description.key] # Upstream
|
|
|
|
|
|
|
126 |
self.device_id = device_id
|
127 |
self.entity_description = description
|
128 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
129 |
+
self._attr_mode = NumberMode.BOX
|
130 |
+
self._attr_native_max_value = self.device[description.key][UPPER_BOUND] # Upstream const
|
131 |
+
self._attr_native_min_value = self.device[description.key][LOWER_BOUND] # Upstream const
|
132 |
|
133 |
+
native_step = self.device[description.key][RESOLUTION] # Upstream const
|
134 |
+
if description.key != TEMPERATURE_OFFSET: # Upstream const
|
135 |
native_step = max(native_step, 0.5)
|
136 |
self._attr_native_step = native_step
|
137 |
|
|
|
143 |
@plugwise_command
|
144 |
async def async_set_native_value(self, value: float) -> None:
|
145 |
"""Change to the new setpoint value."""
|
146 |
+
await self.coordinator.api.set_number(self.device_id, self.entity_description.key, value)
|
147 |
+
LOGGER.debug(
|
148 |
+
"Setting %s to %s was successful", self.entity_description.key, value
|
149 |
)
|
@@ -10,7 +10,25 @@
|
|
10 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
11 |
|
12 |
from . import PlugwiseConfigEntry
|
13 |
-
from .const import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
15 |
from .entity import PlugwiseEntity
|
16 |
from .util import plugwise_command
|
@@ -26,29 +44,30 @@
|
|
26 |
options_key: SelectOptionsType
|
27 |
|
28 |
|
|
|
29 |
SELECT_TYPES = (
|
30 |
PlugwiseSelectEntityDescription(
|
31 |
-
key=
|
32 |
-
translation_key=
|
33 |
-
options_key=
|
34 |
),
|
35 |
PlugwiseSelectEntityDescription(
|
36 |
-
key=
|
37 |
-
translation_key=
|
38 |
entity_category=EntityCategory.CONFIG,
|
39 |
-
options_key=
|
40 |
),
|
41 |
PlugwiseSelectEntityDescription(
|
42 |
-
key=
|
43 |
-
translation_key=
|
44 |
entity_category=EntityCategory.CONFIG,
|
45 |
-
options_key=
|
46 |
),
|
47 |
PlugwiseSelectEntityDescription(
|
48 |
-
key=
|
49 |
-
translation_key=
|
50 |
entity_category=EntityCategory.CONFIG,
|
51 |
-
options_key=
|
52 |
),
|
53 |
)
|
54 |
|
@@ -58,7 +77,7 @@
|
|
58 |
entry: PlugwiseConfigEntry,
|
59 |
async_add_entities: AddEntitiesCallback,
|
60 |
) -> None:
|
61 |
-
"""Set up
|
62 |
coordinator = entry.runtime_data
|
63 |
|
64 |
@callback
|
@@ -67,12 +86,27 @@
|
|
67 |
if not coordinator.new_devices:
|
68 |
return
|
69 |
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
_add_entities()
|
78 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
@@ -95,7 +129,7 @@
|
|
95 |
self.entity_description = entity_description
|
96 |
|
97 |
self._location = device_id
|
98 |
-
if (location := self.device.get(
|
99 |
self._location = location
|
100 |
|
101 |
@property
|
@@ -112,8 +146,13 @@
|
|
112 |
async def async_select_option(self, option: str) -> None:
|
113 |
"""Change to the selected entity option.
|
114 |
|
115 |
-
|
116 |
"""
|
117 |
await self.coordinator.api.set_select(
|
118 |
self.entity_description.key, self._location, option, STATE_ON
|
119 |
)
|
|
|
|
|
|
|
|
|
|
|
|
10 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
11 |
|
12 |
from . import PlugwiseConfigEntry
|
13 |
+
from .const import (
|
14 |
+
AVAILABLE_SCHEDULES,
|
15 |
+
DHW_MODE,
|
16 |
+
DHW_MODES,
|
17 |
+
GATEWAY_MODE,
|
18 |
+
GATEWAY_MODES,
|
19 |
+
LOCATION,
|
20 |
+
LOGGER,
|
21 |
+
REGULATION_MODE,
|
22 |
+
REGULATION_MODES,
|
23 |
+
SELECT_DHW_MODE,
|
24 |
+
SELECT_GATEWAY_MODE,
|
25 |
+
SELECT_REGULATION_MODE,
|
26 |
+
SELECT_SCHEDULE,
|
27 |
+
SelectOptionsType,
|
28 |
+
SelectType,
|
29 |
+
)
|
30 |
+
|
31 |
+
# Upstream consts
|
32 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
33 |
from .entity import PlugwiseEntity
|
34 |
from .util import plugwise_command
|
|
|
44 |
options_key: SelectOptionsType
|
45 |
|
46 |
|
47 |
+
# Upstream + is there a reason we didn't rename this one prefixed?
|
48 |
SELECT_TYPES = (
|
49 |
PlugwiseSelectEntityDescription(
|
50 |
+
key=SELECT_SCHEDULE,
|
51 |
+
translation_key=SELECT_SCHEDULE,
|
52 |
+
options_key=AVAILABLE_SCHEDULES,
|
53 |
),
|
54 |
PlugwiseSelectEntityDescription(
|
55 |
+
key=SELECT_REGULATION_MODE,
|
56 |
+
translation_key=REGULATION_MODE,
|
57 |
entity_category=EntityCategory.CONFIG,
|
58 |
+
options_key=REGULATION_MODES,
|
59 |
),
|
60 |
PlugwiseSelectEntityDescription(
|
61 |
+
key=SELECT_DHW_MODE,
|
62 |
+
translation_key=DHW_MODE,
|
63 |
entity_category=EntityCategory.CONFIG,
|
64 |
+
options_key=DHW_MODES,
|
65 |
),
|
66 |
PlugwiseSelectEntityDescription(
|
67 |
+
key=SELECT_GATEWAY_MODE,
|
68 |
+
translation_key=GATEWAY_MODE,
|
69 |
entity_category=EntityCategory.CONFIG,
|
70 |
+
options_key=GATEWAY_MODES,
|
71 |
),
|
72 |
)
|
73 |
|
|
|
77 |
entry: PlugwiseConfigEntry,
|
78 |
async_add_entities: AddEntitiesCallback,
|
79 |
) -> None:
|
80 |
+
"""Set up Plugwise selector from a config entry."""
|
81 |
coordinator = entry.runtime_data
|
82 |
|
83 |
@callback
|
|
|
86 |
if not coordinator.new_devices:
|
87 |
return
|
88 |
|
89 |
+
# Upstream consts
|
90 |
+
# async_add_entities(
|
91 |
+
# PlugwiseSelectEntity(coordinator, device_id, description)
|
92 |
+
# for device_id in coordinator.new_devices
|
93 |
+
# for description in SELECT_TYPES
|
94 |
+
# if description.options_key in coordinator.data.devices[device_id]
|
95 |
+
# )
|
96 |
+
# pw-beta alternative for debugging
|
97 |
+
entities: list[PlugwiseSelectEntity] = []
|
98 |
+
for device_id in coordinator.new_devices:
|
99 |
+
device = coordinator.data.devices[device_id]
|
100 |
+
for description in SELECT_TYPES:
|
101 |
+
if description.options_key in device:
|
102 |
+
entities.append(
|
103 |
+
PlugwiseSelectEntity(coordinator, device_id, description)
|
104 |
+
)
|
105 |
+
LOGGER.debug(
|
106 |
+
"Add %s %s selector", device["name"], description.translation_key
|
107 |
+
)
|
108 |
+
|
109 |
+
async_add_entities(entities)
|
110 |
|
111 |
_add_entities()
|
112 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
129 |
self.entity_description = entity_description
|
130 |
|
131 |
self._location = device_id
|
132 |
+
if (location := self.device.get(LOCATION)) is not None:
|
133 |
self._location = location
|
134 |
|
135 |
@property
|
|
|
146 |
async def async_select_option(self, option: str) -> None:
|
147 |
"""Change to the selected entity option.
|
148 |
|
149 |
+
Location ID and STATE_ON are required for the thermostat-schedule select.
|
150 |
"""
|
151 |
await self.coordinator.api.set_select(
|
152 |
self.entity_description.key, self._location, option, STATE_ON
|
153 |
)
|
154 |
+
LOGGER.debug(
|
155 |
+
"Set %s to %s was successful",
|
156 |
+
self.entity_description.key,
|
157 |
+
option,
|
158 |
+
)
|
@@ -13,6 +13,7 @@
|
|
13 |
SensorStateClass,
|
14 |
)
|
15 |
from homeassistant.const import (
|
|
|
16 |
LIGHT_LUX,
|
17 |
PERCENTAGE,
|
18 |
EntityCategory,
|
@@ -28,6 +29,57 @@
|
|
28 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
29 |
|
30 |
from . import PlugwiseConfigEntry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
32 |
from .entity import PlugwiseEntity
|
33 |
|
@@ -42,17 +94,18 @@
|
|
42 |
key: SensorType
|
43 |
|
44 |
|
45 |
-
|
|
|
46 |
PlugwiseSensorEntityDescription(
|
47 |
-
key=
|
48 |
-
translation_key=
|
49 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
50 |
device_class=SensorDeviceClass.TEMPERATURE,
|
51 |
state_class=SensorStateClass.MEASUREMENT,
|
52 |
entity_category=EntityCategory.DIAGNOSTIC,
|
53 |
),
|
54 |
PlugwiseSensorEntityDescription(
|
55 |
-
key=
|
56 |
translation_key="cooling_setpoint",
|
57 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
58 |
device_class=SensorDeviceClass.TEMPERATURE,
|
@@ -60,7 +113,7 @@
|
|
60 |
entity_category=EntityCategory.DIAGNOSTIC,
|
61 |
),
|
62 |
PlugwiseSensorEntityDescription(
|
63 |
-
key=
|
64 |
translation_key="heating_setpoint",
|
65 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
66 |
device_class=SensorDeviceClass.TEMPERATURE,
|
@@ -68,276 +121,277 @@
|
|
68 |
entity_category=EntityCategory.DIAGNOSTIC,
|
69 |
),
|
70 |
PlugwiseSensorEntityDescription(
|
71 |
-
key=
|
72 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
73 |
device_class=SensorDeviceClass.TEMPERATURE,
|
74 |
entity_category=EntityCategory.DIAGNOSTIC,
|
75 |
state_class=SensorStateClass.MEASUREMENT,
|
76 |
),
|
77 |
PlugwiseSensorEntityDescription(
|
78 |
-
key=
|
79 |
-
translation_key=
|
80 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
81 |
device_class=SensorDeviceClass.TEMPERATURE,
|
82 |
entity_category=EntityCategory.DIAGNOSTIC,
|
83 |
state_class=SensorStateClass.MEASUREMENT,
|
84 |
),
|
85 |
PlugwiseSensorEntityDescription(
|
86 |
-
key=
|
87 |
-
translation_key=
|
88 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
89 |
device_class=SensorDeviceClass.TEMPERATURE,
|
90 |
entity_category=EntityCategory.DIAGNOSTIC,
|
91 |
state_class=SensorStateClass.MEASUREMENT,
|
92 |
),
|
93 |
PlugwiseSensorEntityDescription(
|
94 |
-
key=
|
95 |
-
translation_key=
|
96 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
97 |
device_class=SensorDeviceClass.TEMPERATURE,
|
98 |
state_class=SensorStateClass.MEASUREMENT,
|
|
|
99 |
),
|
100 |
PlugwiseSensorEntityDescription(
|
101 |
-
key=
|
102 |
-
translation_key=
|
103 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
104 |
device_class=SensorDeviceClass.TEMPERATURE,
|
105 |
entity_category=EntityCategory.DIAGNOSTIC,
|
106 |
state_class=SensorStateClass.MEASUREMENT,
|
107 |
),
|
108 |
PlugwiseSensorEntityDescription(
|
109 |
-
key=
|
110 |
-
translation_key=
|
111 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
112 |
device_class=SensorDeviceClass.TEMPERATURE,
|
113 |
entity_category=EntityCategory.DIAGNOSTIC,
|
114 |
state_class=SensorStateClass.MEASUREMENT,
|
115 |
),
|
116 |
PlugwiseSensorEntityDescription(
|
117 |
-
key=
|
118 |
-
translation_key=
|
119 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
120 |
device_class=SensorDeviceClass.TEMPERATURE,
|
121 |
entity_category=EntityCategory.DIAGNOSTIC,
|
122 |
state_class=SensorStateClass.MEASUREMENT,
|
123 |
),
|
124 |
PlugwiseSensorEntityDescription(
|
125 |
-
key=
|
126 |
-
translation_key=
|
127 |
native_unit_of_measurement=UnitOfPower.WATT,
|
128 |
device_class=SensorDeviceClass.POWER,
|
129 |
state_class=SensorStateClass.MEASUREMENT,
|
130 |
),
|
131 |
PlugwiseSensorEntityDescription(
|
132 |
-
key=
|
133 |
-
translation_key=
|
134 |
native_unit_of_measurement=UnitOfPower.WATT,
|
135 |
device_class=SensorDeviceClass.POWER,
|
136 |
state_class=SensorStateClass.MEASUREMENT,
|
137 |
entity_registry_enabled_default=False,
|
138 |
),
|
139 |
PlugwiseSensorEntityDescription(
|
140 |
-
key=
|
141 |
-
translation_key=
|
142 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
143 |
device_class=SensorDeviceClass.ENERGY,
|
144 |
state_class=SensorStateClass.TOTAL,
|
145 |
),
|
146 |
PlugwiseSensorEntityDescription(
|
147 |
-
key=
|
148 |
-
translation_key=
|
149 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
150 |
device_class=SensorDeviceClass.ENERGY,
|
151 |
state_class=SensorStateClass.TOTAL,
|
152 |
),
|
153 |
PlugwiseSensorEntityDescription(
|
154 |
-
key=
|
155 |
-
translation_key=
|
156 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
157 |
device_class=SensorDeviceClass.ENERGY,
|
158 |
state_class=SensorStateClass.TOTAL,
|
159 |
),
|
160 |
PlugwiseSensorEntityDescription(
|
161 |
-
key=
|
162 |
-
translation_key=
|
163 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
164 |
device_class=SensorDeviceClass.ENERGY,
|
165 |
state_class=SensorStateClass.TOTAL,
|
166 |
entity_registry_enabled_default=False,
|
167 |
),
|
168 |
PlugwiseSensorEntityDescription(
|
169 |
-
key=
|
170 |
-
translation_key=
|
171 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
172 |
device_class=SensorDeviceClass.ENERGY,
|
173 |
state_class=SensorStateClass.TOTAL,
|
174 |
),
|
175 |
PlugwiseSensorEntityDescription(
|
176 |
-
key=
|
177 |
-
translation_key=
|
178 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
179 |
device_class=SensorDeviceClass.ENERGY,
|
180 |
state_class=SensorStateClass.TOTAL,
|
181 |
),
|
182 |
PlugwiseSensorEntityDescription(
|
183 |
-
key=
|
184 |
-
translation_key=
|
185 |
device_class=SensorDeviceClass.POWER,
|
186 |
native_unit_of_measurement=UnitOfPower.WATT,
|
187 |
state_class=SensorStateClass.MEASUREMENT,
|
188 |
),
|
189 |
PlugwiseSensorEntityDescription(
|
190 |
-
key=
|
191 |
-
translation_key=
|
192 |
native_unit_of_measurement=UnitOfPower.WATT,
|
193 |
device_class=SensorDeviceClass.POWER,
|
194 |
state_class=SensorStateClass.MEASUREMENT,
|
195 |
),
|
196 |
PlugwiseSensorEntityDescription(
|
197 |
-
key=
|
198 |
-
translation_key=
|
199 |
native_unit_of_measurement=UnitOfPower.WATT,
|
200 |
device_class=SensorDeviceClass.POWER,
|
201 |
state_class=SensorStateClass.MEASUREMENT,
|
202 |
),
|
203 |
PlugwiseSensorEntityDescription(
|
204 |
-
key=
|
205 |
-
translation_key=
|
206 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
207 |
device_class=SensorDeviceClass.ENERGY,
|
208 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
209 |
),
|
210 |
PlugwiseSensorEntityDescription(
|
211 |
-
key=
|
212 |
-
translation_key=
|
213 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
214 |
device_class=SensorDeviceClass.ENERGY,
|
215 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
216 |
),
|
217 |
PlugwiseSensorEntityDescription(
|
218 |
-
key=
|
219 |
-
translation_key=
|
220 |
device_class=SensorDeviceClass.POWER,
|
221 |
native_unit_of_measurement=UnitOfPower.WATT,
|
222 |
state_class=SensorStateClass.MEASUREMENT,
|
223 |
),
|
224 |
PlugwiseSensorEntityDescription(
|
225 |
-
key=
|
226 |
-
translation_key=
|
227 |
native_unit_of_measurement=UnitOfPower.WATT,
|
228 |
device_class=SensorDeviceClass.POWER,
|
229 |
state_class=SensorStateClass.MEASUREMENT,
|
230 |
),
|
231 |
PlugwiseSensorEntityDescription(
|
232 |
-
key=
|
233 |
-
translation_key=
|
234 |
native_unit_of_measurement=UnitOfPower.WATT,
|
235 |
device_class=SensorDeviceClass.POWER,
|
236 |
state_class=SensorStateClass.MEASUREMENT,
|
237 |
),
|
238 |
PlugwiseSensorEntityDescription(
|
239 |
-
key=
|
240 |
-
translation_key=
|
241 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
242 |
device_class=SensorDeviceClass.ENERGY,
|
243 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
244 |
),
|
245 |
PlugwiseSensorEntityDescription(
|
246 |
-
key=
|
247 |
-
translation_key=
|
248 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
249 |
device_class=SensorDeviceClass.ENERGY,
|
250 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
251 |
),
|
252 |
PlugwiseSensorEntityDescription(
|
253 |
-
key=
|
254 |
-
translation_key=
|
255 |
device_class=SensorDeviceClass.POWER,
|
256 |
native_unit_of_measurement=UnitOfPower.WATT,
|
257 |
state_class=SensorStateClass.MEASUREMENT,
|
258 |
),
|
259 |
PlugwiseSensorEntityDescription(
|
260 |
-
key=
|
261 |
-
translation_key=
|
262 |
device_class=SensorDeviceClass.POWER,
|
263 |
native_unit_of_measurement=UnitOfPower.WATT,
|
264 |
state_class=SensorStateClass.MEASUREMENT,
|
265 |
),
|
266 |
PlugwiseSensorEntityDescription(
|
267 |
-
key=
|
268 |
-
translation_key=
|
269 |
device_class=SensorDeviceClass.POWER,
|
270 |
native_unit_of_measurement=UnitOfPower.WATT,
|
271 |
state_class=SensorStateClass.MEASUREMENT,
|
272 |
),
|
273 |
PlugwiseSensorEntityDescription(
|
274 |
-
key=
|
275 |
-
translation_key=
|
276 |
device_class=SensorDeviceClass.POWER,
|
277 |
native_unit_of_measurement=UnitOfPower.WATT,
|
278 |
state_class=SensorStateClass.MEASUREMENT,
|
279 |
),
|
280 |
PlugwiseSensorEntityDescription(
|
281 |
-
key=
|
282 |
-
translation_key=
|
283 |
device_class=SensorDeviceClass.POWER,
|
284 |
native_unit_of_measurement=UnitOfPower.WATT,
|
285 |
state_class=SensorStateClass.MEASUREMENT,
|
286 |
),
|
287 |
PlugwiseSensorEntityDescription(
|
288 |
-
key=
|
289 |
-
translation_key=
|
290 |
device_class=SensorDeviceClass.POWER,
|
291 |
native_unit_of_measurement=UnitOfPower.WATT,
|
292 |
state_class=SensorStateClass.MEASUREMENT,
|
293 |
),
|
294 |
PlugwiseSensorEntityDescription(
|
295 |
-
key=
|
296 |
-
translation_key=
|
297 |
device_class=SensorDeviceClass.VOLTAGE,
|
298 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
299 |
state_class=SensorStateClass.MEASUREMENT,
|
300 |
entity_registry_enabled_default=False,
|
301 |
),
|
302 |
PlugwiseSensorEntityDescription(
|
303 |
-
key=
|
304 |
-
translation_key=
|
305 |
device_class=SensorDeviceClass.VOLTAGE,
|
306 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
307 |
state_class=SensorStateClass.MEASUREMENT,
|
308 |
entity_registry_enabled_default=False,
|
309 |
),
|
310 |
PlugwiseSensorEntityDescription(
|
311 |
-
key=
|
312 |
-
translation_key=
|
313 |
device_class=SensorDeviceClass.VOLTAGE,
|
314 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
315 |
state_class=SensorStateClass.MEASUREMENT,
|
316 |
entity_registry_enabled_default=False,
|
317 |
),
|
318 |
PlugwiseSensorEntityDescription(
|
319 |
-
key=
|
320 |
-
translation_key=
|
321 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
322 |
state_class=SensorStateClass.MEASUREMENT,
|
323 |
),
|
324 |
PlugwiseSensorEntityDescription(
|
325 |
-
key=
|
326 |
-
translation_key=
|
327 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
328 |
device_class=SensorDeviceClass.GAS,
|
329 |
state_class=SensorStateClass.TOTAL,
|
330 |
),
|
331 |
PlugwiseSensorEntityDescription(
|
332 |
-
key=
|
333 |
-
translation_key=
|
334 |
native_unit_of_measurement=UnitOfPower.WATT,
|
335 |
device_class=SensorDeviceClass.POWER,
|
336 |
state_class=SensorStateClass.MEASUREMENT,
|
337 |
),
|
338 |
PlugwiseSensorEntityDescription(
|
339 |
-
key=
|
340 |
-
translation_key=
|
341 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
342 |
device_class=SensorDeviceClass.ENERGY,
|
343 |
state_class=SensorStateClass.TOTAL,
|
@@ -357,22 +411,22 @@
|
|
357 |
entity_category=EntityCategory.DIAGNOSTIC,
|
358 |
),
|
359 |
PlugwiseSensorEntityDescription(
|
360 |
-
key=
|
361 |
-
translation_key=
|
362 |
native_unit_of_measurement=PERCENTAGE,
|
363 |
entity_category=EntityCategory.DIAGNOSTIC,
|
364 |
state_class=SensorStateClass.MEASUREMENT,
|
365 |
),
|
366 |
PlugwiseSensorEntityDescription(
|
367 |
-
key=
|
368 |
-
translation_key=
|
369 |
entity_category=EntityCategory.DIAGNOSTIC,
|
370 |
native_unit_of_measurement=PERCENTAGE,
|
371 |
state_class=SensorStateClass.MEASUREMENT,
|
372 |
),
|
373 |
PlugwiseSensorEntityDescription(
|
374 |
-
key=
|
375 |
-
translation_key=
|
376 |
native_unit_of_measurement=UnitOfPressure.BAR,
|
377 |
device_class=SensorDeviceClass.PRESSURE,
|
378 |
entity_category=EntityCategory.DIAGNOSTIC,
|
@@ -385,16 +439,16 @@
|
|
385 |
state_class=SensorStateClass.MEASUREMENT,
|
386 |
),
|
387 |
PlugwiseSensorEntityDescription(
|
388 |
-
key=
|
389 |
-
translation_key=
|
390 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
391 |
device_class=SensorDeviceClass.TEMPERATURE,
|
392 |
entity_category=EntityCategory.DIAGNOSTIC,
|
393 |
state_class=SensorStateClass.MEASUREMENT,
|
394 |
),
|
395 |
PlugwiseSensorEntityDescription(
|
396 |
-
key=
|
397 |
-
translation_key=
|
398 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
399 |
device_class=SensorDeviceClass.TEMPERATURE,
|
400 |
entity_category=EntityCategory.DIAGNOSTIC,
|
@@ -408,7 +462,8 @@
|
|
408 |
entry: PlugwiseConfigEntry,
|
409 |
async_add_entities: AddEntitiesCallback,
|
410 |
) -> None:
|
411 |
-
"""Set up
|
|
|
412 |
coordinator = entry.runtime_data
|
413 |
|
414 |
@callback
|
@@ -417,13 +472,29 @@
|
|
417 |
if not coordinator.new_devices:
|
418 |
return
|
419 |
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
427 |
|
428 |
_add_entities()
|
429 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
@@ -442,10 +513,10 @@
|
|
442 |
) -> None:
|
443 |
"""Initialise the sensor."""
|
444 |
super().__init__(coordinator, device_id)
|
445 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
446 |
self.entity_description = description
|
|
|
447 |
|
448 |
@property
|
449 |
def native_value(self) -> int | float:
|
450 |
"""Return the value reported by the sensor."""
|
451 |
-
return self.device[
|
|
|
13 |
SensorStateClass,
|
14 |
)
|
15 |
from homeassistant.const import (
|
16 |
+
ATTR_TEMPERATURE, # Upstream
|
17 |
LIGHT_LUX,
|
18 |
PERCENTAGE,
|
19 |
EntityCategory,
|
|
|
29 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
30 |
|
31 |
from . import PlugwiseConfigEntry
|
32 |
+
from .const import (
|
33 |
+
DHW_SETPOINT,
|
34 |
+
DHW_TEMP,
|
35 |
+
EL_CONS_INTERVAL,
|
36 |
+
EL_CONS_OP_CUMULATIVE,
|
37 |
+
EL_CONS_OP_INTERVAL,
|
38 |
+
EL_CONS_OP_POINT,
|
39 |
+
EL_CONS_P_CUMULATIVE,
|
40 |
+
EL_CONS_P_INTERVAL,
|
41 |
+
EL_CONS_P_POINT,
|
42 |
+
EL_CONS_POINT,
|
43 |
+
EL_CONSUMED,
|
44 |
+
EL_PH1_CONSUMED,
|
45 |
+
EL_PH1_PRODUCED,
|
46 |
+
EL_PH2_CONSUMED,
|
47 |
+
EL_PH2_PRODUCED,
|
48 |
+
EL_PH3_CONSUMED,
|
49 |
+
EL_PH3_PRODUCED,
|
50 |
+
EL_PROD_INTERVAL,
|
51 |
+
EL_PROD_OP_CUMULATIVE,
|
52 |
+
EL_PROD_OP_INTERVAL,
|
53 |
+
EL_PROD_OP_POINT,
|
54 |
+
EL_PROD_P_CUMULATIVE,
|
55 |
+
EL_PROD_P_INTERVAL,
|
56 |
+
EL_PROD_P_POINT,
|
57 |
+
EL_PROD_POINT,
|
58 |
+
EL_PRODUCED,
|
59 |
+
GAS_CONS_CUMULATIVE,
|
60 |
+
GAS_CONS_INTERVAL,
|
61 |
+
INTENDED_BOILER_TEMP,
|
62 |
+
LOGGER, # pw-beta
|
63 |
+
MOD_LEVEL,
|
64 |
+
NET_EL_CUMULATIVE,
|
65 |
+
NET_EL_POINT,
|
66 |
+
OUTDOOR_AIR_TEMP,
|
67 |
+
OUTDOOR_TEMP,
|
68 |
+
RETURN_TEMP,
|
69 |
+
SENSORS,
|
70 |
+
TARGET_TEMP,
|
71 |
+
TARGET_TEMP_HIGH,
|
72 |
+
TARGET_TEMP_LOW,
|
73 |
+
TEMP_DIFF,
|
74 |
+
VALVE_POS,
|
75 |
+
VOLTAGE_PH1,
|
76 |
+
VOLTAGE_PH2,
|
77 |
+
VOLTAGE_PH3,
|
78 |
+
WATER_PRESSURE,
|
79 |
+
WATER_TEMP,
|
80 |
+
)
|
81 |
+
|
82 |
+
# Upstream consts
|
83 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
84 |
from .entity import PlugwiseEntity
|
85 |
|
|
|
94 |
key: SensorType
|
95 |
|
96 |
|
97 |
+
# Upstream consts
|
98 |
+
PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
|
99 |
PlugwiseSensorEntityDescription(
|
100 |
+
key=TARGET_TEMP,
|
101 |
+
translation_key=TARGET_TEMP,
|
102 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
103 |
device_class=SensorDeviceClass.TEMPERATURE,
|
104 |
state_class=SensorStateClass.MEASUREMENT,
|
105 |
entity_category=EntityCategory.DIAGNOSTIC,
|
106 |
),
|
107 |
PlugwiseSensorEntityDescription(
|
108 |
+
key=TARGET_TEMP_HIGH,
|
109 |
translation_key="cooling_setpoint",
|
110 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
111 |
device_class=SensorDeviceClass.TEMPERATURE,
|
|
|
113 |
entity_category=EntityCategory.DIAGNOSTIC,
|
114 |
),
|
115 |
PlugwiseSensorEntityDescription(
|
116 |
+
key=TARGET_TEMP_LOW,
|
117 |
translation_key="heating_setpoint",
|
118 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
119 |
device_class=SensorDeviceClass.TEMPERATURE,
|
|
|
121 |
entity_category=EntityCategory.DIAGNOSTIC,
|
122 |
),
|
123 |
PlugwiseSensorEntityDescription(
|
124 |
+
key=ATTR_TEMPERATURE,
|
125 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
126 |
device_class=SensorDeviceClass.TEMPERATURE,
|
127 |
entity_category=EntityCategory.DIAGNOSTIC,
|
128 |
state_class=SensorStateClass.MEASUREMENT,
|
129 |
),
|
130 |
PlugwiseSensorEntityDescription(
|
131 |
+
key=INTENDED_BOILER_TEMP,
|
132 |
+
translation_key=INTENDED_BOILER_TEMP,
|
133 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
134 |
device_class=SensorDeviceClass.TEMPERATURE,
|
135 |
entity_category=EntityCategory.DIAGNOSTIC,
|
136 |
state_class=SensorStateClass.MEASUREMENT,
|
137 |
),
|
138 |
PlugwiseSensorEntityDescription(
|
139 |
+
key=TEMP_DIFF,
|
140 |
+
translation_key=TEMP_DIFF,
|
141 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
142 |
device_class=SensorDeviceClass.TEMPERATURE,
|
143 |
entity_category=EntityCategory.DIAGNOSTIC,
|
144 |
state_class=SensorStateClass.MEASUREMENT,
|
145 |
),
|
146 |
PlugwiseSensorEntityDescription(
|
147 |
+
key=OUTDOOR_TEMP,
|
148 |
+
translation_key=OUTDOOR_TEMP,
|
149 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
150 |
device_class=SensorDeviceClass.TEMPERATURE,
|
151 |
state_class=SensorStateClass.MEASUREMENT,
|
152 |
+
suggested_display_precision=1,
|
153 |
),
|
154 |
PlugwiseSensorEntityDescription(
|
155 |
+
key=OUTDOOR_AIR_TEMP,
|
156 |
+
translation_key=OUTDOOR_AIR_TEMP,
|
157 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
158 |
device_class=SensorDeviceClass.TEMPERATURE,
|
159 |
entity_category=EntityCategory.DIAGNOSTIC,
|
160 |
state_class=SensorStateClass.MEASUREMENT,
|
161 |
),
|
162 |
PlugwiseSensorEntityDescription(
|
163 |
+
key=WATER_TEMP,
|
164 |
+
translation_key=WATER_TEMP,
|
165 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
166 |
device_class=SensorDeviceClass.TEMPERATURE,
|
167 |
entity_category=EntityCategory.DIAGNOSTIC,
|
168 |
state_class=SensorStateClass.MEASUREMENT,
|
169 |
),
|
170 |
PlugwiseSensorEntityDescription(
|
171 |
+
key=RETURN_TEMP,
|
172 |
+
translation_key=RETURN_TEMP,
|
173 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
174 |
device_class=SensorDeviceClass.TEMPERATURE,
|
175 |
entity_category=EntityCategory.DIAGNOSTIC,
|
176 |
state_class=SensorStateClass.MEASUREMENT,
|
177 |
),
|
178 |
PlugwiseSensorEntityDescription(
|
179 |
+
key=EL_CONSUMED,
|
180 |
+
translation_key=EL_CONSUMED,
|
181 |
native_unit_of_measurement=UnitOfPower.WATT,
|
182 |
device_class=SensorDeviceClass.POWER,
|
183 |
state_class=SensorStateClass.MEASUREMENT,
|
184 |
),
|
185 |
PlugwiseSensorEntityDescription(
|
186 |
+
key=EL_PRODUCED,
|
187 |
+
translation_key=EL_PRODUCED,
|
188 |
native_unit_of_measurement=UnitOfPower.WATT,
|
189 |
device_class=SensorDeviceClass.POWER,
|
190 |
state_class=SensorStateClass.MEASUREMENT,
|
191 |
entity_registry_enabled_default=False,
|
192 |
),
|
193 |
PlugwiseSensorEntityDescription(
|
194 |
+
key=EL_CONS_INTERVAL,
|
195 |
+
translation_key=EL_CONS_INTERVAL,
|
196 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
197 |
device_class=SensorDeviceClass.ENERGY,
|
198 |
state_class=SensorStateClass.TOTAL,
|
199 |
),
|
200 |
PlugwiseSensorEntityDescription(
|
201 |
+
key=EL_CONS_P_INTERVAL,
|
202 |
+
translation_key=EL_CONS_P_INTERVAL,
|
203 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
204 |
device_class=SensorDeviceClass.ENERGY,
|
205 |
state_class=SensorStateClass.TOTAL,
|
206 |
),
|
207 |
PlugwiseSensorEntityDescription(
|
208 |
+
key=EL_CONS_OP_INTERVAL,
|
209 |
+
translation_key=EL_CONS_OP_INTERVAL,
|
210 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
211 |
device_class=SensorDeviceClass.ENERGY,
|
212 |
state_class=SensorStateClass.TOTAL,
|
213 |
),
|
214 |
PlugwiseSensorEntityDescription(
|
215 |
+
key=EL_PROD_INTERVAL,
|
216 |
+
translation_key=EL_PROD_INTERVAL,
|
217 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
218 |
device_class=SensorDeviceClass.ENERGY,
|
219 |
state_class=SensorStateClass.TOTAL,
|
220 |
entity_registry_enabled_default=False,
|
221 |
),
|
222 |
PlugwiseSensorEntityDescription(
|
223 |
+
key=EL_PROD_P_INTERVAL,
|
224 |
+
translation_key=EL_PROD_P_INTERVAL,
|
225 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
226 |
device_class=SensorDeviceClass.ENERGY,
|
227 |
state_class=SensorStateClass.TOTAL,
|
228 |
),
|
229 |
PlugwiseSensorEntityDescription(
|
230 |
+
key=EL_PROD_OP_INTERVAL,
|
231 |
+
translation_key=EL_PROD_OP_INTERVAL,
|
232 |
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
233 |
device_class=SensorDeviceClass.ENERGY,
|
234 |
state_class=SensorStateClass.TOTAL,
|
235 |
),
|
236 |
PlugwiseSensorEntityDescription(
|
237 |
+
key=EL_CONS_POINT,
|
238 |
+
translation_key=EL_CONS_POINT,
|
239 |
device_class=SensorDeviceClass.POWER,
|
240 |
native_unit_of_measurement=UnitOfPower.WATT,
|
241 |
state_class=SensorStateClass.MEASUREMENT,
|
242 |
),
|
243 |
PlugwiseSensorEntityDescription(
|
244 |
+
key=EL_CONS_OP_POINT,
|
245 |
+
translation_key=EL_CONS_OP_POINT,
|
246 |
native_unit_of_measurement=UnitOfPower.WATT,
|
247 |
device_class=SensorDeviceClass.POWER,
|
248 |
state_class=SensorStateClass.MEASUREMENT,
|
249 |
),
|
250 |
PlugwiseSensorEntityDescription(
|
251 |
+
key=EL_CONS_P_POINT,
|
252 |
+
translation_key=EL_CONS_P_POINT,
|
253 |
native_unit_of_measurement=UnitOfPower.WATT,
|
254 |
device_class=SensorDeviceClass.POWER,
|
255 |
state_class=SensorStateClass.MEASUREMENT,
|
256 |
),
|
257 |
PlugwiseSensorEntityDescription(
|
258 |
+
key=EL_CONS_OP_CUMULATIVE,
|
259 |
+
translation_key=EL_CONS_OP_CUMULATIVE,
|
260 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
261 |
device_class=SensorDeviceClass.ENERGY,
|
262 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
263 |
),
|
264 |
PlugwiseSensorEntityDescription(
|
265 |
+
key=EL_CONS_P_CUMULATIVE,
|
266 |
+
translation_key=EL_CONS_P_CUMULATIVE,
|
267 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
268 |
device_class=SensorDeviceClass.ENERGY,
|
269 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
270 |
),
|
271 |
PlugwiseSensorEntityDescription(
|
272 |
+
key=EL_PROD_POINT,
|
273 |
+
translation_key=EL_PROD_POINT,
|
274 |
device_class=SensorDeviceClass.POWER,
|
275 |
native_unit_of_measurement=UnitOfPower.WATT,
|
276 |
state_class=SensorStateClass.MEASUREMENT,
|
277 |
),
|
278 |
PlugwiseSensorEntityDescription(
|
279 |
+
key=EL_PROD_OP_POINT,
|
280 |
+
translation_key=EL_PROD_OP_POINT,
|
281 |
native_unit_of_measurement=UnitOfPower.WATT,
|
282 |
device_class=SensorDeviceClass.POWER,
|
283 |
state_class=SensorStateClass.MEASUREMENT,
|
284 |
),
|
285 |
PlugwiseSensorEntityDescription(
|
286 |
+
key=EL_PROD_P_POINT,
|
287 |
+
translation_key=EL_PROD_P_POINT,
|
288 |
native_unit_of_measurement=UnitOfPower.WATT,
|
289 |
device_class=SensorDeviceClass.POWER,
|
290 |
state_class=SensorStateClass.MEASUREMENT,
|
291 |
),
|
292 |
PlugwiseSensorEntityDescription(
|
293 |
+
key=EL_PROD_OP_CUMULATIVE,
|
294 |
+
translation_key=EL_PROD_OP_CUMULATIVE,
|
295 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
296 |
device_class=SensorDeviceClass.ENERGY,
|
297 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
298 |
),
|
299 |
PlugwiseSensorEntityDescription(
|
300 |
+
key=EL_PROD_P_CUMULATIVE,
|
301 |
+
translation_key=EL_PROD_P_CUMULATIVE,
|
302 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
303 |
device_class=SensorDeviceClass.ENERGY,
|
304 |
state_class=SensorStateClass.TOTAL_INCREASING,
|
305 |
),
|
306 |
PlugwiseSensorEntityDescription(
|
307 |
+
key=EL_PH1_CONSUMED,
|
308 |
+
translation_key=EL_PH1_CONSUMED,
|
309 |
device_class=SensorDeviceClass.POWER,
|
310 |
native_unit_of_measurement=UnitOfPower.WATT,
|
311 |
state_class=SensorStateClass.MEASUREMENT,
|
312 |
),
|
313 |
PlugwiseSensorEntityDescription(
|
314 |
+
key=EL_PH2_CONSUMED,
|
315 |
+
translation_key=EL_PH2_CONSUMED,
|
316 |
device_class=SensorDeviceClass.POWER,
|
317 |
native_unit_of_measurement=UnitOfPower.WATT,
|
318 |
state_class=SensorStateClass.MEASUREMENT,
|
319 |
),
|
320 |
PlugwiseSensorEntityDescription(
|
321 |
+
key=EL_PH3_CONSUMED,
|
322 |
+
translation_key=EL_PH3_CONSUMED,
|
323 |
device_class=SensorDeviceClass.POWER,
|
324 |
native_unit_of_measurement=UnitOfPower.WATT,
|
325 |
state_class=SensorStateClass.MEASUREMENT,
|
326 |
),
|
327 |
PlugwiseSensorEntityDescription(
|
328 |
+
key=EL_PH1_PRODUCED,
|
329 |
+
translation_key=EL_PH1_PRODUCED,
|
330 |
device_class=SensorDeviceClass.POWER,
|
331 |
native_unit_of_measurement=UnitOfPower.WATT,
|
332 |
state_class=SensorStateClass.MEASUREMENT,
|
333 |
),
|
334 |
PlugwiseSensorEntityDescription(
|
335 |
+
key=EL_PH2_PRODUCED,
|
336 |
+
translation_key=EL_PH2_PRODUCED,
|
337 |
device_class=SensorDeviceClass.POWER,
|
338 |
native_unit_of_measurement=UnitOfPower.WATT,
|
339 |
state_class=SensorStateClass.MEASUREMENT,
|
340 |
),
|
341 |
PlugwiseSensorEntityDescription(
|
342 |
+
key=EL_PH3_PRODUCED,
|
343 |
+
translation_key=EL_PH3_PRODUCED,
|
344 |
device_class=SensorDeviceClass.POWER,
|
345 |
native_unit_of_measurement=UnitOfPower.WATT,
|
346 |
state_class=SensorStateClass.MEASUREMENT,
|
347 |
),
|
348 |
PlugwiseSensorEntityDescription(
|
349 |
+
key=VOLTAGE_PH1,
|
350 |
+
translation_key=VOLTAGE_PH1,
|
351 |
device_class=SensorDeviceClass.VOLTAGE,
|
352 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
353 |
state_class=SensorStateClass.MEASUREMENT,
|
354 |
entity_registry_enabled_default=False,
|
355 |
),
|
356 |
PlugwiseSensorEntityDescription(
|
357 |
+
key=VOLTAGE_PH2,
|
358 |
+
translation_key=VOLTAGE_PH2,
|
359 |
device_class=SensorDeviceClass.VOLTAGE,
|
360 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
361 |
state_class=SensorStateClass.MEASUREMENT,
|
362 |
entity_registry_enabled_default=False,
|
363 |
),
|
364 |
PlugwiseSensorEntityDescription(
|
365 |
+
key=VOLTAGE_PH3,
|
366 |
+
translation_key=VOLTAGE_PH3,
|
367 |
device_class=SensorDeviceClass.VOLTAGE,
|
368 |
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
369 |
state_class=SensorStateClass.MEASUREMENT,
|
370 |
entity_registry_enabled_default=False,
|
371 |
),
|
372 |
PlugwiseSensorEntityDescription(
|
373 |
+
key=GAS_CONS_INTERVAL,
|
374 |
+
translation_key=GAS_CONS_INTERVAL,
|
375 |
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
376 |
state_class=SensorStateClass.MEASUREMENT,
|
377 |
),
|
378 |
PlugwiseSensorEntityDescription(
|
379 |
+
key=GAS_CONS_CUMULATIVE,
|
380 |
+
translation_key=GAS_CONS_CUMULATIVE,
|
381 |
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
382 |
device_class=SensorDeviceClass.GAS,
|
383 |
state_class=SensorStateClass.TOTAL,
|
384 |
),
|
385 |
PlugwiseSensorEntityDescription(
|
386 |
+
key=NET_EL_POINT,
|
387 |
+
translation_key=NET_EL_POINT,
|
388 |
native_unit_of_measurement=UnitOfPower.WATT,
|
389 |
device_class=SensorDeviceClass.POWER,
|
390 |
state_class=SensorStateClass.MEASUREMENT,
|
391 |
),
|
392 |
PlugwiseSensorEntityDescription(
|
393 |
+
key=NET_EL_CUMULATIVE,
|
394 |
+
translation_key=NET_EL_CUMULATIVE,
|
395 |
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
396 |
device_class=SensorDeviceClass.ENERGY,
|
397 |
state_class=SensorStateClass.TOTAL,
|
|
|
411 |
entity_category=EntityCategory.DIAGNOSTIC,
|
412 |
),
|
413 |
PlugwiseSensorEntityDescription(
|
414 |
+
key=MOD_LEVEL,
|
415 |
+
translation_key=MOD_LEVEL,
|
416 |
native_unit_of_measurement=PERCENTAGE,
|
417 |
entity_category=EntityCategory.DIAGNOSTIC,
|
418 |
state_class=SensorStateClass.MEASUREMENT,
|
419 |
),
|
420 |
PlugwiseSensorEntityDescription(
|
421 |
+
key=VALVE_POS,
|
422 |
+
translation_key=VALVE_POS,
|
423 |
entity_category=EntityCategory.DIAGNOSTIC,
|
424 |
native_unit_of_measurement=PERCENTAGE,
|
425 |
state_class=SensorStateClass.MEASUREMENT,
|
426 |
),
|
427 |
PlugwiseSensorEntityDescription(
|
428 |
+
key=WATER_PRESSURE,
|
429 |
+
translation_key=WATER_PRESSURE,
|
430 |
native_unit_of_measurement=UnitOfPressure.BAR,
|
431 |
device_class=SensorDeviceClass.PRESSURE,
|
432 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
|
439 |
state_class=SensorStateClass.MEASUREMENT,
|
440 |
),
|
441 |
PlugwiseSensorEntityDescription(
|
442 |
+
key=DHW_TEMP,
|
443 |
+
translation_key=DHW_TEMP,
|
444 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
445 |
device_class=SensorDeviceClass.TEMPERATURE,
|
446 |
entity_category=EntityCategory.DIAGNOSTIC,
|
447 |
state_class=SensorStateClass.MEASUREMENT,
|
448 |
),
|
449 |
PlugwiseSensorEntityDescription(
|
450 |
+
key=DHW_SETPOINT,
|
451 |
+
translation_key=DHW_SETPOINT,
|
452 |
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
453 |
device_class=SensorDeviceClass.TEMPERATURE,
|
454 |
entity_category=EntityCategory.DIAGNOSTIC,
|
|
|
462 |
entry: PlugwiseConfigEntry,
|
463 |
async_add_entities: AddEntitiesCallback,
|
464 |
) -> None:
|
465 |
+
"""Set up Plugwise sensors from a config entry."""
|
466 |
+
# Upstream as Plugwise not Smile
|
467 |
coordinator = entry.runtime_data
|
468 |
|
469 |
@callback
|
|
|
472 |
if not coordinator.new_devices:
|
473 |
return
|
474 |
|
475 |
+
# Upstream consts
|
476 |
+
# async_add_entities(
|
477 |
+
# PlugwiseSensorEntity(coordinator, device_id, description)
|
478 |
+
# for device_id in coordinator.new_devices
|
479 |
+
# if (sensors := coordinator.data.devices[device_id].get(SENSORS))
|
480 |
+
# for description in PLUGWISE_SENSORS
|
481 |
+
# if description.key in sensors
|
482 |
+
# )
|
483 |
+
# pw-beta alternative for debugging
|
484 |
+
entities: list[PlugwiseSensorEntity] = []
|
485 |
+
for device_id in coordinator.new_devices:
|
486 |
+
device = coordinator.data.devices[device_id]
|
487 |
+
if not (sensors := device.get(SENSORS)):
|
488 |
+
continue
|
489 |
+
for description in PLUGWISE_SENSORS:
|
490 |
+
if description.key not in sensors:
|
491 |
+
continue
|
492 |
+
entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
|
493 |
+
LOGGER.debug(
|
494 |
+
"Add %s %s sensor", device["name"], description.translation_key or description.key
|
495 |
+
)
|
496 |
+
|
497 |
+
async_add_entities(entities)
|
498 |
|
499 |
_add_entities()
|
500 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
513 |
) -> None:
|
514 |
"""Initialise the sensor."""
|
515 |
super().__init__(coordinator, device_id)
|
|
|
516 |
self.entity_description = description
|
517 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
518 |
|
519 |
@property
|
520 |
def native_value(self) -> int | float:
|
521 |
"""Return the value reported by the sensor."""
|
522 |
+
return self.device[SENSORS][self.entity_description.key] # Upstream consts
|
@@ -1,47 +1,57 @@
|
|
1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
"config": {
|
3 |
"step": {
|
4 |
"reconfigure": {
|
5 |
"description": "Update configuration for {title}.",
|
6 |
"data": {
|
7 |
-
"host": "
|
8 |
-
"port": "
|
9 |
-
},
|
10 |
-
"data_description": {
|
11 |
-
"host": "[%key:component::plugwise::config::step::user::data_description::host%]",
|
12 |
-
"port": "[%key:component::plugwise::config::step::user::data_description::port%]"
|
13 |
}
|
14 |
},
|
15 |
"user": {
|
16 |
-
"title": "
|
17 |
-
"description": "
|
18 |
"data": {
|
19 |
-
"
|
20 |
-
"
|
21 |
-
"
|
22 |
-
"
|
23 |
-
},
|
24 |
-
"data_description": {
|
25 |
-
"password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
|
26 |
-
"host": "The hostname or IP-address of your Smile. You can find it in your router or the Plugwise App.",
|
27 |
-
"port": "By default your Smile uses port 80, normally you should not have to change this.",
|
28 |
-
"username": "Default is `smile`, or `stretch` for the legacy Stretch."
|
29 |
}
|
30 |
}
|
31 |
},
|
32 |
"error": {
|
33 |
-
"cannot_connect": "
|
34 |
-
"invalid_auth": "
|
35 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
|
|
|
|
36 |
"response_error": "Invalid XML data, or error indication received",
|
37 |
-
"
|
|
|
38 |
"unsupported": "Device with unsupported firmware"
|
39 |
},
|
40 |
"abort": {
|
41 |
-
"already_configured": "
|
42 |
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
|
43 |
"not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
|
44 |
-
"reconfigure_successful": "
|
45 |
}
|
46 |
},
|
47 |
"entity": {
|
@@ -59,10 +69,10 @@
|
|
59 |
"name": "Flame state"
|
60 |
},
|
61 |
"heating_state": {
|
62 |
-
"name": "
|
63 |
},
|
64 |
"cooling_state": {
|
65 |
-
"name": "
|
66 |
},
|
67 |
"secondary_boiler_state": {
|
68 |
"name": "Secondary boiler state"
|
@@ -79,20 +89,14 @@
|
|
79 |
"climate": {
|
80 |
"plugwise": {
|
81 |
"state_attributes": {
|
82 |
-
"available_schemas": {
|
83 |
-
"name": "Available schemas"
|
84 |
-
},
|
85 |
"preset_mode": {
|
86 |
"state": {
|
87 |
"asleep": "Night",
|
88 |
-
"away": "
|
89 |
-
"home": "
|
90 |
"no_frost": "Anti-frost",
|
91 |
"vacation": "Vacation"
|
92 |
}
|
93 |
-
},
|
94 |
-
"selected_schema": {
|
95 |
-
"name": "Selected schema"
|
96 |
}
|
97 |
}
|
98 |
}
|
@@ -112,10 +116,20 @@
|
|
112 |
"dhw_mode": {
|
113 |
"name": "DHW mode",
|
114 |
"state": {
|
115 |
-
"off": "[%key:common::state::off%]",
|
116 |
"auto": "Auto",
|
117 |
-
"boost": "
|
118 |
-
"comfort": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
}
|
120 |
},
|
121 |
"gateway_mode": {
|
@@ -126,16 +140,6 @@
|
|
126 |
"vacation": "Vacation"
|
127 |
}
|
128 |
},
|
129 |
-
"regulation_mode": {
|
130 |
-
"name": "Regulation mode",
|
131 |
-
"state": {
|
132 |
-
"bleeding_cold": "Bleeding cold",
|
133 |
-
"bleeding_hot": "Bleeding hot",
|
134 |
-
"cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
|
135 |
-
"heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
|
136 |
-
"off": "[%key:common::state::off%]"
|
137 |
-
}
|
138 |
-
},
|
139 |
"select_schedule": {
|
140 |
"name": "Thermostat schedule",
|
141 |
"state": {
|
@@ -177,6 +181,12 @@
|
|
177 |
"electricity_produced": {
|
178 |
"name": "Electricity produced"
|
179 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
"electricity_consumed_interval": {
|
181 |
"name": "Electricity consumed interval"
|
182 |
},
|
@@ -195,9 +205,6 @@
|
|
195 |
"electricity_produced_off_peak_interval": {
|
196 |
"name": "Electricity produced off peak interval"
|
197 |
},
|
198 |
-
"electricity_consumed_point": {
|
199 |
-
"name": "Electricity consumed point"
|
200 |
-
},
|
201 |
"electricity_consumed_off_peak_point": {
|
202 |
"name": "Electricity consumed off peak point"
|
203 |
},
|
@@ -210,9 +217,6 @@
|
|
210 |
"electricity_consumed_peak_cumulative": {
|
211 |
"name": "Electricity consumed peak cumulative"
|
212 |
},
|
213 |
-
"electricity_produced_point": {
|
214 |
-
"name": "Electricity produced point"
|
215 |
-
},
|
216 |
"electricity_produced_off_peak_point": {
|
217 |
"name": "Electricity produced off peak point"
|
218 |
},
|
@@ -280,18 +284,18 @@
|
|
280 |
"name": "DHW setpoint"
|
281 |
},
|
282 |
"maximum_boiler_temperature": {
|
283 |
-
"name": "Maximum boiler temperature"
|
284 |
}
|
285 |
},
|
286 |
"switch": {
|
287 |
"cooling_ena_switch": {
|
288 |
-
"name": "
|
289 |
},
|
290 |
"dhw_cm_switch": {
|
291 |
"name": "DHW comfort mode"
|
292 |
},
|
293 |
"lock": {
|
294 |
-
"name": "
|
295 |
},
|
296 |
"relay": {
|
297 |
"name": "Relay"
|
@@ -300,7 +304,7 @@
|
|
300 |
},
|
301 |
"exceptions": {
|
302 |
"authentication_failed": {
|
303 |
-
"message": "
|
304 |
},
|
305 |
"data_incomplete_or_missing": {
|
306 |
"message": "Data incomplete or missing."
|
@@ -309,16 +313,19 @@
|
|
309 |
"message": "Error communicating with API: {error}."
|
310 |
},
|
311 |
"failed_to_connect": {
|
312 |
-
"message": "
|
313 |
},
|
314 |
"invalid_xml_data": {
|
315 |
-
"message": "
|
316 |
},
|
317 |
"unsupported_firmware": {
|
318 |
-
"message": "
|
319 |
-
}
|
320 |
-
|
321 |
-
|
|
|
|
|
|
|
322 |
}
|
323 |
}
|
324 |
}
|
|
|
1 |
{
|
2 |
+
"options": {
|
3 |
+
"step": {
|
4 |
+
"none": {
|
5 |
+
"title": "No Options available",
|
6 |
+
"description": "This Integration does not provide any Options"
|
7 |
+
},
|
8 |
+
"init": {
|
9 |
+
"description": "Adjust Smile/Stretch Options",
|
10 |
+
"data": {
|
11 |
+
"cooling_on": "Anna: cooling-mode is on",
|
12 |
+
"scan_interval": "Scan Interval (seconds) *) beta-only option",
|
13 |
+
"homekit_emulation": "Homekit emulation (i.e. on hvac_off => Away) *) beta-only option",
|
14 |
+
"refresh_interval": "Frontend refresh-time (1.5 - 5 seconds) *) beta-only option"
|
15 |
+
}
|
16 |
+
}
|
17 |
+
}
|
18 |
+
},
|
19 |
"config": {
|
20 |
"step": {
|
21 |
"reconfigure": {
|
22 |
"description": "Update configuration for {title}.",
|
23 |
"data": {
|
24 |
+
"host": "IP-address",
|
25 |
+
"port": "Port number"
|
|
|
|
|
|
|
|
|
26 |
}
|
27 |
},
|
28 |
"user": {
|
29 |
+
"title": "Set up Plugwise Adam/Smile/Stretch",
|
30 |
+
"description": "Enter your Plugwise device: (setup can take up to 90s)",
|
31 |
"data": {
|
32 |
+
"password": "ID",
|
33 |
+
"username": "Username",
|
34 |
+
"host": "IP-address",
|
35 |
+
"port": "Port number"
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
}
|
37 |
}
|
38 |
},
|
39 |
"error": {
|
40 |
+
"cannot_connect": "Failed to connect",
|
41 |
+
"invalid_auth": "Authentication failed",
|
42 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
43 |
+
"network_down": "Plugwise Zigbee network is down",
|
44 |
+
"network_timeout": "Network communication timeout",
|
45 |
"response_error": "Invalid XML data, or error indication received",
|
46 |
+
"stick_init": "Initialization of Plugwise USB-stick failed",
|
47 |
+
"unknown": "Unknown error!",
|
48 |
"unsupported": "Device with unsupported firmware"
|
49 |
},
|
50 |
"abort": {
|
51 |
+
"already_configured": "This device is already configured",
|
52 |
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
|
53 |
"not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
|
54 |
+
"reconfigure_successful": "Reconfiguration successful"
|
55 |
}
|
56 |
},
|
57 |
"entity": {
|
|
|
69 |
"name": "Flame state"
|
70 |
},
|
71 |
"heating_state": {
|
72 |
+
"name": "Heating"
|
73 |
},
|
74 |
"cooling_state": {
|
75 |
+
"name": "Cooling"
|
76 |
},
|
77 |
"secondary_boiler_state": {
|
78 |
"name": "Secondary boiler state"
|
|
|
89 |
"climate": {
|
90 |
"plugwise": {
|
91 |
"state_attributes": {
|
|
|
|
|
|
|
92 |
"preset_mode": {
|
93 |
"state": {
|
94 |
"asleep": "Night",
|
95 |
+
"away": "Away",
|
96 |
+
"home": "Home",
|
97 |
"no_frost": "Anti-frost",
|
98 |
"vacation": "Vacation"
|
99 |
}
|
|
|
|
|
|
|
100 |
}
|
101 |
}
|
102 |
}
|
|
|
116 |
"dhw_mode": {
|
117 |
"name": "DHW mode",
|
118 |
"state": {
|
|
|
119 |
"auto": "Auto",
|
120 |
+
"boost": "Boost",
|
121 |
+
"comfort": "Comfort",
|
122 |
+
"off": "Off"
|
123 |
+
}
|
124 |
+
},
|
125 |
+
"regulation_mode": {
|
126 |
+
"name": "Regulation mode",
|
127 |
+
"state": {
|
128 |
+
"bleeding_cold": "Bleeding cold",
|
129 |
+
"bleeding_hot": "Bleeding hot",
|
130 |
+
"cooling": "Cooling",
|
131 |
+
"heating": "Heating",
|
132 |
+
"off": "Off"
|
133 |
}
|
134 |
},
|
135 |
"gateway_mode": {
|
|
|
140 |
"vacation": "Vacation"
|
141 |
}
|
142 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
"select_schedule": {
|
144 |
"name": "Thermostat schedule",
|
145 |
"state": {
|
|
|
181 |
"electricity_produced": {
|
182 |
"name": "Electricity produced"
|
183 |
},
|
184 |
+
"electricity_consumed_point": {
|
185 |
+
"name": "Electricity consumed point"
|
186 |
+
},
|
187 |
+
"electricity_produced_point": {
|
188 |
+
"name": "Electricity produced point"
|
189 |
+
},
|
190 |
"electricity_consumed_interval": {
|
191 |
"name": "Electricity consumed interval"
|
192 |
},
|
|
|
205 |
"electricity_produced_off_peak_interval": {
|
206 |
"name": "Electricity produced off peak interval"
|
207 |
},
|
|
|
|
|
|
|
208 |
"electricity_consumed_off_peak_point": {
|
209 |
"name": "Electricity consumed off peak point"
|
210 |
},
|
|
|
217 |
"electricity_consumed_peak_cumulative": {
|
218 |
"name": "Electricity consumed peak cumulative"
|
219 |
},
|
|
|
|
|
|
|
220 |
"electricity_produced_off_peak_point": {
|
221 |
"name": "Electricity produced off peak point"
|
222 |
},
|
|
|
284 |
"name": "DHW setpoint"
|
285 |
},
|
286 |
"maximum_boiler_temperature": {
|
287 |
+
"name": "Maximum boiler temperature setpoint"
|
288 |
}
|
289 |
},
|
290 |
"switch": {
|
291 |
"cooling_ena_switch": {
|
292 |
+
"name": "Cooling"
|
293 |
},
|
294 |
"dhw_cm_switch": {
|
295 |
"name": "DHW comfort mode"
|
296 |
},
|
297 |
"lock": {
|
298 |
+
"name": "Lock"
|
299 |
},
|
300 |
"relay": {
|
301 |
"name": "Relay"
|
|
|
304 |
},
|
305 |
"exceptions": {
|
306 |
"authentication_failed": {
|
307 |
+
"message": "Invalid authentication"
|
308 |
},
|
309 |
"data_incomplete_or_missing": {
|
310 |
"message": "Data incomplete or missing."
|
|
|
313 |
"message": "Error communicating with API: {error}."
|
314 |
},
|
315 |
"failed_to_connect": {
|
316 |
+
"message": "Failed to connect"
|
317 |
},
|
318 |
"invalid_xml_data": {
|
319 |
+
"message": "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
|
320 |
},
|
321 |
"unsupported_firmware": {
|
322 |
+
"message": "Device with unsupported firmware"
|
323 |
+
}
|
324 |
+
},
|
325 |
+
"services": {
|
326 |
+
"delete_notification": {
|
327 |
+
"name": "Delete Plugwise notification",
|
328 |
+
"description": "Deletes a Plugwise Notification"
|
329 |
}
|
330 |
}
|
331 |
}
|
@@ -17,6 +17,17 @@
|
|
17 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
18 |
|
19 |
from . import PlugwiseConfigEntry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
21 |
from .entity import PlugwiseEntity
|
22 |
from .util import plugwise_command
|
@@ -31,25 +42,29 @@
|
|
31 |
key: SwitchType
|
32 |
|
33 |
|
34 |
-
|
|
|
35 |
PlugwiseSwitchEntityDescription(
|
36 |
-
key=
|
37 |
-
translation_key=
|
|
|
38 |
entity_category=EntityCategory.CONFIG,
|
39 |
),
|
40 |
PlugwiseSwitchEntityDescription(
|
41 |
-
key=
|
42 |
-
translation_key=
|
|
|
43 |
entity_category=EntityCategory.CONFIG,
|
44 |
),
|
45 |
PlugwiseSwitchEntityDescription(
|
46 |
-
key=
|
47 |
-
translation_key=
|
48 |
device_class=SwitchDeviceClass.SWITCH,
|
49 |
),
|
50 |
PlugwiseSwitchEntityDescription(
|
51 |
-
key=
|
52 |
-
translation_key=
|
|
|
53 |
entity_category=EntityCategory.CONFIG,
|
54 |
),
|
55 |
)
|
@@ -60,7 +75,7 @@
|
|
60 |
entry: PlugwiseConfigEntry,
|
61 |
async_add_entities: AddEntitiesCallback,
|
62 |
) -> None:
|
63 |
-
"""Set up
|
64 |
coordinator = entry.runtime_data
|
65 |
|
66 |
@callback
|
@@ -69,13 +84,28 @@
|
|
69 |
if not coordinator.new_devices:
|
70 |
return
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
_add_entities()
|
81 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
@@ -94,30 +124,30 @@
|
|
94 |
) -> None:
|
95 |
"""Set up the Plugwise API."""
|
96 |
super().__init__(coordinator, device_id)
|
97 |
-
self._attr_unique_id = f"{device_id}-{description.key}"
|
98 |
self.entity_description = description
|
|
|
99 |
|
100 |
@property
|
101 |
def is_on(self) -> bool:
|
102 |
"""Return True if entity is on."""
|
103 |
-
return self.device[
|
104 |
|
105 |
@plugwise_command
|
106 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
107 |
"""Turn the device on."""
|
108 |
await self.coordinator.api.set_switch_state(
|
109 |
self._dev_id,
|
110 |
-
self.device.get(
|
111 |
self.entity_description.key,
|
112 |
"on",
|
113 |
-
)
|
114 |
|
115 |
@plugwise_command
|
116 |
async def async_turn_off(self, **kwargs: Any) -> None:
|
117 |
"""Turn the device off."""
|
118 |
await self.coordinator.api.set_switch_state(
|
119 |
self._dev_id,
|
120 |
-
self.device.get(
|
121 |
self.entity_description.key,
|
122 |
"off",
|
123 |
-
)
|
|
|
17 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
18 |
|
19 |
from . import PlugwiseConfigEntry
|
20 |
+
from .const import (
|
21 |
+
COOLING_ENA_SWITCH,
|
22 |
+
DHW_CM_SWITCH,
|
23 |
+
LOCK,
|
24 |
+
LOGGER, # pw-beta
|
25 |
+
MEMBERS,
|
26 |
+
RELAY,
|
27 |
+
SWITCHES,
|
28 |
+
)
|
29 |
+
|
30 |
+
# Upstream consts
|
31 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
32 |
from .entity import PlugwiseEntity
|
33 |
from .util import plugwise_command
|
|
|
42 |
key: SwitchType
|
43 |
|
44 |
|
45 |
+
# Upstream consts
|
46 |
+
PLUGWISE_SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
|
47 |
PlugwiseSwitchEntityDescription(
|
48 |
+
key=DHW_CM_SWITCH,
|
49 |
+
translation_key=DHW_CM_SWITCH,
|
50 |
+
device_class=SwitchDeviceClass.SWITCH,
|
51 |
entity_category=EntityCategory.CONFIG,
|
52 |
),
|
53 |
PlugwiseSwitchEntityDescription(
|
54 |
+
key=LOCK,
|
55 |
+
translation_key=LOCK,
|
56 |
+
device_class=SwitchDeviceClass.SWITCH,
|
57 |
entity_category=EntityCategory.CONFIG,
|
58 |
),
|
59 |
PlugwiseSwitchEntityDescription(
|
60 |
+
key=RELAY,
|
61 |
+
translation_key=RELAY,
|
62 |
device_class=SwitchDeviceClass.SWITCH,
|
63 |
),
|
64 |
PlugwiseSwitchEntityDescription(
|
65 |
+
key=COOLING_ENA_SWITCH,
|
66 |
+
translation_key=COOLING_ENA_SWITCH,
|
67 |
+
device_class=SwitchDeviceClass.SWITCH,
|
68 |
entity_category=EntityCategory.CONFIG,
|
69 |
),
|
70 |
)
|
|
|
75 |
entry: PlugwiseConfigEntry,
|
76 |
async_add_entities: AddEntitiesCallback,
|
77 |
) -> None:
|
78 |
+
"""Set up Plugwise switches from a config entry."""
|
79 |
coordinator = entry.runtime_data
|
80 |
|
81 |
@callback
|
|
|
84 |
if not coordinator.new_devices:
|
85 |
return
|
86 |
|
87 |
+
# Upstream consts
|
88 |
+
# async_add_entities(
|
89 |
+
# PlugwiseSwitchEntity(coordinator, device_id, description)
|
90 |
+
# for device_id in coordinator.new_devices
|
91 |
+
# if (switches := coordinator.data.devices[device_id].get(SWITCHES))
|
92 |
+
# for description in PLUGWISE_SWITCHES
|
93 |
+
# if description.key in switches
|
94 |
+
# )
|
95 |
+
# pw-beta alternative for debugging
|
96 |
+
entities: list[PlugwiseSwitchEntity] = []
|
97 |
+
for device_id in coordinator.new_devices:
|
98 |
+
device = coordinator.data.devices[device_id]
|
99 |
+
if not (switches := device.get(SWITCHES)):
|
100 |
+
continue
|
101 |
+
for description in PLUGWISE_SWITCHES:
|
102 |
+
if description.key not in switches:
|
103 |
+
continue
|
104 |
+
entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
|
105 |
+
LOGGER.debug(
|
106 |
+
"Add %s %s switch", device["name"], description.translation_key
|
107 |
+
)
|
108 |
+
async_add_entities(entities)
|
109 |
|
110 |
_add_entities()
|
111 |
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
|
|
124 |
) -> None:
|
125 |
"""Set up the Plugwise API."""
|
126 |
super().__init__(coordinator, device_id)
|
|
|
127 |
self.entity_description = description
|
128 |
+
self._attr_unique_id = f"{device_id}-{description.key}"
|
129 |
|
130 |
@property
|
131 |
def is_on(self) -> bool:
|
132 |
"""Return True if entity is on."""
|
133 |
+
return self.device[SWITCHES][self.entity_description.key] # Upstream const
|
134 |
|
135 |
@plugwise_command
|
136 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
137 |
"""Turn the device on."""
|
138 |
await self.coordinator.api.set_switch_state(
|
139 |
self._dev_id,
|
140 |
+
self.device.get(MEMBERS),
|
141 |
self.entity_description.key,
|
142 |
"on",
|
143 |
+
) # Upstream const
|
144 |
|
145 |
@plugwise_command
|
146 |
async def async_turn_off(self, **kwargs: Any) -> None:
|
147 |
"""Turn the device off."""
|
148 |
await self.coordinator.api.set_switch_state(
|
149 |
self._dev_id,
|
150 |
+
self.device.get(MEMBERS),
|
151 |
self.entity_description.key,
|
152 |
"off",
|
153 |
+
) # Upstream const
|
@@ -1,5 +1,7 @@
|
|
1 |
"""Utilities for Plugwise."""
|
2 |
|
|
|
|
|
3 |
from collections.abc import Awaitable, Callable, Coroutine
|
4 |
from typing import Any, Concatenate
|
5 |
|
@@ -10,6 +12,11 @@
|
|
10 |
from .const import DOMAIN
|
11 |
from .entity import PlugwiseEntity
|
12 |
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
|
15 |
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
|
|
|
1 |
"""Utilities for Plugwise."""
|
2 |
|
3 |
+
from __future__ import annotations
|
4 |
+
|
5 |
from collections.abc import Awaitable, Callable, Coroutine
|
6 |
from typing import Any, Concatenate
|
7 |
|
|
|
12 |
from .const import DOMAIN
|
13 |
from .entity import PlugwiseEntity
|
14 |
|
15 |
+
# For reference:
|
16 |
+
# _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
|
17 |
+
# _R = TypeVar("_R")
|
18 |
+
# _P = ParamSpec("_P")
|
19 |
+
|
20 |
|
21 |
def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
|
22 |
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
|