@@ -1,53 +1,29 @@
|
|
1 |
"""Plugwise platform for Home Assistant Core."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
from typing import Any
|
5 |
|
6 |
-
import voluptuous as vol
|
7 |
-
|
8 |
-
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
9 |
from homeassistant.config_entries import ConfigEntry
|
10 |
from homeassistant.const import Platform
|
11 |
from homeassistant.core import HomeAssistant, callback
|
12 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
13 |
-
from plugwise.exceptions import PlugwiseError
|
14 |
|
15 |
-
from .const import
|
16 |
-
COORDINATOR,
|
17 |
-
DOMAIN,
|
18 |
-
LOGGER,
|
19 |
-
PLATFORMS_GATEWAY,
|
20 |
-
SERVICE_DELETE,
|
21 |
-
UNDO_UPDATE_LISTENER,
|
22 |
-
)
|
23 |
-
from .const import CONF_REFRESH_INTERVAL # pw-beta options
|
24 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
25 |
|
|
|
|
|
26 |
|
27 |
-
async def async_setup_entry(hass: HomeAssistant, entry:
|
28 |
-
"""Set up Plugwise
|
29 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
30 |
|
31 |
-
|
32 |
-
if (
|
33 |
-
custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
|
34 |
-
) is not None: # pragma: no cover
|
35 |
-
cooldown = custom_refresh
|
36 |
-
LOGGER.debug("DUC cooldown interval: %s", cooldown)
|
37 |
-
|
38 |
-
coordinator = PlugwiseDataUpdateCoordinator(
|
39 |
-
hass, entry, cooldown
|
40 |
-
) # pw-beta - cooldown, update_interval as extra
|
41 |
await coordinator.async_config_entry_first_refresh()
|
42 |
-
# Migrate a changed sensor unique_id
|
43 |
migrate_sensor_entities(hass, coordinator)
|
44 |
|
45 |
-
|
46 |
-
|
47 |
-
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
48 |
-
COORDINATOR: coordinator, # pw-beta
|
49 |
-
UNDO_UPDATE_LISTENER: undo_listener, # pw-beta
|
50 |
-
}
|
51 |
|
52 |
device_registry = dr.async_get(hass)
|
53 |
device_registry.async_get_or_create(
|
@@ -55,60 +31,42 @@
|
|
55 |
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
|
56 |
manufacturer="Plugwise",
|
57 |
model=coordinator.api.smile_model,
|
|
|
58 |
name=coordinator.api.smile_name,
|
59 |
-
sw_version=coordinator.api.smile_version
|
60 |
-
)
|
61 |
|
62 |
-
|
63 |
-
self,
|
64 |
-
): # 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", coordinator.api.smile_name
|
68 |
-
)
|
69 |
-
try:
|
70 |
-
deleted = await coordinator.api.delete_notification()
|
71 |
-
LOGGER.debug("PW Notification deleted: %s", deleted)
|
72 |
-
except PlugwiseError:
|
73 |
-
LOGGER.debug(
|
74 |
-
"Failed to delete the Plugwise Notification for %s",
|
75 |
-
coordinator.api.smile_name,
|
76 |
-
)
|
77 |
-
|
78 |
-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY)
|
79 |
-
|
80 |
-
for component in PLATFORMS_GATEWAY: # pw-beta
|
81 |
-
if component == Platform.BINARY_SENSOR:
|
82 |
-
hass.services.async_register(
|
83 |
-
DOMAIN, SERVICE_DELETE, delete_notification, schema=vol.Schema({})
|
84 |
-
)
|
85 |
|
86 |
return True
|
87 |
|
88 |
|
89 |
-
async def
|
90 |
-
|
91 |
-
|
92 |
-
"""Handle options update."""
|
93 |
-
await hass.config_entries.async_reload(entry.entry_id)
|
94 |
-
|
95 |
-
|
96 |
-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
97 |
-
"""Unload a config entry."""
|
98 |
-
if unload_ok := await hass.config_entries.async_unload_platforms(
|
99 |
-
entry, PLATFORMS_GATEWAY
|
100 |
-
):
|
101 |
-
hass.data[DOMAIN].pop(entry.entry_id)
|
102 |
-
return unload_ok
|
103 |
|
104 |
|
105 |
@callback
|
106 |
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
107 |
"""Migrate Plugwise entity entries.
|
108 |
|
109 |
-
- Migrates unique ID from old
|
110 |
"""
|
111 |
-
if entry.domain ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
|
113 |
|
114 |
# No migration needed
|
@@ -122,10 +80,10 @@
|
|
122 |
"""Migrate Sensors if needed."""
|
123 |
ent_reg = er.async_get(hass)
|
124 |
|
125 |
-
#
|
126 |
# to opentherm_outdoor_air_temperature sensor
|
127 |
for device_id, device in coordinator.data.devices.items():
|
128 |
-
if device
|
129 |
continue
|
130 |
|
131 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
@@ -133,4 +91,10 @@
|
|
133 |
Platform.SENSOR, DOMAIN, old_unique_id
|
134 |
):
|
135 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
|
1 |
"""Plugwise platform for Home Assistant Core."""
|
2 |
+
|
3 |
from __future__ import annotations
|
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 HomeAssistant, callback
|
10 |
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
11 |
|
12 |
+
from .const import DOMAIN, LOGGER, PLATFORMS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 components from a config entry."""
|
20 |
await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
|
21 |
|
22 |
+
coordinator = PlugwiseDataUpdateCoordinator(hass)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
await coordinator.async_config_entry_first_refresh()
|
|
|
24 |
migrate_sensor_entities(hass, coordinator)
|
25 |
|
26 |
+
entry.runtime_data = coordinator
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
device_registry = dr.async_get(hass)
|
29 |
device_registry.async_get_or_create(
|
|
|
31 |
identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
|
32 |
manufacturer="Plugwise",
|
33 |
model=coordinator.api.smile_model,
|
34 |
+
model_id=coordinator.api.smile_model_id,
|
35 |
name=coordinator.api.smile_name,
|
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 the Plugwise components."""
|
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.
|
52 |
|
53 |
+
- Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
|
54 |
"""
|
55 |
+
if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
|
56 |
+
"-slave_boiler_state"
|
57 |
+
):
|
58 |
+
return {
|
59 |
+
"new_unique_id": entry.unique_id.replace(
|
60 |
+
"-slave_boiler_state", "-secondary_boiler_state"
|
61 |
+
)
|
62 |
+
}
|
63 |
+
if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
|
64 |
+
"-relative_humidity"
|
65 |
+
):
|
66 |
+
return {
|
67 |
+
"new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
|
68 |
+
}
|
69 |
+
if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"):
|
70 |
return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
|
71 |
|
72 |
# No migration needed
|
|
|
80 |
"""Migrate Sensors if needed."""
|
81 |
ent_reg = er.async_get(hass)
|
82 |
|
83 |
+
# Migrating opentherm_outdoor_temperature
|
84 |
# to opentherm_outdoor_air_temperature sensor
|
85 |
for device_id, device in coordinator.data.devices.items():
|
86 |
+
if device.get("dev_class") != "heater_central":
|
87 |
continue
|
88 |
|
89 |
old_unique_id = f"{device_id}-outdoor_temperature"
|
|
|
91 |
Platform.SENSOR, DOMAIN, old_unique_id
|
92 |
):
|
93 |
new_unique_id = f"{device_id}-outdoor_air_temperature"
|
94 |
+
LOGGER.debug(
|
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)
|
@@ -1,51 +1,115 @@
|
|
1 |
"""Plugwise Binary Sensor component for Home Assistant."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
from collections.abc import Mapping
|
|
|
5 |
from typing import Any
|
6 |
|
7 |
-
from
|
8 |
-
from homeassistant.config_entries import ConfigEntry
|
9 |
-
from homeassistant.core import HomeAssistant
|
10 |
-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
11 |
|
12 |
-
from .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
|
|
14 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
15 |
from .entity import PlugwiseEntity
|
16 |
-
from .models import PW_BINARY_SENSOR_TYPES, PlugwiseBinarySensorEntityDescription
|
17 |
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
|
20 |
|
21 |
async def async_setup_entry(
|
22 |
hass: HomeAssistant,
|
23 |
-
|
24 |
async_add_entities: AddEntitiesCallback,
|
25 |
) -> None:
|
26 |
"""Set up the Smile binary_sensors from a config entry."""
|
27 |
-
coordinator
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
34 |
if (
|
35 |
-
|
36 |
-
|
37 |
-
):
|
38 |
-
continue
|
39 |
-
|
40 |
-
entities.append(
|
41 |
-
PlugwiseBinarySensorEntity(
|
42 |
-
coordinator,
|
43 |
-
device_id,
|
44 |
-
description,
|
45 |
)
|
46 |
)
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
49 |
|
50 |
|
51 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
@@ -63,27 +127,11 @@
|
|
63 |
super().__init__(coordinator, device_id)
|
64 |
self.entity_description = description
|
65 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
66 |
-
self._notification: dict[str, str] = {} # pw-beta
|
67 |
|
68 |
@property
|
69 |
-
def is_on(self) -> bool
|
70 |
"""Return true if the binary sensor is on."""
|
71 |
-
|
72 |
-
self._notification
|
73 |
-
): # pw-beta: show Plugwise notifications as HA persistent notifications
|
74 |
-
for notify_id, message in self._notification.items():
|
75 |
-
self.hass.components.persistent_notification.async_create(
|
76 |
-
message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
|
77 |
-
)
|
78 |
-
|
79 |
-
return self.device["binary_sensors"].get(self.entity_description.key)
|
80 |
-
|
81 |
-
@property
|
82 |
-
def icon(self) -> str | None:
|
83 |
-
"""Return the icon to use in the frontend, if any."""
|
84 |
-
if (icon_off := self.entity_description.icon_off) and self.is_on is False:
|
85 |
-
return icon_off
|
86 |
-
return self.entity_description.icon
|
87 |
|
88 |
@property
|
89 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
@@ -91,25 +139,13 @@
|
|
91 |
if self.entity_description.key != "plugwise_notification":
|
92 |
return None
|
93 |
|
94 |
-
|
95 |
-
# not all severities including those without content as empty lists
|
96 |
-
attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
|
97 |
-
self._notification = {} # pw-beta
|
98 |
if notify := self.coordinator.data.gateway["notifications"]:
|
99 |
-
for
|
100 |
for msg_type, msg in details.items():
|
101 |
msg_type = msg_type.lower()
|
102 |
if msg_type not in SEVERITIES:
|
103 |
-
msg_type = "other"
|
104 |
-
|
105 |
-
if (
|
106 |
-
f"{msg_type}_msg" not in attrs
|
107 |
-
): # pw-beta Re-evaluate against Core
|
108 |
-
attrs[f"{msg_type}_msg"] = []
|
109 |
attrs[f"{msg_type}_msg"].append(msg)
|
110 |
|
111 |
-
self._notification[
|
112 |
-
notify_id
|
113 |
-
] = f"{msg_type.title()}: {msg}" # pw-beta
|
114 |
-
|
115 |
return attrs
|
|
|
1 |
"""Plugwise Binary Sensor component for Home Assistant."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
from collections.abc import Mapping
|
6 |
+
from dataclasses import dataclass
|
7 |
from typing import Any
|
8 |
|
9 |
+
from plugwise.constants import BinarySensorType
|
|
|
|
|
|
|
10 |
|
11 |
+
from homeassistant.components.binary_sensor import (
|
12 |
+
BinarySensorDeviceClass,
|
13 |
+
BinarySensorEntity,
|
14 |
+
BinarySensorEntityDescription,
|
15 |
+
)
|
16 |
+
from homeassistant.const import EntityCategory
|
17 |
+
from homeassistant.core import HomeAssistant, callback
|
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 |
+
|
27 |
+
@dataclass(frozen=True)
|
28 |
+
class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
|
29 |
+
"""Describes a Plugwise binary sensor entity."""
|
30 |
+
|
31 |
+
key: BinarySensorType
|
32 |
+
|
33 |
+
|
34 |
+
BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
|
35 |
+
PlugwiseBinarySensorEntityDescription(
|
36 |
+
key="low_battery",
|
37 |
+
translation_key="low_battery",
|
38 |
+
device_class=BinarySensorDeviceClass.BATTERY,
|
39 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
40 |
+
),
|
41 |
+
PlugwiseBinarySensorEntityDescription(
|
42 |
+
key="compressor_state",
|
43 |
+
translation_key="compressor_state",
|
44 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
45 |
+
),
|
46 |
+
PlugwiseBinarySensorEntityDescription(
|
47 |
+
key="cooling_enabled",
|
48 |
+
translation_key="cooling_enabled",
|
49 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
50 |
+
),
|
51 |
+
PlugwiseBinarySensorEntityDescription(
|
52 |
+
key="dhw_state",
|
53 |
+
translation_key="dhw_state",
|
54 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
55 |
+
),
|
56 |
+
PlugwiseBinarySensorEntityDescription(
|
57 |
+
key="flame_state",
|
58 |
+
translation_key="flame_state",
|
59 |
+
name="Flame state",
|
60 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
61 |
+
),
|
62 |
+
PlugwiseBinarySensorEntityDescription(
|
63 |
+
key="heating_state",
|
64 |
+
translation_key="heating_state",
|
65 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
66 |
+
),
|
67 |
+
PlugwiseBinarySensorEntityDescription(
|
68 |
+
key="cooling_state",
|
69 |
+
translation_key="cooling_state",
|
70 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
71 |
+
),
|
72 |
+
PlugwiseBinarySensorEntityDescription(
|
73 |
+
key="secondary_boiler_state",
|
74 |
+
translation_key="secondary_boiler_state",
|
75 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
76 |
+
),
|
77 |
+
PlugwiseBinarySensorEntityDescription(
|
78 |
+
key="plugwise_notification",
|
79 |
+
translation_key="plugwise_notification",
|
80 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
81 |
+
),
|
82 |
+
)
|
83 |
|
84 |
|
85 |
async def async_setup_entry(
|
86 |
hass: HomeAssistant,
|
87 |
+
entry: PlugwiseConfigEntry,
|
88 |
async_add_entities: AddEntitiesCallback,
|
89 |
) -> None:
|
90 |
"""Set up the Smile binary_sensors from a config entry."""
|
91 |
+
coordinator = entry.runtime_data
|
92 |
+
|
93 |
+
@callback
|
94 |
+
def _add_entities() -> None:
|
95 |
+
"""Add Entities."""
|
96 |
+
if not coordinator.new_devices:
|
97 |
+
return
|
98 |
+
|
99 |
+
async_add_entities(
|
100 |
+
PlugwiseBinarySensorEntity(coordinator, device_id, description)
|
101 |
+
for device_id in coordinator.new_devices
|
102 |
if (
|
103 |
+
binary_sensors := coordinator.data.devices[device_id].get(
|
104 |
+
"binary_sensors"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
)
|
106 |
)
|
107 |
+
for description in BINARY_SENSORS
|
108 |
+
if description.key in binary_sensors
|
109 |
+
)
|
110 |
+
|
111 |
+
_add_entities()
|
112 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
113 |
|
114 |
|
115 |
class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
|
|
|
127 |
super().__init__(coordinator, device_id)
|
128 |
self.entity_description = description
|
129 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
|
|
130 |
|
131 |
@property
|
132 |
+
def is_on(self) -> bool:
|
133 |
"""Return true if the binary sensor is on."""
|
134 |
+
return self.device["binary_sensors"][self.entity_description.key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
|
136 |
@property
|
137 |
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
|
|
139 |
if self.entity_description.key != "plugwise_notification":
|
140 |
return None
|
141 |
|
142 |
+
attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
|
|
|
|
|
|
|
143 |
if notify := self.coordinator.data.gateway["notifications"]:
|
144 |
+
for details in notify.values():
|
145 |
for msg_type, msg in details.items():
|
146 |
msg_type = msg_type.lower()
|
147 |
if msg_type not in SEVERITIES:
|
148 |
+
msg_type = "other"
|
|
|
|
|
|
|
|
|
|
|
149 |
attrs[f"{msg_type}_msg"].append(msg)
|
150 |
|
|
|
|
|
|
|
|
|
151 |
return attrs
|
@@ -1,4 +1,5 @@
|
|
1 |
"""Plugwise Climate component for Home Assistant."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
from typing import Any
|
@@ -12,20 +13,12 @@
|
|
12 |
HVACAction,
|
13 |
HVACMode,
|
14 |
)
|
15 |
-
from homeassistant.components.climate.const import (
|
16 |
-
PRESET_AWAY, # pw-beta homekit emulation
|
17 |
-
)
|
18 |
-
from homeassistant.components.climate.const import (
|
19 |
-
PRESET_HOME, # pw-beta homekit emulation
|
20 |
-
)
|
21 |
-
from homeassistant.config_entries import ConfigEntry
|
22 |
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
23 |
-
from homeassistant.core import HomeAssistant
|
24 |
from homeassistant.exceptions import HomeAssistantError
|
25 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
26 |
|
27 |
-
from .
|
28 |
-
from .const import COORDINATOR # pw-beta
|
29 |
from .const import DOMAIN, MASTER_THERMOSTATS
|
30 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
31 |
from .entity import PlugwiseEntity
|
@@ -34,72 +27,89 @@
|
|
34 |
|
35 |
async def async_setup_entry(
|
36 |
hass: HomeAssistant,
|
37 |
-
|
38 |
async_add_entities: AddEntitiesCallback,
|
39 |
) -> None:
|
40 |
"""Set up the Smile Thermostats from a config entry."""
|
41 |
-
coordinator
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
)
|
|
|
56 |
|
57 |
|
58 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
59 |
-
"""Representation of
|
60 |
|
61 |
_attr_has_entity_name = True
|
|
|
62 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
63 |
_attr_translation_key = DOMAIN
|
|
|
|
|
|
|
64 |
|
65 |
def __init__(
|
66 |
self,
|
67 |
coordinator: PlugwiseDataUpdateCoordinator,
|
68 |
device_id: str,
|
69 |
-
homekit_enabled: bool, # pw-beta homekit emulation
|
70 |
) -> None:
|
71 |
"""Set up the Plugwise API."""
|
72 |
super().__init__(coordinator, device_id)
|
73 |
-
self.
|
74 |
-
self._homekit_mode: str | None = None # pw-beta homekit emulation
|
75 |
self._attr_unique_id = f"{device_id}-climate"
|
76 |
-
|
|
|
|
|
77 |
# Determine supported features
|
78 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
79 |
-
if
|
|
|
|
|
|
|
80 |
self._attr_supported_features = (
|
81 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
82 |
)
|
|
|
|
|
|
|
|
|
83 |
if presets := self.device.get("preset_modes"):
|
84 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
85 |
self._attr_preset_modes = presets
|
86 |
|
87 |
-
# Determine hvac modes and current hvac mode
|
88 |
-
self._attr_hvac_modes = [HVACMode.HEAT]
|
89 |
-
if self.coordinator.data.gateway["cooling_present"]:
|
90 |
-
self._attr_hvac_modes = [HVACMode.HEAT_COOL]
|
91 |
-
if self.device["available_schedules"] != ["None"]:
|
92 |
-
self._attr_hvac_modes.append(HVACMode.AUTO)
|
93 |
-
if self._homekit_enabled: # pw-beta homekit emulation
|
94 |
-
self._attr_hvac_modes.append(HVACMode.OFF) # pragma: no cover
|
95 |
-
|
96 |
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
97 |
-
self._attr_max_temp = self.device["thermostat"]["upper_bound"]
|
98 |
# Ensure we don't drop below 0.1
|
99 |
self._attr_target_temperature_step = max(
|
100 |
self.device["thermostat"]["resolution"], 0.1
|
101 |
)
|
102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
@property
|
104 |
def current_temperature(self) -> float:
|
105 |
"""Return the current temperature."""
|
@@ -132,36 +142,55 @@
|
|
132 |
|
133 |
@property
|
134 |
def hvac_mode(self) -> HVACMode:
|
135 |
-
"""Return HVAC operation ie. auto, heat, heat_cool, or off mode."""
|
136 |
-
if (
|
137 |
-
|
138 |
-
) is None or mode not in self.hvac_modes: # pw-beta add to Core
|
139 |
-
return HVACMode.HEAT # pragma: no cover
|
140 |
-
# pw-beta homekit emulation
|
141 |
-
if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
|
142 |
-
mode = HVACMode.OFF # pragma: no cover
|
143 |
-
|
144 |
return HVACMode(mode)
|
145 |
|
146 |
@property
|
147 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
"""Return the current running hvac operation if supported."""
|
149 |
-
#
|
|
|
|
|
|
|
150 |
if (control_state := self.device.get("control_state")) == "cooling":
|
151 |
return HVACAction.COOLING
|
152 |
-
|
153 |
-
# until preheating is added as a separate state
|
154 |
-
if control_state in ["heating", "preheating"]:
|
155 |
return HVACAction.HEATING
|
|
|
|
|
156 |
if control_state == "off":
|
157 |
return HVACAction.IDLE
|
158 |
|
159 |
-
|
160 |
-
|
161 |
-
]
|
162 |
-
if hc_data["binary_sensors"]["heating_state"]:
|
163 |
return HVACAction.HEATING
|
164 |
-
if
|
165 |
return HVACAction.COOLING
|
166 |
|
167 |
return HVACAction.IDLE
|
@@ -169,28 +198,28 @@
|
|
169 |
@property
|
170 |
def preset_mode(self) -> str | None:
|
171 |
"""Return the current preset mode."""
|
172 |
-
return self.device
|
173 |
|
174 |
@plugwise_command
|
175 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
176 |
"""Set new target temperature."""
|
177 |
-
if ATTR_HVAC_MODE in kwargs: # pw-beta add to Core
|
178 |
-
await self.async_set_hvac_mode(
|
179 |
-
kwargs[ATTR_HVAC_MODE]
|
180 |
-
) # pw-beta add to Core
|
181 |
-
|
182 |
data: dict[str, Any] = {}
|
183 |
if ATTR_TEMPERATURE in kwargs:
|
184 |
-
data["setpoint"] = kwargs
|
185 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
186 |
-
data["setpoint_high"] = kwargs
|
187 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
188 |
-
data["setpoint_low"] = kwargs
|
189 |
|
190 |
for temperature in data.values():
|
191 |
-
if
|
|
|
|
|
192 |
raise ValueError("Invalid temperature change requested")
|
193 |
|
|
|
|
|
|
|
194 |
await self.coordinator.api.set_temperature(self.device["location"], data)
|
195 |
|
196 |
@plugwise_command
|
@@ -199,22 +228,18 @@
|
|
199 |
if hvac_mode not in self.hvac_modes:
|
200 |
raise HomeAssistantError("Unsupported hvac_mode")
|
201 |
|
202 |
-
|
203 |
-
|
204 |
-
self.device["last_used"],
|
205 |
-
"on" if hvac_mode == HVACMode.AUTO else "off",
|
206 |
-
)
|
207 |
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
|
218 |
|
219 |
@plugwise_command
|
220 |
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
|
|
1 |
"""Plugwise Climate component for Home Assistant."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
from typing import Any
|
|
|
13 |
HVACAction,
|
14 |
HVACMode,
|
15 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
17 |
+
from homeassistant.core import HomeAssistant, callback
|
18 |
from homeassistant.exceptions import HomeAssistantError
|
19 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
20 |
|
21 |
+
from . import PlugwiseConfigEntry
|
|
|
22 |
from .const import DOMAIN, MASTER_THERMOSTATS
|
23 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
24 |
from .entity import PlugwiseEntity
|
|
|
27 |
|
28 |
async def async_setup_entry(
|
29 |
hass: HomeAssistant,
|
30 |
+
entry: PlugwiseConfigEntry,
|
31 |
async_add_entities: AddEntitiesCallback,
|
32 |
) -> None:
|
33 |
"""Set up the Smile Thermostats from a config entry."""
|
34 |
+
coordinator = entry.runtime_data
|
35 |
+
|
36 |
+
@callback
|
37 |
+
def _add_entities() -> None:
|
38 |
+
"""Add Entities."""
|
39 |
+
if not coordinator.new_devices:
|
40 |
+
return
|
41 |
+
|
42 |
+
async_add_entities(
|
43 |
+
PlugwiseClimateEntity(coordinator, device_id)
|
44 |
+
for device_id in coordinator.new_devices
|
45 |
+
if coordinator.data.devices[device_id]["dev_class"] in MASTER_THERMOSTATS
|
46 |
+
)
|
47 |
+
|
48 |
+
_add_entities()
|
49 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
50 |
|
51 |
|
52 |
class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
53 |
+
"""Representation of a Plugwise thermostat."""
|
54 |
|
55 |
_attr_has_entity_name = True
|
56 |
+
_attr_name = None
|
57 |
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
58 |
_attr_translation_key = DOMAIN
|
59 |
+
_enable_turn_on_off_backwards_compatibility = False
|
60 |
+
|
61 |
+
_previous_mode: str = "heating"
|
62 |
|
63 |
def __init__(
|
64 |
self,
|
65 |
coordinator: PlugwiseDataUpdateCoordinator,
|
66 |
device_id: str,
|
|
|
67 |
) -> None:
|
68 |
"""Set up the Plugwise API."""
|
69 |
super().__init__(coordinator, device_id)
|
70 |
+
self._attr_extra_state_attributes = {}
|
|
|
71 |
self._attr_unique_id = f"{device_id}-climate"
|
72 |
+
self.cdr_gateway = coordinator.data.gateway
|
73 |
+
gateway_id: str = coordinator.data.gateway["gateway_id"]
|
74 |
+
self.gateway_data = coordinator.data.devices[gateway_id]
|
75 |
# Determine supported features
|
76 |
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
77 |
+
if (
|
78 |
+
self.cdr_gateway["cooling_present"]
|
79 |
+
and self.cdr_gateway["smile_name"] != "Adam"
|
80 |
+
):
|
81 |
self._attr_supported_features = (
|
82 |
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
83 |
)
|
84 |
+
if HVACMode.OFF in self.hvac_modes:
|
85 |
+
self._attr_supported_features |= (
|
86 |
+
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
87 |
+
)
|
88 |
if presets := self.device.get("preset_modes"):
|
89 |
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
90 |
self._attr_preset_modes = presets
|
91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
self._attr_min_temp = self.device["thermostat"]["lower_bound"]
|
93 |
+
self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
|
94 |
# Ensure we don't drop below 0.1
|
95 |
self._attr_target_temperature_step = max(
|
96 |
self.device["thermostat"]["resolution"], 0.1
|
97 |
)
|
98 |
|
99 |
+
def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
|
100 |
+
"""Return the previous action-mode when the regulation-mode is not heating or cooling.
|
101 |
+
|
102 |
+
Helper for set_hvac_mode().
|
103 |
+
"""
|
104 |
+
# When no cooling available, _previous_mode is always heating
|
105 |
+
if (
|
106 |
+
"regulation_modes" in self.gateway_data
|
107 |
+
and "cooling" in self.gateway_data["regulation_modes"]
|
108 |
+
):
|
109 |
+
mode = self.gateway_data["select_regulation_mode"]
|
110 |
+
if mode in ("cooling", "heating"):
|
111 |
+
self._previous_mode = mode
|
112 |
+
|
113 |
@property
|
114 |
def current_temperature(self) -> float:
|
115 |
"""Return the current temperature."""
|
|
|
142 |
|
143 |
@property
|
144 |
def hvac_mode(self) -> HVACMode:
|
145 |
+
"""Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
|
146 |
+
if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes:
|
147 |
+
return HVACMode.HEAT
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
return HVACMode(mode)
|
149 |
|
150 |
@property
|
151 |
+
def hvac_modes(self) -> list[HVACMode]:
|
152 |
+
"""Return a list of available HVACModes."""
|
153 |
+
hvac_modes: list[HVACMode] = []
|
154 |
+
if "regulation_modes" in self.gateway_data:
|
155 |
+
hvac_modes.append(HVACMode.OFF)
|
156 |
+
|
157 |
+
if "available_schedules" in self.device:
|
158 |
+
hvac_modes.append(HVACMode.AUTO)
|
159 |
+
|
160 |
+
if self.cdr_gateway["cooling_present"]:
|
161 |
+
if "regulation_modes" in self.gateway_data:
|
162 |
+
if self.gateway_data["select_regulation_mode"] == "cooling":
|
163 |
+
hvac_modes.append(HVACMode.COOL)
|
164 |
+
if self.gateway_data["select_regulation_mode"] == "heating":
|
165 |
+
hvac_modes.append(HVACMode.HEAT)
|
166 |
+
else:
|
167 |
+
hvac_modes.append(HVACMode.HEAT_COOL)
|
168 |
+
else:
|
169 |
+
hvac_modes.append(HVACMode.HEAT)
|
170 |
+
|
171 |
+
return hvac_modes
|
172 |
+
|
173 |
+
@property
|
174 |
+
def hvac_action(self) -> HVACAction:
|
175 |
"""Return the current running hvac operation if supported."""
|
176 |
+
# Keep track of the previous action-mode
|
177 |
+
self._previous_action_mode(self.coordinator)
|
178 |
+
|
179 |
+
# Adam provides the hvac_action for each thermostat
|
180 |
if (control_state := self.device.get("control_state")) == "cooling":
|
181 |
return HVACAction.COOLING
|
182 |
+
if control_state == "heating":
|
|
|
|
|
183 |
return HVACAction.HEATING
|
184 |
+
if control_state == "preheating":
|
185 |
+
return HVACAction.PREHEATING
|
186 |
if control_state == "off":
|
187 |
return HVACAction.IDLE
|
188 |
|
189 |
+
heater: str = self.coordinator.data.gateway["heater_id"]
|
190 |
+
heater_data = self.coordinator.data.devices[heater]
|
191 |
+
if heater_data["binary_sensors"]["heating_state"]:
|
|
|
192 |
return HVACAction.HEATING
|
193 |
+
if heater_data["binary_sensors"].get("cooling_state", False):
|
194 |
return HVACAction.COOLING
|
195 |
|
196 |
return HVACAction.IDLE
|
|
|
198 |
@property
|
199 |
def preset_mode(self) -> str | None:
|
200 |
"""Return the current preset mode."""
|
201 |
+
return self.device.get("active_preset")
|
202 |
|
203 |
@plugwise_command
|
204 |
async def async_set_temperature(self, **kwargs: Any) -> None:
|
205 |
"""Set new target temperature."""
|
|
|
|
|
|
|
|
|
|
|
206 |
data: dict[str, Any] = {}
|
207 |
if ATTR_TEMPERATURE in kwargs:
|
208 |
+
data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
|
209 |
if ATTR_TARGET_TEMP_HIGH in kwargs:
|
210 |
+
data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
211 |
if ATTR_TARGET_TEMP_LOW in kwargs:
|
212 |
+
data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
213 |
|
214 |
for temperature in data.values():
|
215 |
+
if temperature is None or not (
|
216 |
+
self._attr_min_temp <= temperature <= self._attr_max_temp
|
217 |
+
):
|
218 |
raise ValueError("Invalid temperature change requested")
|
219 |
|
220 |
+
if mode := kwargs.get(ATTR_HVAC_MODE):
|
221 |
+
await self.async_set_hvac_mode(mode)
|
222 |
+
|
223 |
await self.coordinator.api.set_temperature(self.device["location"], data)
|
224 |
|
225 |
@plugwise_command
|
|
|
228 |
if hvac_mode not in self.hvac_modes:
|
229 |
raise HomeAssistantError("Unsupported hvac_mode")
|
230 |
|
231 |
+
if hvac_mode == self.hvac_mode:
|
232 |
+
return
|
|
|
|
|
|
|
233 |
|
234 |
+
if hvac_mode == HVACMode.OFF:
|
235 |
+
await self.coordinator.api.set_regulation_mode(hvac_mode)
|
236 |
+
else:
|
237 |
+
await self.coordinator.api.set_schedule_state(
|
238 |
+
self.device["location"],
|
239 |
+
"on" if hvac_mode == HVACMode.AUTO else "off",
|
240 |
+
)
|
241 |
+
if self.hvac_mode == HVACMode.OFF:
|
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:
|
@@ -1,114 +1,69 @@
|
|
1 |
"""Config flow for Plugwise integration."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
-
|
5 |
-
from typing import Any
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
import voluptuous as vol
|
8 |
|
9 |
-
from homeassistant import config_entries
|
10 |
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
11 |
-
from homeassistant.config_entries import
|
12 |
from homeassistant.const import (
|
|
|
13 |
CONF_BASE,
|
14 |
CONF_HOST,
|
15 |
CONF_NAME,
|
16 |
CONF_PASSWORD,
|
17 |
CONF_PORT,
|
18 |
-
CONF_SCAN_INTERVAL,
|
19 |
CONF_USERNAME,
|
20 |
)
|
21 |
-
from homeassistant.core import HomeAssistant
|
22 |
-
from homeassistant.data_entry_flow import FlowResult
|
23 |
-
from homeassistant.helpers import config_validation as cv
|
24 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
25 |
-
from plugwise.exceptions import (
|
26 |
-
ConnectionFailedError,
|
27 |
-
InvalidAuthentication,
|
28 |
-
InvalidSetupError,
|
29 |
-
InvalidXMLError,
|
30 |
-
ResponseError,
|
31 |
-
UnsupportedDeviceError,
|
32 |
-
)
|
33 |
-
|
34 |
-
# pw-beta Note; the below are explicit through isort
|
35 |
-
from plugwise import Smile
|
36 |
|
37 |
from .const import (
|
38 |
-
API,
|
39 |
-
COORDINATOR,
|
40 |
DEFAULT_PORT,
|
41 |
DEFAULT_USERNAME,
|
42 |
DOMAIN,
|
43 |
FLOW_SMILE,
|
44 |
FLOW_STRETCH,
|
45 |
-
PW_TYPE,
|
46 |
SMILE,
|
47 |
STRETCH,
|
48 |
STRETCH_USERNAME,
|
49 |
ZEROCONF_MAP,
|
50 |
)
|
51 |
|
52 |
-
|
53 |
-
|
54 |
-
from .const import CONF_REFRESH_INTERVAL # pw-beta option
|
55 |
-
from .const import CONF_USB_PATH # pw-beta usb
|
56 |
-
from .const import DEFAULT_SCAN_INTERVAL # pw-beta option
|
57 |
-
from .const import FLOW_NET # pw-beta usb
|
58 |
-
from .const import FLOW_TYPE # pw-beta usb
|
59 |
-
from .const import FLOW_USB # pw-beta usb
|
60 |
-
from .const import STICK # pw-beta usb
|
61 |
-
|
62 |
-
CONNECTION_SCHEMA = vol.Schema(
|
63 |
-
{vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])}
|
64 |
-
) # pw-beta usb
|
65 |
-
|
66 |
-
|
67 |
-
@callback
|
68 |
-
def plugwise_stick_entries(hass): # pw-beta usb
|
69 |
-
"""Return existing connections for Plugwise USB-stick domain."""
|
70 |
-
sticks = []
|
71 |
-
for entry in hass.config_entries.async_entries(DOMAIN):
|
72 |
-
if entry.data.get(PW_TYPE) == STICK:
|
73 |
-
sticks.append(entry.data.get(CONF_USB_PATH))
|
74 |
-
return sticks
|
75 |
-
|
76 |
-
|
77 |
-
def _base_gw_schema(
|
78 |
-
discovery_info: ZeroconfServiceInfo | None,
|
79 |
-
user_input: dict[str, Any] | None,
|
80 |
-
) -> vol.Schema:
|
81 |
"""Generate base schema for gateways."""
|
|
|
|
|
82 |
if not discovery_info:
|
83 |
-
|
84 |
-
return vol.Schema(
|
85 |
-
{
|
86 |
-
vol.Required(CONF_PASSWORD): str,
|
87 |
-
vol.Required(CONF_HOST): str,
|
88 |
-
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
89 |
-
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
90 |
-
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
91 |
-
),
|
92 |
-
}
|
93 |
-
)
|
94 |
-
return vol.Schema(
|
95 |
{
|
96 |
-
vol.Required(
|
97 |
-
vol.
|
98 |
-
vol.
|
99 |
-
vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): vol.In(
|
100 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
101 |
),
|
102 |
}
|
103 |
)
|
104 |
|
105 |
-
return
|
106 |
|
107 |
|
108 |
-
async def
|
109 |
"""Validate whether the user input allows us to connect to the gateway.
|
110 |
|
111 |
-
Data has the keys from
|
112 |
"""
|
113 |
websession = async_get_clientsession(hass, verify_ssl=False)
|
114 |
api = Smile(
|
@@ -116,7 +71,6 @@
|
|
116 |
password=data[CONF_PASSWORD],
|
117 |
port=data[CONF_PORT],
|
118 |
username=data[CONF_USERNAME],
|
119 |
-
timeout=30,
|
120 |
websession=websession,
|
121 |
)
|
122 |
await api.connect()
|
@@ -129,11 +83,12 @@
|
|
129 |
VERSION = 1
|
130 |
|
131 |
discovery_info: ZeroconfServiceInfo | None = None
|
|
|
132 |
_username: str = DEFAULT_USERNAME
|
133 |
|
134 |
async def async_step_zeroconf(
|
135 |
self, discovery_info: ZeroconfServiceInfo
|
136 |
-
) ->
|
137 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
138 |
self.discovery_info = discovery_info
|
139 |
_properties = discovery_info.properties
|
@@ -141,7 +96,7 @@
|
|
141 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
142 |
if config_entry := await self.async_set_unique_id(unique_id):
|
143 |
try:
|
144 |
-
await
|
145 |
self.hass,
|
146 |
{
|
147 |
CONF_HOST: discovery_info.host,
|
@@ -150,7 +105,7 @@
|
|
150 |
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
151 |
},
|
152 |
)
|
153 |
-
except Exception: #
|
154 |
self._abort_if_unique_id_configured()
|
155 |
else:
|
156 |
self._abort_if_unique_id_configured(
|
@@ -162,7 +117,7 @@
|
|
162 |
|
163 |
if DEFAULT_USERNAME not in unique_id:
|
164 |
self._username = STRETCH_USERNAME
|
165 |
-
_product = _properties.get("product",
|
166 |
_version = _properties.get("version", "n/a")
|
167 |
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
168 |
|
@@ -174,172 +129,67 @@
|
|
174 |
# If we have discovered an Adam or Anna, both might be on the network.
|
175 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
176 |
# be added.
|
177 |
-
|
178 |
-
|
179 |
-
if (
|
180 |
-
_product == "smile_thermo"
|
181 |
-
and "context" in flow
|
182 |
-
and flow["context"].get("product") == "smile_open_therm"
|
183 |
-
):
|
184 |
-
return self.async_abort(reason="anna_with_adam")
|
185 |
-
|
186 |
-
# This is an Adam, and there is already an Anna flow in progress
|
187 |
-
if (
|
188 |
-
_product == "smile_open_therm"
|
189 |
-
and "context" in flow
|
190 |
-
and flow["context"].get("product") == "smile_thermo"
|
191 |
-
and "flow_id" in flow
|
192 |
-
):
|
193 |
-
self.hass.config_entries.flow.async_abort(flow["flow_id"])
|
194 |
|
195 |
self.context.update(
|
196 |
{
|
197 |
-
"title_placeholders": {
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
CONF_USERNAME: self._username,
|
202 |
-
},
|
203 |
-
"configuration_url": f"http://{discovery_info.host}:{discovery_info.port}",
|
204 |
-
"product": _product,
|
205 |
}
|
206 |
)
|
207 |
-
return await self.
|
208 |
-
|
209 |
-
async def async_step_user_gateway(
|
210 |
-
self, user_input: dict[str, Any] | None = None
|
211 |
-
) -> FlowResult:
|
212 |
-
"""Handle the initial step when using network/gateway setups."""
|
213 |
-
errors: dict[str, str] = {}
|
214 |
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
if self.
|
223 |
-
|
224 |
-
user_input[CONF_PORT] = self.discovery_info.port
|
225 |
-
user_input[CONF_USERNAME] = self._username
|
226 |
-
try:
|
227 |
-
api = await validate_gw_input(self.hass, user_input)
|
228 |
-
except ConnectionFailedError:
|
229 |
-
errors[CONF_BASE] = "cannot_connect"
|
230 |
-
except InvalidAuthentication:
|
231 |
-
errors[CONF_BASE] = "invalid_auth"
|
232 |
-
except InvalidSetupError:
|
233 |
-
errors[CONF_BASE] = "invalid_setup"
|
234 |
-
except (InvalidXMLError, ResponseError):
|
235 |
-
errors[CONF_BASE] = "response_error"
|
236 |
-
except UnsupportedDeviceError:
|
237 |
-
errors[CONF_BASE] = "unsupported"
|
238 |
-
except Exception: # pylint: disable=broad-except
|
239 |
-
errors[CONF_BASE] = "unknown"
|
240 |
-
|
241 |
-
if errors:
|
242 |
-
return self.async_show_form(
|
243 |
-
step_id="user_gateway",
|
244 |
-
data_schema=_base_gw_schema(None, user_input),
|
245 |
-
errors=errors,
|
246 |
-
)
|
247 |
-
|
248 |
-
await self.async_set_unique_id(
|
249 |
-
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
250 |
-
)
|
251 |
-
self._abort_if_unique_id_configured()
|
252 |
|
253 |
-
|
254 |
-
return self.async_create_entry(title=api.smile_name, data=user_input)
|
255 |
|
256 |
async def async_step_user(
|
257 |
self, user_input: dict[str, Any] | None = None
|
258 |
-
) ->
|
259 |
"""Handle the initial step when using network/gateway setups."""
|
260 |
errors: dict[str, str] = {}
|
|
|
261 |
if user_input is not None:
|
262 |
-
if
|
263 |
-
|
|
|
|
|
264 |
|
265 |
-
|
266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
|
268 |
return self.async_show_form(
|
269 |
-
step_id=
|
270 |
-
data_schema=
|
271 |
errors=errors,
|
272 |
)
|
273 |
-
|
274 |
-
@staticmethod
|
275 |
-
@callback
|
276 |
-
def async_get_options_flow(
|
277 |
-
config_entry: ConfigEntry,
|
278 |
-
) -> config_entries.OptionsFlow: # pw-beta options
|
279 |
-
"""Get the options flow for this handler."""
|
280 |
-
return PlugwiseOptionsFlowHandler(config_entry)
|
281 |
-
|
282 |
-
|
283 |
-
# pw-beta - change the scan-interval via CONFIGURE
|
284 |
-
# pw-beta - add homekit emulation via CONFIGURE
|
285 |
-
# pw-beta - change the frontend refresh interval via CONFIGURE
|
286 |
-
class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): # pw-beta options
|
287 |
-
"""Plugwise option flow."""
|
288 |
-
|
289 |
-
def __init__(self, config_entry: ConfigEntry) -> None: # pragma: no cover
|
290 |
-
"""Initialize options flow."""
|
291 |
-
self.config_entry = config_entry
|
292 |
-
|
293 |
-
async def async_step_none(
|
294 |
-
self, user_input: dict[str, Any] | None = None
|
295 |
-
) -> FlowResult: # pragma: no cover
|
296 |
-
"""No options available."""
|
297 |
-
if user_input is not None:
|
298 |
-
# Apparently not possible to abort an options flow at the moment
|
299 |
-
return self.async_create_entry(title="", data=self.config_entry.options)
|
300 |
-
|
301 |
-
return self.async_show_form(step_id="none")
|
302 |
-
|
303 |
-
async def async_step_init(
|
304 |
-
self, user_input: dict[str, Any] | None = None
|
305 |
-
) -> FlowResult: # pragma: no cover
|
306 |
-
"""Manage the Plugwise options."""
|
307 |
-
if not self.config_entry.data.get(CONF_HOST):
|
308 |
-
return await self.async_step_none(user_input)
|
309 |
-
|
310 |
-
if user_input is not None:
|
311 |
-
return self.async_create_entry(title="", data=user_input)
|
312 |
-
|
313 |
-
coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][COORDINATOR]
|
314 |
-
interval: dt.timedelta = DEFAULT_SCAN_INTERVAL[
|
315 |
-
coordinator.api.smile_type
|
316 |
-
] # pw-beta options
|
317 |
-
|
318 |
-
data = {
|
319 |
-
vol.Optional(
|
320 |
-
CONF_SCAN_INTERVAL,
|
321 |
-
default=self.config_entry.options.get(
|
322 |
-
CONF_SCAN_INTERVAL, interval.seconds
|
323 |
-
),
|
324 |
-
): vol.All(cv.positive_int, vol.Clamp(min=10)),
|
325 |
-
} # pw-beta
|
326 |
-
|
327 |
-
if coordinator.api.smile_type != "thermostat":
|
328 |
-
return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
|
329 |
-
|
330 |
-
data.update(
|
331 |
-
{
|
332 |
-
vol.Optional(
|
333 |
-
CONF_HOMEKIT_EMULATION,
|
334 |
-
default=self.config_entry.options.get(
|
335 |
-
CONF_HOMEKIT_EMULATION, False
|
336 |
-
),
|
337 |
-
): cv.boolean,
|
338 |
-
vol.Optional(
|
339 |
-
CONF_REFRESH_INTERVAL,
|
340 |
-
default=self.config_entry.options.get(CONF_REFRESH_INTERVAL, 1.5),
|
341 |
-
): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
|
342 |
-
}
|
343 |
-
) # pw-beta
|
344 |
-
|
345 |
-
return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
|
|
|
1 |
"""Config flow for Plugwise integration."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
+
from typing import Any, Self
|
|
|
6 |
|
7 |
+
from plugwise import Smile
|
8 |
+
from plugwise.exceptions import (
|
9 |
+
ConnectionFailedError,
|
10 |
+
InvalidAuthentication,
|
11 |
+
InvalidSetupError,
|
12 |
+
InvalidXMLError,
|
13 |
+
ResponseError,
|
14 |
+
UnsupportedDeviceError,
|
15 |
+
)
|
16 |
import voluptuous as vol
|
17 |
|
|
|
18 |
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
19 |
+
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
20 |
from homeassistant.const import (
|
21 |
+
ATTR_CONFIGURATION_URL,
|
22 |
CONF_BASE,
|
23 |
CONF_HOST,
|
24 |
CONF_NAME,
|
25 |
CONF_PASSWORD,
|
26 |
CONF_PORT,
|
|
|
27 |
CONF_USERNAME,
|
28 |
)
|
29 |
+
from homeassistant.core import HomeAssistant
|
|
|
|
|
30 |
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
from .const import (
|
|
|
|
|
33 |
DEFAULT_PORT,
|
34 |
DEFAULT_USERNAME,
|
35 |
DOMAIN,
|
36 |
FLOW_SMILE,
|
37 |
FLOW_STRETCH,
|
|
|
38 |
SMILE,
|
39 |
STRETCH,
|
40 |
STRETCH_USERNAME,
|
41 |
ZEROCONF_MAP,
|
42 |
)
|
43 |
|
44 |
+
|
45 |
+
def base_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
"""Generate base schema for gateways."""
|
47 |
+
schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
48 |
+
|
49 |
if not discovery_info:
|
50 |
+
schema = schema.extend(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
{
|
52 |
+
vol.Required(CONF_HOST): str,
|
53 |
+
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
54 |
+
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
|
|
|
55 |
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
|
56 |
),
|
57 |
}
|
58 |
)
|
59 |
|
60 |
+
return schema
|
61 |
|
62 |
|
63 |
+
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
|
64 |
"""Validate whether the user input allows us to connect to the gateway.
|
65 |
|
66 |
+
Data has the keys from base_schema() with values provided by the user.
|
67 |
"""
|
68 |
websession = async_get_clientsession(hass, verify_ssl=False)
|
69 |
api = Smile(
|
|
|
71 |
password=data[CONF_PASSWORD],
|
72 |
port=data[CONF_PORT],
|
73 |
username=data[CONF_USERNAME],
|
|
|
74 |
websession=websession,
|
75 |
)
|
76 |
await api.connect()
|
|
|
83 |
VERSION = 1
|
84 |
|
85 |
discovery_info: ZeroconfServiceInfo | None = None
|
86 |
+
product: str = "Unknown Smile"
|
87 |
_username: str = DEFAULT_USERNAME
|
88 |
|
89 |
async def async_step_zeroconf(
|
90 |
self, discovery_info: ZeroconfServiceInfo
|
91 |
+
) -> ConfigFlowResult:
|
92 |
"""Prepare configuration for a discovered Plugwise Smile."""
|
93 |
self.discovery_info = discovery_info
|
94 |
_properties = discovery_info.properties
|
|
|
96 |
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
97 |
if config_entry := await self.async_set_unique_id(unique_id):
|
98 |
try:
|
99 |
+
await validate_input(
|
100 |
self.hass,
|
101 |
{
|
102 |
CONF_HOST: discovery_info.host,
|
|
|
105 |
CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
|
106 |
},
|
107 |
)
|
108 |
+
except Exception: # noqa: BLE001
|
109 |
self._abort_if_unique_id_configured()
|
110 |
else:
|
111 |
self._abort_if_unique_id_configured(
|
|
|
117 |
|
118 |
if DEFAULT_USERNAME not in unique_id:
|
119 |
self._username = STRETCH_USERNAME
|
120 |
+
self.product = _product = _properties.get("product", "Unknown Smile")
|
121 |
_version = _properties.get("version", "n/a")
|
122 |
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
123 |
|
|
|
129 |
# If we have discovered an Adam or Anna, both might be on the network.
|
130 |
# In that case, we need to cancel the Anna flow, as the Adam should
|
131 |
# be added.
|
132 |
+
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
133 |
+
return self.async_abort(reason="anna_with_adam")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
|
135 |
self.context.update(
|
136 |
{
|
137 |
+
"title_placeholders": {CONF_NAME: _name},
|
138 |
+
ATTR_CONFIGURATION_URL: (
|
139 |
+
f"http://{discovery_info.host}:{discovery_info.port}"
|
140 |
+
),
|
|
|
|
|
|
|
|
|
141 |
}
|
142 |
)
|
143 |
+
return await self.async_step_user()
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
|
145 |
+
def is_matching(self, other_flow: Self) -> bool:
|
146 |
+
"""Return True if other_flow is matching this flow."""
|
147 |
+
# This is an Anna, and there is already an Adam flow in progress
|
148 |
+
if self.product == "smile_thermo" and other_flow.product == "smile_open_therm":
|
149 |
+
return True
|
150 |
+
|
151 |
+
# This is an Adam, and there is already an Anna flow in progress
|
152 |
+
if self.product == "smile_open_therm" and other_flow.product == "smile_thermo":
|
153 |
+
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
|
155 |
+
return False
|
|
|
156 |
|
157 |
async def async_step_user(
|
158 |
self, user_input: dict[str, Any] | None = None
|
159 |
+
) -> ConfigFlowResult:
|
160 |
"""Handle the initial step when using network/gateway setups."""
|
161 |
errors: dict[str, str] = {}
|
162 |
+
|
163 |
if user_input is not None:
|
164 |
+
if self.discovery_info:
|
165 |
+
user_input[CONF_HOST] = self.discovery_info.host
|
166 |
+
user_input[CONF_PORT] = self.discovery_info.port
|
167 |
+
user_input[CONF_USERNAME] = self._username
|
168 |
|
169 |
+
try:
|
170 |
+
api = await validate_input(self.hass, user_input)
|
171 |
+
except ConnectionFailedError:
|
172 |
+
errors[CONF_BASE] = "cannot_connect"
|
173 |
+
except InvalidAuthentication:
|
174 |
+
errors[CONF_BASE] = "invalid_auth"
|
175 |
+
except InvalidSetupError:
|
176 |
+
errors[CONF_BASE] = "invalid_setup"
|
177 |
+
except (InvalidXMLError, ResponseError):
|
178 |
+
errors[CONF_BASE] = "response_error"
|
179 |
+
except UnsupportedDeviceError:
|
180 |
+
errors[CONF_BASE] = "unsupported"
|
181 |
+
except Exception: # noqa: BLE001
|
182 |
+
errors[CONF_BASE] = "unknown"
|
183 |
+
else:
|
184 |
+
await self.async_set_unique_id(
|
185 |
+
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
186 |
+
)
|
187 |
+
self._abort_if_unique_id_configured()
|
188 |
+
|
189 |
+
return self.async_create_entry(title=api.smile_name, data=user_input)
|
190 |
|
191 |
return self.async_show_form(
|
192 |
+
step_id=SOURCE_USER,
|
193 |
+
data_schema=base_schema(self.discovery_info),
|
194 |
errors=errors,
|
195 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,70 +1,39 @@
|
|
1 |
"""Constants for Plugwise component."""
|
|
|
|
|
|
|
2 |
from datetime import timedelta
|
3 |
import logging
|
4 |
-
from typing import Final
|
5 |
-
|
6 |
-
import voluptuous as vol # pw-beta usb
|
7 |
|
8 |
from homeassistant.const import Platform
|
9 |
-
from homeassistant.helpers import config_validation as cv
|
10 |
|
11 |
DOMAIN: Final = "plugwise"
|
12 |
|
13 |
LOGGER = logging.getLogger(__package__)
|
14 |
|
15 |
API: Final = "api"
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
CONF_MANUAL_PATH: Final = "Enter Manually"
|
20 |
GATEWAY: Final = "gateway"
|
|
|
|
|
21 |
PW_TYPE: Final = "plugwise_type"
|
|
|
22 |
SMILE: Final = "smile"
|
23 |
-
STICK: Final = "stick"
|
24 |
STRETCH: Final = "stretch"
|
25 |
STRETCH_USERNAME: Final = "stretch"
|
26 |
-
USB: Final = "usb"
|
27 |
-
|
28 |
-
FLOW_NET: Final = "Network: Smile/Stretch"
|
29 |
-
FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
|
30 |
-
FLOW_STRETCH: Final = "Stretch (Stretch)"
|
31 |
-
FLOW_TYPE: Final = "flow_type"
|
32 |
-
FLOW_USB: Final = "USB: Stick"
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
# Default directives
|
37 |
-
DEFAULT_PORT: Final = 80
|
38 |
-
DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
|
39 |
-
"power": timedelta(seconds=10),
|
40 |
-
"stretch": timedelta(seconds=60),
|
41 |
-
"thermostat": timedelta(seconds=60),
|
42 |
-
}
|
43 |
-
DEFAULT_TIMEOUT: Final = 10
|
44 |
-
DEFAULT_USERNAME: Final = "smile"
|
45 |
-
|
46 |
-
# --- Const for Plugwise Smile and Stretch
|
47 |
-
PLATFORMS_GATEWAY: Final[list[str]] = [
|
48 |
Platform.BINARY_SENSOR,
|
|
|
49 |
Platform.CLIMATE,
|
50 |
Platform.NUMBER,
|
51 |
Platform.SELECT,
|
52 |
Platform.SENSOR,
|
53 |
Platform.SWITCH,
|
54 |
]
|
55 |
-
SENSOR_PLATFORMS: Final[list[str]] = [Platform.SENSOR, Platform.SWITCH]
|
56 |
-
SERVICE_DELETE: Final = "delete_notification"
|
57 |
-
SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
|
58 |
-
|
59 |
-
# Climate const:
|
60 |
-
MASTER_THERMOSTATS: Final[list[str]] = [
|
61 |
-
"thermostat",
|
62 |
-
"zone_thermometer",
|
63 |
-
"zone_thermostat",
|
64 |
-
"thermostatic_radiator_valve",
|
65 |
-
]
|
66 |
-
|
67 |
-
# Config_flow const:
|
68 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
69 |
"smile": "Smile P1",
|
70 |
"smile_thermo": "Smile Anna",
|
@@ -72,149 +41,39 @@
|
|
72 |
"stretch": "Stretch",
|
73 |
}
|
74 |
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
PW_NOTIFICATION: Final = "plugwise_notification"
|
81 |
-
SLAVE_BOILER_STATE: Final = "slave_boiler_state"
|
82 |
-
|
83 |
-
# Sensors:
|
84 |
-
BATTERY: Final = "battery"
|
85 |
-
CURRENT_TEMP: Final = "temperature"
|
86 |
-
DEVICE_STATE: Final = "device_state"
|
87 |
-
DHW_TEMP: Final = "dhw_temperature"
|
88 |
-
EL_CONSUMED: Final = "electricity_consumed"
|
89 |
-
EL_CONSUMED_INTERVAL: Final = "electricity_consumed_interval"
|
90 |
-
EL_CONSUMED_OFF_PEAK_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
|
91 |
-
EL_CONSUMED_OFF_PEAK_INTERVAL: Final = "electricity_consumed_off_peak_interval"
|
92 |
-
EL_CONSUMED_OFF_PEAK_POINT: Final = "electricity_consumed_off_peak_point"
|
93 |
-
EL_CONSUMED_PEAK_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
|
94 |
-
EL_CONSUMED_PEAK_INTERVAL: Final = "electricity_consumed_peak_interval"
|
95 |
-
EL_CONSUMED_PEAK_POINT: Final = "electricity_consumed_peak_point"
|
96 |
-
EL_CONSUMED_POINT: Final = "electricity_consumed_point"
|
97 |
-
EL_PHASE_ONE_CONSUMED: Final = "electricity_phase_one_consumed"
|
98 |
-
EL_PHASE_TWO_CONSUMED: Final = "electricity_phase_two_consumed"
|
99 |
-
EL_PHASE_THREE_CONSUMED: Final = "electricity_phase_three_consumed"
|
100 |
-
EL_PHASE_ONE_PRODUCED: Final = "electricity_phase_one_produced"
|
101 |
-
EL_PHASE_TWO_PRODUCED: Final = "electricity_phase_two_produced"
|
102 |
-
EL_PHASE_THREE_PRODUCED: Final = "electricity_phase_three_produced"
|
103 |
-
EL_PRODUCED: Final = "electricity_produced"
|
104 |
-
EL_PRODUCED_INTERVAL: Final = "electricity_produced_interval"
|
105 |
-
EL_PRODUCED_OFF_PEAK_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
|
106 |
-
EL_PRODUCED_OFF_PEAK_INTERVAL: Final = "electricity_produced_off_peak_interval"
|
107 |
-
EL_PRODUCED_OFF_PEAK_POINT: Final = "electricity_produced_off_peak_point"
|
108 |
-
EL_PRODUCED_PEAK_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
|
109 |
-
EL_PRODUCED_PEAK_INTERVAL: Final = "electricity_produced_peak_interval"
|
110 |
-
EL_PRODUCED_PEAK_POINT: Final = "electricity_produced_peak_point"
|
111 |
-
EL_PRODUCED_POINT: Final = "electricity_produced_point"
|
112 |
-
GAS_CONSUMED_CUMULATIVE: Final = "gas_consumed_cumulative"
|
113 |
-
GAS_CONSUMED_INTERVAL: Final = "gas_consumed_interval"
|
114 |
-
HUMIDITY: Final = "humidity"
|
115 |
-
INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
|
116 |
-
MOD_LEVEL: Final = "modulation_level"
|
117 |
-
NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
|
118 |
-
NET_EL_POINT: Final = "net_electricity_point"
|
119 |
-
OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
|
120 |
-
OUTDOOR_TEMP: Final = "outdoor_temperature"
|
121 |
-
RETURN_TEMP: Final = "return_temperature"
|
122 |
-
TARGET_TEMP: Final = "setpoint"
|
123 |
-
TARGET_TEMP_HIGH: Final = "setpoint_high"
|
124 |
-
TARGET_TEMP_LOW: Final = "setpoint_low"
|
125 |
-
TEMP_DIFF: Final = "temperature_difference"
|
126 |
-
VALVE_POS: Final = "valve_position"
|
127 |
-
V_PHASE_ONE: Final = "voltage_phase_one"
|
128 |
-
V_PHASE_TWO: Final = "voltage_phase_two"
|
129 |
-
V_PHASE_THREE: Final = "voltage_phase_three"
|
130 |
-
WATER_PRESSURE: Final = "water_pressure"
|
131 |
-
WATER_TEMP: Final = "water_temperature"
|
132 |
-
|
133 |
-
# Switches
|
134 |
-
COOLING_ENABLED = "cooling_ena_switch"
|
135 |
-
DHW_COMF_MODE: Final = "dhw_cm_switch"
|
136 |
-
LOCK: Final = "lock"
|
137 |
-
RELAY: Final = "relay"
|
138 |
-
|
139 |
-
|
140 |
-
# --- Const for Plugwise USB-stick.
|
141 |
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
]
|
147 |
-
CONF_USB_PATH: Final = "usb_path"
|
148 |
|
149 |
-
#
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
ATTR_MAC_ADDRESS: Final = "mac"
|
158 |
-
|
159 |
-
SERVICE_USB_DEVICE_ADD: Final = "device_add"
|
160 |
-
SERVICE_USB_DEVICE_REMOVE: Final = "device_remove"
|
161 |
-
SERVICE_USB_DEVICE_SCHEMA: Final = vol.Schema(
|
162 |
-
{vol.Required(ATTR_MAC_ADDRESS): cv.string}
|
163 |
-
) # pw-beta usb
|
164 |
-
|
165 |
-
|
166 |
-
# USB Relay device constants
|
167 |
-
USB_RELAY_ID: Final = "relay"
|
168 |
-
|
169 |
-
|
170 |
-
# USB SED (battery powered) device constants
|
171 |
-
ATTR_SED_STAY_ACTIVE: Final = "stay_active"
|
172 |
-
ATTR_SED_SLEEP_FOR: Final = "sleep_for"
|
173 |
-
ATTR_SED_MAINTENANCE_INTERVAL: Final = "maintenance_interval"
|
174 |
-
ATTR_SED_CLOCK_SYNC: Final = "clock_sync"
|
175 |
-
ATTR_SED_CLOCK_INTERVAL: Final = "clock_interval"
|
176 |
-
|
177 |
-
SERVICE_USB_SED_BATTERY_CONFIG: Final = "configure_battery_savings"
|
178 |
-
SERVICE_USB_SED_BATTERY_CONFIG_SCHEMA: Final = {
|
179 |
-
vol.Required(ATTR_SED_STAY_ACTIVE): vol.All(
|
180 |
-
vol.Coerce(int), vol.Range(min=1, max=120)
|
181 |
-
),
|
182 |
-
vol.Required(ATTR_SED_SLEEP_FOR): vol.All(
|
183 |
-
vol.Coerce(int), vol.Range(min=10, max=60)
|
184 |
-
),
|
185 |
-
vol.Required(ATTR_SED_MAINTENANCE_INTERVAL): vol.All(
|
186 |
-
vol.Coerce(int), vol.Range(min=5, max=1440)
|
187 |
-
),
|
188 |
-
vol.Required(ATTR_SED_CLOCK_SYNC): cv.boolean,
|
189 |
-
vol.Required(ATTR_SED_CLOCK_INTERVAL): vol.All(
|
190 |
-
vol.Coerce(int), vol.Range(min=60, max=10080)
|
191 |
-
),
|
192 |
}
|
|
|
193 |
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
ATTR_SCAN_SENSITIVITY_MODE: Final = "sensitivity_mode"
|
200 |
-
ATTR_SCAN_RESET_TIMER: Final = "reset_timer"
|
201 |
-
|
202 |
-
SCAN_SENSITIVITY_HIGH: Final = "high"
|
203 |
-
SCAN_SENSITIVITY_MEDIUM: Final = "medium"
|
204 |
-
SCAN_SENSITIVITY_OFF: Final = "off"
|
205 |
-
SCAN_SENSITIVITY_MODES = [
|
206 |
-
SCAN_SENSITIVITY_HIGH,
|
207 |
-
SCAN_SENSITIVITY_MEDIUM,
|
208 |
-
SCAN_SENSITIVITY_OFF,
|
209 |
]
|
210 |
-
|
211 |
-
SERVICE_USB_SCAN_CONFIG: Final = "configure_scan"
|
212 |
-
SERVICE_USB_SCAN_CONFIG_SCHEMA = (
|
213 |
-
{
|
214 |
-
vol.Required(ATTR_SCAN_SENSITIVITY_MODE): vol.In(SCAN_SENSITIVITY_MODES),
|
215 |
-
vol.Required(ATTR_SCAN_RESET_TIMER): vol.All(
|
216 |
-
vol.Coerce(int), vol.Range(min=1, max=240)
|
217 |
-
),
|
218 |
-
vol.Required(ATTR_SCAN_DAYLIGHT_MODE): cv.boolean,
|
219 |
-
},
|
220 |
-
)
|
|
|
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 |
+
FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
|
17 |
+
FLOW_STRETCH: Final = "stretch (Stretch)"
|
18 |
+
FLOW_TYPE: Final = "flow_type"
|
|
|
19 |
GATEWAY: Final = "gateway"
|
20 |
+
GATEWAY_ID: Final = "gateway_id"
|
21 |
+
LOCATION: Final = "location"
|
22 |
PW_TYPE: Final = "plugwise_type"
|
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,
|
31 |
Platform.CLIMATE,
|
32 |
Platform.NUMBER,
|
33 |
Platform.SELECT,
|
34 |
Platform.SENSOR,
|
35 |
Platform.SWITCH,
|
36 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
ZEROCONF_MAP: Final[dict[str, str]] = {
|
38 |
"smile": "Smile P1",
|
39 |
"smile_thermo": "Smile Anna",
|
|
|
41 |
"stretch": "Stretch",
|
42 |
}
|
43 |
|
44 |
+
type NumberType = Literal[
|
45 |
+
"maximum_boiler_temperature",
|
46 |
+
"max_dhw_temperature",
|
47 |
+
"temperature_offset",
|
48 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
|
50 |
+
type SelectType = Literal[
|
51 |
+
"select_dhw_mode",
|
52 |
+
"select_gateway_mode",
|
53 |
+
"select_regulation_mode",
|
54 |
+
"select_schedule",
|
55 |
+
]
|
56 |
+
type SelectOptionsType = Literal[
|
57 |
+
"dhw_modes",
|
58 |
+
"gateway_modes",
|
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,24 +1,28 @@
|
|
1 |
"""DataUpdateCoordinator for Plugwise."""
|
|
|
2 |
from datetime import timedelta
|
3 |
|
4 |
-
from
|
5 |
-
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
6 |
-
from homeassistant.const import CONF_SCAN_INTERVAL # pw-beta options
|
7 |
-
from homeassistant.core import HomeAssistant
|
8 |
-
from homeassistant.exceptions import ConfigEntryError
|
9 |
-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
10 |
-
from homeassistant.helpers.debounce import Debouncer
|
11 |
-
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
12 |
from plugwise import PlugwiseData, Smile
|
13 |
from plugwise.exceptions import (
|
14 |
ConnectionFailedError,
|
15 |
InvalidAuthentication,
|
16 |
InvalidXMLError,
|
|
|
17 |
ResponseError,
|
18 |
UnsupportedDeviceError,
|
19 |
)
|
20 |
|
21 |
-
from .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
|
24 |
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
@@ -26,85 +30,103 @@
|
|
26 |
|
27 |
_connected: bool = False
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
entry: ConfigEntry,
|
33 |
-
cooldown: float,
|
34 |
-
update_interval: timedelta = timedelta(seconds=60),
|
35 |
-
) -> None: # pw-beta cooldown
|
36 |
"""Initialize the coordinator."""
|
37 |
super().__init__(
|
38 |
hass,
|
39 |
LOGGER,
|
40 |
name=DOMAIN,
|
41 |
-
|
42 |
-
update_interval=update_interval,
|
43 |
# Don't refresh immediately, give the device time to process
|
44 |
# the change in state before we query it.
|
45 |
request_refresh_debouncer=Debouncer(
|
46 |
hass,
|
47 |
LOGGER,
|
48 |
-
cooldown=
|
49 |
immediate=False,
|
50 |
),
|
51 |
)
|
52 |
|
53 |
self.api = Smile(
|
54 |
-
host=
|
55 |
-
username=
|
56 |
-
password=
|
57 |
-
port=
|
58 |
-
timeout=30,
|
59 |
websession=async_get_clientsession(hass, verify_ssl=False),
|
60 |
)
|
61 |
-
self.
|
62 |
-
self.
|
63 |
-
self.update_interval = update_interval
|
64 |
|
65 |
async def _connect(self) -> None:
|
66 |
"""Connect to the Plugwise Smile."""
|
67 |
-
|
68 |
-
self.
|
69 |
-
|
70 |
-
|
71 |
-
self.api.smile_type, timedelta(seconds=60)
|
72 |
-
) # pw-beta options scan-interval
|
73 |
-
if (custom_time := self._entry.options.get(CONF_SCAN_INTERVAL)) is not None:
|
74 |
-
self.update_interval = timedelta(
|
75 |
-
seconds=int(custom_time)
|
76 |
-
) # pragma: no cover # pw-beta options
|
77 |
-
|
78 |
-
LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
|
79 |
|
80 |
async def _async_update_data(self) -> PlugwiseData:
|
81 |
"""Fetch data from Plugwise."""
|
82 |
-
data = PlugwiseData(
|
83 |
-
|
84 |
try:
|
85 |
if not self._connected:
|
86 |
await self._connect()
|
87 |
data = await self.api.async_update()
|
88 |
-
|
89 |
-
|
90 |
-
self._unavailable_logged = False
|
91 |
except InvalidAuthentication as err:
|
92 |
-
|
93 |
-
self._unavailable_logged = True
|
94 |
-
raise ConfigEntryError("Authentication failed") from err
|
95 |
except (InvalidXMLError, ResponseError) as err:
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
except UnsupportedDeviceError as err:
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
except ConnectionFailedError as err:
|
106 |
-
if not self._unavailable_logged: # pw-beta add to Core
|
107 |
-
self._unavailable_logged = True
|
108 |
-
raise UpdateFailed("Failed to connect") from err
|
109 |
|
110 |
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
"""DataUpdateCoordinator for Plugwise."""
|
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,
|
9 |
InvalidAuthentication,
|
10 |
InvalidXMLError,
|
11 |
+
PlugwiseError,
|
12 |
ResponseError,
|
13 |
UnsupportedDeviceError,
|
14 |
)
|
15 |
|
16 |
+
from homeassistant.config_entries import ConfigEntry
|
17 |
+
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
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 DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER
|
26 |
|
27 |
|
28 |
class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
|
|
|
30 |
|
31 |
_connected: bool = False
|
32 |
|
33 |
+
config_entry: ConfigEntry
|
34 |
+
|
35 |
+
def __init__(self, hass: HomeAssistant) -> None:
|
|
|
|
|
|
|
|
|
36 |
"""Initialize the coordinator."""
|
37 |
super().__init__(
|
38 |
hass,
|
39 |
LOGGER,
|
40 |
name=DOMAIN,
|
41 |
+
update_interval=timedelta(seconds=60),
|
|
|
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=1.5,
|
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.get(CONF_PORT, DEFAULT_PORT),
|
|
|
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."""
|
64 |
+
version = await self.api.connect()
|
65 |
+
self._connected = isinstance(version, Version)
|
66 |
+
if self._connected:
|
67 |
+
self.api.get_all_devices()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
async def _async_update_data(self) -> PlugwiseData:
|
70 |
"""Fetch data from Plugwise."""
|
71 |
+
data = PlugwiseData({}, {})
|
|
|
72 |
try:
|
73 |
if not self._connected:
|
74 |
await self._connect()
|
75 |
data = await self.api.async_update()
|
76 |
+
except ConnectionFailedError as err:
|
77 |
+
raise UpdateFailed("Failed to connect") from err
|
|
|
78 |
except InvalidAuthentication as err:
|
79 |
+
raise ConfigEntryError("Authentication failed") from err
|
|
|
|
|
80 |
except (InvalidXMLError, ResponseError) as err:
|
81 |
+
raise UpdateFailed(
|
82 |
+
"Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
|
83 |
+
) from err
|
84 |
+
except PlugwiseError as err:
|
85 |
+
raise UpdateFailed("Data incomplete or missing") from err
|
86 |
except UnsupportedDeviceError as err:
|
87 |
+
raise ConfigEntryError("Device with unsupported firmware") from err
|
88 |
+
else:
|
89 |
+
self._async_add_remove_devices(data, self.config_entry)
|
|
|
|
|
|
|
|
|
90 |
|
91 |
return data
|
92 |
+
|
93 |
+
def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
|
94 |
+
"""Add new Plugwise devices, remove non-existing devices."""
|
95 |
+
# Check for new or removed devices
|
96 |
+
self.new_devices = set(data.devices) - self._current_devices
|
97 |
+
removed_devices = self._current_devices - set(data.devices)
|
98 |
+
self._current_devices = set(data.devices)
|
99 |
+
|
100 |
+
if removed_devices:
|
101 |
+
self._async_remove_devices(data, entry)
|
102 |
+
|
103 |
+
def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
|
104 |
+
"""Clean registries when removed devices found."""
|
105 |
+
device_reg = dr.async_get(self.hass)
|
106 |
+
device_list = dr.async_entries_for_config_entry(
|
107 |
+
device_reg, self.config_entry.entry_id
|
108 |
+
)
|
109 |
+
# First find the Plugwise via_device
|
110 |
+
gateway_device = device_reg.async_get_device(
|
111 |
+
{(DOMAIN, data.gateway[GATEWAY_ID])}
|
112 |
+
)
|
113 |
+
assert gateway_device is not None
|
114 |
+
via_device_id = gateway_device.id
|
115 |
+
|
116 |
+
# Then remove the connected orphaned device(s)
|
117 |
+
for device_entry in device_list:
|
118 |
+
for identifier in device_entry.identifiers:
|
119 |
+
if identifier[0] == DOMAIN:
|
120 |
+
if (
|
121 |
+
device_entry.via_device_id == via_device_id
|
122 |
+
and identifier[1] not in data.devices
|
123 |
+
):
|
124 |
+
device_reg.async_update_device(
|
125 |
+
device_entry.id, remove_config_entry_id=entry.entry_id
|
126 |
+
)
|
127 |
+
LOGGER.debug(
|
128 |
+
"Removed %s device %s %s from device_registry",
|
129 |
+
DOMAIN,
|
130 |
+
device_entry.model,
|
131 |
+
identifier[1],
|
132 |
+
)
|
@@ -1,23 +1,19 @@
|
|
1 |
"""Diagnostics support for Plugwise."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
from typing import Any
|
5 |
|
6 |
-
from homeassistant.config_entries import ConfigEntry
|
7 |
from homeassistant.core import HomeAssistant
|
8 |
|
9 |
-
from .
|
10 |
-
from .const import DOMAIN
|
11 |
-
from .coordinator import PlugwiseDataUpdateCoordinator
|
12 |
|
13 |
|
14 |
async def async_get_config_entry_diagnostics(
|
15 |
-
hass: HomeAssistant, entry:
|
16 |
) -> dict[str, Any]:
|
17 |
"""Return diagnostics for a config entry."""
|
18 |
-
coordinator
|
19 |
-
COORDINATOR
|
20 |
-
]
|
21 |
return {
|
22 |
"gateway": coordinator.data.gateway,
|
23 |
"devices": coordinator.data.devices,
|
|
|
1 |
"""Diagnostics support for Plugwise."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
from typing import Any
|
6 |
|
|
|
7 |
from homeassistant.core import HomeAssistant
|
8 |
|
9 |
+
from . import PlugwiseConfigEntry
|
|
|
|
|
10 |
|
11 |
|
12 |
async def async_get_config_entry_diagnostics(
|
13 |
+
hass: HomeAssistant, entry: PlugwiseConfigEntry
|
14 |
) -> dict[str, Any]:
|
15 |
"""Return diagnostics for a config entry."""
|
16 |
+
coordinator = entry.runtime_data
|
|
|
|
|
17 |
return {
|
18 |
"gateway": coordinator.data.gateway,
|
19 |
"devices": coordinator.data.devices,
|
@@ -1,14 +1,16 @@
|
|
1 |
"""Generic Plugwise Entity Class."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
|
|
|
|
4 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
5 |
from homeassistant.helpers.device_registry import (
|
6 |
CONNECTION_NETWORK_MAC,
|
7 |
CONNECTION_ZIGBEE,
|
|
|
8 |
)
|
9 |
-
from homeassistant.helpers.entity import DeviceInfo
|
10 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
11 |
-
from plugwise.constants import DeviceData
|
12 |
|
13 |
from .const import DOMAIN
|
14 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
@@ -45,6 +47,7 @@
|
|
45 |
connections=connections,
|
46 |
manufacturer=data.get("vendor"),
|
47 |
model=data.get("model"),
|
|
|
48 |
name=coordinator.data.gateway["smile_name"],
|
49 |
sw_version=data.get("firmware"),
|
50 |
hw_version=data.get("hardware"),
|
@@ -66,7 +69,7 @@
|
|
66 |
"""Return if entity is available."""
|
67 |
return (
|
68 |
self._dev_id in self.coordinator.data.devices
|
69 |
-
and ("available" not in self.device or self.device["available"])
|
70 |
and super().available
|
71 |
)
|
72 |
|
|
|
1 |
"""Generic Plugwise Entity Class."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
+
from plugwise.constants import DeviceData
|
6 |
+
|
7 |
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
8 |
from homeassistant.helpers.device_registry import (
|
9 |
CONNECTION_NETWORK_MAC,
|
10 |
CONNECTION_ZIGBEE,
|
11 |
+
DeviceInfo,
|
12 |
)
|
|
|
13 |
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
14 |
|
15 |
from .const import DOMAIN
|
16 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
|
|
47 |
connections=connections,
|
48 |
manufacturer=data.get("vendor"),
|
49 |
model=data.get("model"),
|
50 |
+
model_id=data.get("model_id"),
|
51 |
name=coordinator.data.gateway["smile_name"],
|
52 |
sw_version=data.get("firmware"),
|
53 |
hw_version=data.get("hardware"),
|
|
|
69 |
"""Return if entity is available."""
|
70 |
return (
|
71 |
self._dev_id in self.coordinator.data.devices
|
72 |
+
and ("available" not in self.device or self.device["available"] is True)
|
73 |
and super().available
|
74 |
)
|
75 |
|
@@ -1,13 +1,12 @@
|
|
1 |
{
|
2 |
"domain": "plugwise",
|
3 |
-
"name": "Plugwise
|
4 |
-
"
|
5 |
-
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra"],
|
6 |
"config_flow": true,
|
7 |
-
"documentation": "https://
|
8 |
"integration_type": "hub",
|
9 |
"iot_class": "local_polling",
|
10 |
"loggers": ["plugwise"],
|
11 |
-
"requirements": ["
|
12 |
-
"
|
13 |
}
|
|
|
1 |
{
|
2 |
"domain": "plugwise",
|
3 |
+
"name": "Plugwise",
|
4 |
+
"codeowners": ["@CoMPaTech", "@bouwew", "@frenck"],
|
|
|
5 |
"config_flow": true,
|
6 |
+
"documentation": "https://www.home-assistant.io/integrations/plugwise",
|
7 |
"integration_type": "hub",
|
8 |
"iot_class": "local_polling",
|
9 |
"loggers": ["plugwise"],
|
10 |
+
"requirements": ["plugwise==1.5.0"],
|
11 |
+
"zeroconf": ["_plugwise._tcp.local."]
|
12 |
}
|
@@ -1,7 +1,7 @@
|
|
1 |
"""Number platform for Plugwise integration."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
-
from collections.abc import Awaitable, Callable
|
5 |
from dataclasses import dataclass
|
6 |
|
7 |
from homeassistant.components.number import (
|
@@ -10,86 +10,72 @@
|
|
10 |
NumberEntityDescription,
|
11 |
NumberMode,
|
12 |
)
|
13 |
-
from homeassistant.
|
14 |
-
from homeassistant.
|
15 |
-
from homeassistant.core import HomeAssistant
|
16 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
17 |
-
from plugwise import Smile
|
18 |
|
19 |
-
from .
|
20 |
-
from .const import
|
21 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
22 |
from .entity import PlugwiseEntity
|
|
|
23 |
|
24 |
|
25 |
-
@dataclass
|
26 |
-
class
|
27 |
-
"""Mixin values for Plugwse entities."""
|
28 |
-
|
29 |
-
command: Callable[[Smile, str, float], Awaitable[None]]
|
30 |
-
|
31 |
-
|
32 |
-
@dataclass
|
33 |
-
class PlugwiseNumberEntityDescription(
|
34 |
-
NumberEntityDescription, PlugwiseEntityDescriptionMixin
|
35 |
-
):
|
36 |
"""Class describing Plugwise Number entities."""
|
37 |
|
38 |
-
|
39 |
-
native_min_value_key: str | None = None
|
40 |
-
native_step_key: str | None = None
|
41 |
-
native_value_key: str | None = None
|
42 |
|
43 |
|
44 |
NUMBER_TYPES = (
|
45 |
PlugwiseNumberEntityDescription(
|
46 |
key="maximum_boiler_temperature",
|
47 |
-
|
48 |
device_class=NumberDeviceClass.TEMPERATURE,
|
49 |
-
name="Maximum boiler temperature setpoint",
|
50 |
entity_category=EntityCategory.CONFIG,
|
51 |
-
|
52 |
-
native_min_value_key="lower_bound",
|
53 |
-
native_step_key="resolution",
|
54 |
-
native_unit_of_measurement=TEMP_CELSIUS,
|
55 |
-
native_value_key="setpoint",
|
56 |
),
|
57 |
PlugwiseNumberEntityDescription(
|
58 |
key="max_dhw_temperature",
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
device_class=NumberDeviceClass.TEMPERATURE,
|
61 |
-
name="Domestic hot water setpoint",
|
62 |
entity_category=EntityCategory.CONFIG,
|
63 |
-
|
64 |
-
native_min_value_key="lower_bound",
|
65 |
-
native_step_key="resolution",
|
66 |
-
native_unit_of_measurement=TEMP_CELSIUS,
|
67 |
-
native_value_key="setpoint",
|
68 |
),
|
69 |
)
|
70 |
|
71 |
|
72 |
async def async_setup_entry(
|
73 |
hass: HomeAssistant,
|
74 |
-
|
75 |
async_add_entities: AddEntitiesCallback,
|
76 |
) -> None:
|
77 |
"""Set up Plugwise number platform."""
|
|
|
78 |
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
|
92 |
-
|
|
|
93 |
|
94 |
|
95 |
class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
@@ -105,47 +91,26 @@
|
|
105 |
) -> None:
|
106 |
"""Initiate Plugwise Number."""
|
107 |
super().__init__(coordinator, device_id)
|
|
|
108 |
self.entity_description = description
|
109 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
110 |
self._attr_mode = NumberMode.BOX
|
|
|
|
|
111 |
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
self.device[self.entity_description.key][
|
117 |
-
self.entity_description.native_step_key
|
118 |
-
],
|
119 |
-
1,
|
120 |
-
)
|
121 |
|
122 |
@property
|
123 |
def native_value(self) -> float:
|
124 |
"""Return the present setpoint value."""
|
125 |
-
return self.device[self.entity_description.key][
|
126 |
-
self.entity_description.native_value_key
|
127 |
-
]
|
128 |
-
|
129 |
-
@property
|
130 |
-
def native_min_value(self) -> float:
|
131 |
-
"""Return the setpoint min. value."""
|
132 |
-
return self.device[self.entity_description.key][
|
133 |
-
self.entity_description.native_min_value_key
|
134 |
-
]
|
135 |
-
|
136 |
-
@property
|
137 |
-
def native_max_value(self) -> float:
|
138 |
-
"""Return the setpoint max. value."""
|
139 |
-
return self.device[self.entity_description.key][
|
140 |
-
self.entity_description.native_max_value_key
|
141 |
-
]
|
142 |
|
|
|
143 |
async def async_set_native_value(self, value: float) -> None:
|
144 |
"""Change to the new setpoint value."""
|
145 |
-
await self.
|
146 |
-
self.
|
147 |
-
)
|
148 |
-
LOGGER.debug(
|
149 |
-
"Setting %s to %s was successful", self.entity_description.name, value
|
150 |
)
|
151 |
-
await self.coordinator.async_request_refresh()
|
|
|
1 |
"""Number platform for Plugwise integration."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
|
|
5 |
from dataclasses import dataclass
|
6 |
|
7 |
from homeassistant.components.number import (
|
|
|
10 |
NumberEntityDescription,
|
11 |
NumberMode,
|
12 |
)
|
13 |
+
from homeassistant.const import EntityCategory, UnitOfTemperature
|
14 |
+
from homeassistant.core import HomeAssistant, callback
|
|
|
15 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
16 |
|
17 |
+
from . import PlugwiseConfigEntry
|
18 |
+
from .const import NumberType
|
19 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
20 |
from .entity import PlugwiseEntity
|
21 |
+
from .util import plugwise_command
|
22 |
|
23 |
|
24 |
+
@dataclass(frozen=True, kw_only=True)
|
25 |
+
class PlugwiseNumberEntityDescription(NumberEntityDescription):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
"""Class describing Plugwise Number entities."""
|
27 |
|
28 |
+
key: NumberType
|
|
|
|
|
|
|
29 |
|
30 |
|
31 |
NUMBER_TYPES = (
|
32 |
PlugwiseNumberEntityDescription(
|
33 |
key="maximum_boiler_temperature",
|
34 |
+
translation_key="maximum_boiler_temperature",
|
35 |
device_class=NumberDeviceClass.TEMPERATURE,
|
|
|
36 |
entity_category=EntityCategory.CONFIG,
|
37 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
|
|
38 |
),
|
39 |
PlugwiseNumberEntityDescription(
|
40 |
key="max_dhw_temperature",
|
41 |
+
translation_key="max_dhw_temperature",
|
42 |
+
device_class=NumberDeviceClass.TEMPERATURE,
|
43 |
+
entity_category=EntityCategory.CONFIG,
|
44 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
45 |
+
),
|
46 |
+
PlugwiseNumberEntityDescription(
|
47 |
+
key="temperature_offset",
|
48 |
+
translation_key="temperature_offset",
|
49 |
device_class=NumberDeviceClass.TEMPERATURE,
|
|
|
50 |
entity_category=EntityCategory.CONFIG,
|
51 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
|
|
|
|
|
|
|
52 |
),
|
53 |
)
|
54 |
|
55 |
|
56 |
async def async_setup_entry(
|
57 |
hass: HomeAssistant,
|
58 |
+
entry: PlugwiseConfigEntry,
|
59 |
async_add_entities: AddEntitiesCallback,
|
60 |
) -> None:
|
61 |
"""Set up Plugwise number platform."""
|
62 |
+
coordinator = entry.runtime_data
|
63 |
|
64 |
+
@callback
|
65 |
+
def _add_entities() -> None:
|
66 |
+
"""Add Entities."""
|
67 |
+
if not coordinator.new_devices:
|
68 |
+
return
|
69 |
+
|
70 |
+
async_add_entities(
|
71 |
+
PlugwiseNumberEntity(coordinator, device_id, description)
|
72 |
+
for device_id in coordinator.new_devices
|
73 |
+
for description in NUMBER_TYPES
|
74 |
+
if description.key in coordinator.data.devices[device_id]
|
75 |
+
)
|
76 |
|
77 |
+
_add_entities()
|
78 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
79 |
|
80 |
|
81 |
class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
|
|
|
91 |
) -> None:
|
92 |
"""Initiate Plugwise Number."""
|
93 |
super().__init__(coordinator, device_id)
|
94 |
+
self.device_id = device_id
|
95 |
self.entity_description = description
|
96 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
97 |
self._attr_mode = NumberMode.BOX
|
98 |
+
self._attr_native_max_value = self.device[description.key]["upper_bound"]
|
99 |
+
self._attr_native_min_value = self.device[description.key]["lower_bound"]
|
100 |
|
101 |
+
native_step = self.device[description.key]["resolution"]
|
102 |
+
if description.key != "temperature_offset":
|
103 |
+
native_step = max(native_step, 0.5)
|
104 |
+
self._attr_native_step = native_step
|
|
|
|
|
|
|
|
|
|
|
105 |
|
106 |
@property
|
107 |
def native_value(self) -> float:
|
108 |
"""Return the present setpoint value."""
|
109 |
+
return self.device[self.entity_description.key]["setpoint"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
111 |
+
@plugwise_command
|
112 |
async def async_set_native_value(self, value: float) -> None:
|
113 |
"""Change to the new setpoint value."""
|
114 |
+
await self.coordinator.api.set_number(
|
115 |
+
self.device_id, self.entity_description.key, value
|
|
|
|
|
|
|
116 |
)
|
|
@@ -1,96 +1,79 @@
|
|
1 |
"""Plugwise Select component for Home Assistant."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
-
from collections.abc import Awaitable, Callable
|
5 |
from dataclasses import dataclass
|
6 |
-
from typing import Any
|
7 |
|
8 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
9 |
-
from homeassistant.config_entries import ConfigEntry
|
10 |
from homeassistant.const import STATE_ON, EntityCategory
|
11 |
-
from homeassistant.core import HomeAssistant
|
12 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
13 |
-
from plugwise import Smile
|
14 |
|
15 |
-
from .
|
16 |
-
from .const import
|
17 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
18 |
from .entity import PlugwiseEntity
|
|
|
19 |
|
20 |
-
PARALLEL_UPDATES = 0
|
21 |
-
|
22 |
-
|
23 |
-
@dataclass
|
24 |
-
class PlugwiseSelectDescriptionMixin:
|
25 |
-
"""Mixin values for Plugwise Select entities."""
|
26 |
-
|
27 |
-
command: Callable[[Smile, str, str], Awaitable[Any]]
|
28 |
-
current_option_key: str
|
29 |
-
options_key: str
|
30 |
|
|
|
|
|
|
|
31 |
|
32 |
-
|
33 |
-
|
34 |
-
SelectEntityDescription, PlugwiseSelectDescriptionMixin
|
35 |
-
):
|
36 |
-
"""Class describing Plugwise Number entities."""
|
37 |
|
38 |
|
39 |
SELECT_TYPES = (
|
40 |
PlugwiseSelectEntityDescription(
|
41 |
key="select_schedule",
|
42 |
-
|
43 |
-
icon="mdi:calendar-clock",
|
44 |
-
command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
|
45 |
-
current_option_key="selected_schedule",
|
46 |
options_key="available_schedules",
|
47 |
),
|
48 |
PlugwiseSelectEntityDescription(
|
49 |
key="select_regulation_mode",
|
50 |
-
name="Regulation mode",
|
51 |
-
icon="mdi:hvac",
|
52 |
-
entity_category=EntityCategory.CONFIG,
|
53 |
translation_key="regulation_mode",
|
54 |
-
|
55 |
-
current_option_key="regulation_mode",
|
56 |
options_key="regulation_modes",
|
57 |
),
|
58 |
PlugwiseSelectEntityDescription(
|
59 |
key="select_dhw_mode",
|
60 |
-
name="DHW mode",
|
61 |
-
icon="mdi:shower",
|
62 |
-
entity_category=EntityCategory.CONFIG,
|
63 |
translation_key="dhw_mode",
|
64 |
-
|
65 |
-
current_option_key="dhw_mode",
|
66 |
options_key="dhw_modes",
|
67 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
)
|
69 |
|
70 |
|
71 |
async def async_setup_entry(
|
72 |
hass: HomeAssistant,
|
73 |
-
|
74 |
async_add_entities: AddEntitiesCallback,
|
75 |
) -> None:
|
76 |
"""Set up the Smile selector from a config entry."""
|
77 |
-
coordinator
|
78 |
-
config_entry.entry_id
|
79 |
-
][COORDINATOR]
|
80 |
-
|
81 |
-
entities: list[PlugwiseSelectEntity] = []
|
82 |
-
for device_id, device in coordinator.data.devices.items():
|
83 |
-
for description in SELECT_TYPES:
|
84 |
-
if (
|
85 |
-
description.options_key in device
|
86 |
-
and len(device[description.options_key]) > 1
|
87 |
-
):
|
88 |
-
entities.append(
|
89 |
-
PlugwiseSelectEntity(coordinator, device_id, description)
|
90 |
-
)
|
91 |
-
LOGGER.debug("Add %s %s selector", device["name"], description.name)
|
92 |
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
|
95 |
|
96 |
class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
@@ -112,21 +95,19 @@
|
|
112 |
@property
|
113 |
def current_option(self) -> str:
|
114 |
"""Return the selected entity option to represent the entity state."""
|
115 |
-
return self.device[self.entity_description.
|
116 |
|
117 |
@property
|
118 |
def options(self) -> list[str]:
|
119 |
-
"""Return the
|
120 |
return self.device[self.entity_description.options_key]
|
121 |
|
|
|
122 |
async def async_select_option(self, option: str) -> None:
|
123 |
-
"""Change to the selected entity option.
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
self.entity_description.name,
|
130 |
-
option,
|
131 |
)
|
132 |
-
await self.coordinator.async_request_refresh()
|
|
|
1 |
"""Plugwise Select component for Home Assistant."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
|
|
5 |
from dataclasses import dataclass
|
|
|
6 |
|
7 |
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
|
|
8 |
from homeassistant.const import STATE_ON, EntityCategory
|
9 |
+
from homeassistant.core import HomeAssistant, callback
|
10 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
11 |
|
12 |
+
from . import PlugwiseConfigEntry
|
13 |
+
from .const import LOCATION, SelectOptionsType, SelectType
|
14 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
15 |
from .entity import PlugwiseEntity
|
16 |
+
from .util import plugwise_command
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
+
@dataclass(frozen=True, kw_only=True)
|
20 |
+
class PlugwiseSelectEntityDescription(SelectEntityDescription):
|
21 |
+
"""Class describing Plugwise Select entities."""
|
22 |
|
23 |
+
key: SelectType
|
24 |
+
options_key: SelectOptionsType
|
|
|
|
|
|
|
25 |
|
26 |
|
27 |
SELECT_TYPES = (
|
28 |
PlugwiseSelectEntityDescription(
|
29 |
key="select_schedule",
|
30 |
+
translation_key="select_schedule",
|
|
|
|
|
|
|
31 |
options_key="available_schedules",
|
32 |
),
|
33 |
PlugwiseSelectEntityDescription(
|
34 |
key="select_regulation_mode",
|
|
|
|
|
|
|
35 |
translation_key="regulation_mode",
|
36 |
+
entity_category=EntityCategory.CONFIG,
|
|
|
37 |
options_key="regulation_modes",
|
38 |
),
|
39 |
PlugwiseSelectEntityDescription(
|
40 |
key="select_dhw_mode",
|
|
|
|
|
|
|
41 |
translation_key="dhw_mode",
|
42 |
+
entity_category=EntityCategory.CONFIG,
|
|
|
43 |
options_key="dhw_modes",
|
44 |
),
|
45 |
+
PlugwiseSelectEntityDescription(
|
46 |
+
key="select_gateway_mode",
|
47 |
+
translation_key="gateway_mode",
|
48 |
+
entity_category=EntityCategory.CONFIG,
|
49 |
+
options_key="gateway_modes",
|
50 |
+
),
|
51 |
)
|
52 |
|
53 |
|
54 |
async def async_setup_entry(
|
55 |
hass: HomeAssistant,
|
56 |
+
entry: PlugwiseConfigEntry,
|
57 |
async_add_entities: AddEntitiesCallback,
|
58 |
) -> None:
|
59 |
"""Set up the Smile selector from a config entry."""
|
60 |
+
coordinator = entry.runtime_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
+
@callback
|
63 |
+
def _add_entities() -> None:
|
64 |
+
"""Add Entities."""
|
65 |
+
if not coordinator.new_devices:
|
66 |
+
return
|
67 |
+
|
68 |
+
async_add_entities(
|
69 |
+
PlugwiseSelectEntity(coordinator, device_id, description)
|
70 |
+
for device_id in coordinator.new_devices
|
71 |
+
for description in SELECT_TYPES
|
72 |
+
if description.options_key in coordinator.data.devices[device_id]
|
73 |
+
)
|
74 |
+
|
75 |
+
_add_entities()
|
76 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
77 |
|
78 |
|
79 |
class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
|
|
|
95 |
@property
|
96 |
def current_option(self) -> str:
|
97 |
"""Return the selected entity option to represent the entity state."""
|
98 |
+
return self.device[self.entity_description.key]
|
99 |
|
100 |
@property
|
101 |
def options(self) -> list[str]:
|
102 |
+
"""Return the available select-options."""
|
103 |
return self.device[self.entity_description.options_key]
|
104 |
|
105 |
+
@plugwise_command
|
106 |
async def async_select_option(self, option: str) -> None:
|
107 |
+
"""Change to the selected entity option.
|
108 |
+
|
109 |
+
self.device[LOCATION] and STATE_ON are required for the thermostat-schedule select.
|
110 |
+
"""
|
111 |
+
await self.coordinator.api.set_select(
|
112 |
+
self.entity_description.key, self.device[LOCATION], option, STATE_ON
|
|
|
|
|
113 |
)
|
|
@@ -1,52 +1,436 @@
|
|
1 |
"""Plugwise Sensor component for Home Assistant."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
4 |
-
from
|
5 |
-
|
6 |
-
from
|
7 |
-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
8 |
|
9 |
-
from .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
|
|
11 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
12 |
from .entity import PlugwiseEntity
|
13 |
-
from .models import PW_SENSOR_TYPES, PlugwiseSensorEntityDescription
|
14 |
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
|
18 |
async def async_setup_entry(
|
19 |
hass: HomeAssistant,
|
20 |
-
|
21 |
async_add_entities: AddEntitiesCallback,
|
22 |
) -> None:
|
23 |
"""Set up the Smile sensors from a config entry."""
|
24 |
-
coordinator =
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
description,
|
40 |
-
)
|
41 |
-
)
|
42 |
-
LOGGER.debug("Add %s sensor", description.key)
|
43 |
|
44 |
-
|
|
|
45 |
|
46 |
|
47 |
class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
|
48 |
"""Represent Plugwise Sensors."""
|
49 |
|
|
|
|
|
50 |
def __init__(
|
51 |
self,
|
52 |
coordinator: PlugwiseDataUpdateCoordinator,
|
@@ -59,6 +443,6 @@
|
|
59 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
60 |
|
61 |
@property
|
62 |
-
def native_value(self) -> int | float
|
63 |
"""Return the value reported by the sensor."""
|
64 |
-
return self.device["sensors"]
|
|
|
1 |
"""Plugwise Sensor component for Home Assistant."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
+
from dataclasses import dataclass
|
6 |
+
|
7 |
+
from plugwise.constants import SensorType
|
|
|
8 |
|
9 |
+
from homeassistant.components.sensor import (
|
10 |
+
SensorDeviceClass,
|
11 |
+
SensorEntity,
|
12 |
+
SensorEntityDescription,
|
13 |
+
SensorStateClass,
|
14 |
+
)
|
15 |
+
from homeassistant.const import (
|
16 |
+
LIGHT_LUX,
|
17 |
+
PERCENTAGE,
|
18 |
+
EntityCategory,
|
19 |
+
UnitOfElectricPotential,
|
20 |
+
UnitOfEnergy,
|
21 |
+
UnitOfPower,
|
22 |
+
UnitOfPressure,
|
23 |
+
UnitOfTemperature,
|
24 |
+
UnitOfVolume,
|
25 |
+
UnitOfVolumeFlowRate,
|
26 |
+
)
|
27 |
+
from homeassistant.core import HomeAssistant, callback
|
28 |
+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
29 |
|
30 |
+
from . import PlugwiseConfigEntry
|
31 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
32 |
from .entity import PlugwiseEntity
|
|
|
33 |
|
34 |
+
|
35 |
+
@dataclass(frozen=True)
|
36 |
+
class PlugwiseSensorEntityDescription(SensorEntityDescription):
|
37 |
+
"""Describes Plugwise sensor entity."""
|
38 |
+
|
39 |
+
key: SensorType
|
40 |
+
|
41 |
+
|
42 |
+
SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
|
43 |
+
PlugwiseSensorEntityDescription(
|
44 |
+
key="setpoint",
|
45 |
+
translation_key="setpoint",
|
46 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
47 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
48 |
+
state_class=SensorStateClass.MEASUREMENT,
|
49 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
50 |
+
),
|
51 |
+
PlugwiseSensorEntityDescription(
|
52 |
+
key="setpoint_high",
|
53 |
+
translation_key="cooling_setpoint",
|
54 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
55 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
56 |
+
state_class=SensorStateClass.MEASUREMENT,
|
57 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
58 |
+
),
|
59 |
+
PlugwiseSensorEntityDescription(
|
60 |
+
key="setpoint_low",
|
61 |
+
translation_key="heating_setpoint",
|
62 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
63 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
64 |
+
state_class=SensorStateClass.MEASUREMENT,
|
65 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
66 |
+
),
|
67 |
+
PlugwiseSensorEntityDescription(
|
68 |
+
key="temperature",
|
69 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
70 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
71 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
72 |
+
state_class=SensorStateClass.MEASUREMENT,
|
73 |
+
),
|
74 |
+
PlugwiseSensorEntityDescription(
|
75 |
+
key="intended_boiler_temperature",
|
76 |
+
translation_key="intended_boiler_temperature",
|
77 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
78 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
79 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
80 |
+
state_class=SensorStateClass.MEASUREMENT,
|
81 |
+
),
|
82 |
+
PlugwiseSensorEntityDescription(
|
83 |
+
key="temperature_difference",
|
84 |
+
translation_key="temperature_difference",
|
85 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
86 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
87 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
88 |
+
state_class=SensorStateClass.MEASUREMENT,
|
89 |
+
),
|
90 |
+
PlugwiseSensorEntityDescription(
|
91 |
+
key="outdoor_temperature",
|
92 |
+
translation_key="outdoor_temperature",
|
93 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
94 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
95 |
+
state_class=SensorStateClass.MEASUREMENT,
|
96 |
+
),
|
97 |
+
PlugwiseSensorEntityDescription(
|
98 |
+
key="outdoor_air_temperature",
|
99 |
+
translation_key="outdoor_air_temperature",
|
100 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
101 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
102 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
103 |
+
state_class=SensorStateClass.MEASUREMENT,
|
104 |
+
),
|
105 |
+
PlugwiseSensorEntityDescription(
|
106 |
+
key="water_temperature",
|
107 |
+
translation_key="water_temperature",
|
108 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
109 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
110 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
111 |
+
state_class=SensorStateClass.MEASUREMENT,
|
112 |
+
),
|
113 |
+
PlugwiseSensorEntityDescription(
|
114 |
+
key="return_temperature",
|
115 |
+
translation_key="return_temperature",
|
116 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
117 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
118 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
119 |
+
state_class=SensorStateClass.MEASUREMENT,
|
120 |
+
),
|
121 |
+
PlugwiseSensorEntityDescription(
|
122 |
+
key="electricity_consumed",
|
123 |
+
translation_key="electricity_consumed",
|
124 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
125 |
+
device_class=SensorDeviceClass.POWER,
|
126 |
+
state_class=SensorStateClass.MEASUREMENT,
|
127 |
+
),
|
128 |
+
PlugwiseSensorEntityDescription(
|
129 |
+
key="electricity_produced",
|
130 |
+
translation_key="electricity_produced",
|
131 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
132 |
+
device_class=SensorDeviceClass.POWER,
|
133 |
+
state_class=SensorStateClass.MEASUREMENT,
|
134 |
+
entity_registry_enabled_default=False,
|
135 |
+
),
|
136 |
+
PlugwiseSensorEntityDescription(
|
137 |
+
key="electricity_consumed_interval",
|
138 |
+
translation_key="electricity_consumed_interval",
|
139 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
140 |
+
device_class=SensorDeviceClass.ENERGY,
|
141 |
+
state_class=SensorStateClass.TOTAL,
|
142 |
+
),
|
143 |
+
PlugwiseSensorEntityDescription(
|
144 |
+
key="electricity_consumed_peak_interval",
|
145 |
+
translation_key="electricity_consumed_peak_interval",
|
146 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
147 |
+
device_class=SensorDeviceClass.ENERGY,
|
148 |
+
state_class=SensorStateClass.TOTAL,
|
149 |
+
),
|
150 |
+
PlugwiseSensorEntityDescription(
|
151 |
+
key="electricity_consumed_off_peak_interval",
|
152 |
+
translation_key="electricity_consumed_off_peak_interval",
|
153 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
154 |
+
device_class=SensorDeviceClass.ENERGY,
|
155 |
+
state_class=SensorStateClass.TOTAL,
|
156 |
+
),
|
157 |
+
PlugwiseSensorEntityDescription(
|
158 |
+
key="electricity_produced_interval",
|
159 |
+
translation_key="electricity_produced_interval",
|
160 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
161 |
+
device_class=SensorDeviceClass.ENERGY,
|
162 |
+
state_class=SensorStateClass.TOTAL,
|
163 |
+
entity_registry_enabled_default=False,
|
164 |
+
),
|
165 |
+
PlugwiseSensorEntityDescription(
|
166 |
+
key="electricity_produced_peak_interval",
|
167 |
+
translation_key="electricity_produced_peak_interval",
|
168 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
169 |
+
device_class=SensorDeviceClass.ENERGY,
|
170 |
+
state_class=SensorStateClass.TOTAL,
|
171 |
+
),
|
172 |
+
PlugwiseSensorEntityDescription(
|
173 |
+
key="electricity_produced_off_peak_interval",
|
174 |
+
translation_key="electricity_produced_off_peak_interval",
|
175 |
+
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
176 |
+
device_class=SensorDeviceClass.ENERGY,
|
177 |
+
state_class=SensorStateClass.TOTAL,
|
178 |
+
),
|
179 |
+
PlugwiseSensorEntityDescription(
|
180 |
+
key="electricity_consumed_point",
|
181 |
+
translation_key="electricity_consumed_point",
|
182 |
+
device_class=SensorDeviceClass.POWER,
|
183 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
184 |
+
state_class=SensorStateClass.MEASUREMENT,
|
185 |
+
),
|
186 |
+
PlugwiseSensorEntityDescription(
|
187 |
+
key="electricity_consumed_off_peak_point",
|
188 |
+
translation_key="electricity_consumed_off_peak_point",
|
189 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
190 |
+
device_class=SensorDeviceClass.POWER,
|
191 |
+
state_class=SensorStateClass.MEASUREMENT,
|
192 |
+
),
|
193 |
+
PlugwiseSensorEntityDescription(
|
194 |
+
key="electricity_consumed_peak_point",
|
195 |
+
translation_key="electricity_consumed_peak_point",
|
196 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
197 |
+
device_class=SensorDeviceClass.POWER,
|
198 |
+
state_class=SensorStateClass.MEASUREMENT,
|
199 |
+
),
|
200 |
+
PlugwiseSensorEntityDescription(
|
201 |
+
key="electricity_consumed_off_peak_cumulative",
|
202 |
+
translation_key="electricity_consumed_off_peak_cumulative",
|
203 |
+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
204 |
+
device_class=SensorDeviceClass.ENERGY,
|
205 |
+
state_class=SensorStateClass.TOTAL_INCREASING,
|
206 |
+
),
|
207 |
+
PlugwiseSensorEntityDescription(
|
208 |
+
key="electricity_consumed_peak_cumulative",
|
209 |
+
translation_key="electricity_consumed_peak_cumulative",
|
210 |
+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
211 |
+
device_class=SensorDeviceClass.ENERGY,
|
212 |
+
state_class=SensorStateClass.TOTAL_INCREASING,
|
213 |
+
),
|
214 |
+
PlugwiseSensorEntityDescription(
|
215 |
+
key="electricity_produced_point",
|
216 |
+
translation_key="electricity_produced_point",
|
217 |
+
device_class=SensorDeviceClass.POWER,
|
218 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
219 |
+
state_class=SensorStateClass.MEASUREMENT,
|
220 |
+
),
|
221 |
+
PlugwiseSensorEntityDescription(
|
222 |
+
key="electricity_produced_off_peak_point",
|
223 |
+
translation_key="electricity_produced_off_peak_point",
|
224 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
225 |
+
device_class=SensorDeviceClass.POWER,
|
226 |
+
state_class=SensorStateClass.MEASUREMENT,
|
227 |
+
),
|
228 |
+
PlugwiseSensorEntityDescription(
|
229 |
+
key="electricity_produced_peak_point",
|
230 |
+
translation_key="electricity_produced_peak_point",
|
231 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
232 |
+
device_class=SensorDeviceClass.POWER,
|
233 |
+
state_class=SensorStateClass.MEASUREMENT,
|
234 |
+
),
|
235 |
+
PlugwiseSensorEntityDescription(
|
236 |
+
key="electricity_produced_off_peak_cumulative",
|
237 |
+
translation_key="electricity_produced_off_peak_cumulative",
|
238 |
+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
239 |
+
device_class=SensorDeviceClass.ENERGY,
|
240 |
+
state_class=SensorStateClass.TOTAL_INCREASING,
|
241 |
+
),
|
242 |
+
PlugwiseSensorEntityDescription(
|
243 |
+
key="electricity_produced_peak_cumulative",
|
244 |
+
translation_key="electricity_produced_peak_cumulative",
|
245 |
+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
246 |
+
device_class=SensorDeviceClass.ENERGY,
|
247 |
+
state_class=SensorStateClass.TOTAL_INCREASING,
|
248 |
+
),
|
249 |
+
PlugwiseSensorEntityDescription(
|
250 |
+
key="electricity_phase_one_consumed",
|
251 |
+
translation_key="electricity_phase_one_consumed",
|
252 |
+
device_class=SensorDeviceClass.POWER,
|
253 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
254 |
+
state_class=SensorStateClass.MEASUREMENT,
|
255 |
+
),
|
256 |
+
PlugwiseSensorEntityDescription(
|
257 |
+
key="electricity_phase_two_consumed",
|
258 |
+
translation_key="electricity_phase_two_consumed",
|
259 |
+
device_class=SensorDeviceClass.POWER,
|
260 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
261 |
+
state_class=SensorStateClass.MEASUREMENT,
|
262 |
+
),
|
263 |
+
PlugwiseSensorEntityDescription(
|
264 |
+
key="electricity_phase_three_consumed",
|
265 |
+
translation_key="electricity_phase_three_consumed",
|
266 |
+
device_class=SensorDeviceClass.POWER,
|
267 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
268 |
+
state_class=SensorStateClass.MEASUREMENT,
|
269 |
+
),
|
270 |
+
PlugwiseSensorEntityDescription(
|
271 |
+
key="electricity_phase_one_produced",
|
272 |
+
translation_key="electricity_phase_one_produced",
|
273 |
+
device_class=SensorDeviceClass.POWER,
|
274 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
275 |
+
state_class=SensorStateClass.MEASUREMENT,
|
276 |
+
),
|
277 |
+
PlugwiseSensorEntityDescription(
|
278 |
+
key="electricity_phase_two_produced",
|
279 |
+
translation_key="electricity_phase_two_produced",
|
280 |
+
device_class=SensorDeviceClass.POWER,
|
281 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
282 |
+
state_class=SensorStateClass.MEASUREMENT,
|
283 |
+
),
|
284 |
+
PlugwiseSensorEntityDescription(
|
285 |
+
key="electricity_phase_three_produced",
|
286 |
+
translation_key="electricity_phase_three_produced",
|
287 |
+
device_class=SensorDeviceClass.POWER,
|
288 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
289 |
+
state_class=SensorStateClass.MEASUREMENT,
|
290 |
+
),
|
291 |
+
PlugwiseSensorEntityDescription(
|
292 |
+
key="voltage_phase_one",
|
293 |
+
translation_key="voltage_phase_one",
|
294 |
+
device_class=SensorDeviceClass.VOLTAGE,
|
295 |
+
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
296 |
+
state_class=SensorStateClass.MEASUREMENT,
|
297 |
+
entity_registry_enabled_default=False,
|
298 |
+
),
|
299 |
+
PlugwiseSensorEntityDescription(
|
300 |
+
key="voltage_phase_two",
|
301 |
+
translation_key="voltage_phase_two",
|
302 |
+
device_class=SensorDeviceClass.VOLTAGE,
|
303 |
+
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
304 |
+
state_class=SensorStateClass.MEASUREMENT,
|
305 |
+
entity_registry_enabled_default=False,
|
306 |
+
),
|
307 |
+
PlugwiseSensorEntityDescription(
|
308 |
+
key="voltage_phase_three",
|
309 |
+
translation_key="voltage_phase_three",
|
310 |
+
device_class=SensorDeviceClass.VOLTAGE,
|
311 |
+
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
312 |
+
state_class=SensorStateClass.MEASUREMENT,
|
313 |
+
entity_registry_enabled_default=False,
|
314 |
+
),
|
315 |
+
PlugwiseSensorEntityDescription(
|
316 |
+
key="gas_consumed_interval",
|
317 |
+
translation_key="gas_consumed_interval",
|
318 |
+
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
319 |
+
state_class=SensorStateClass.MEASUREMENT,
|
320 |
+
),
|
321 |
+
PlugwiseSensorEntityDescription(
|
322 |
+
key="gas_consumed_cumulative",
|
323 |
+
translation_key="gas_consumed_cumulative",
|
324 |
+
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
325 |
+
device_class=SensorDeviceClass.GAS,
|
326 |
+
state_class=SensorStateClass.TOTAL,
|
327 |
+
),
|
328 |
+
PlugwiseSensorEntityDescription(
|
329 |
+
key="net_electricity_point",
|
330 |
+
translation_key="net_electricity_point",
|
331 |
+
native_unit_of_measurement=UnitOfPower.WATT,
|
332 |
+
device_class=SensorDeviceClass.POWER,
|
333 |
+
state_class=SensorStateClass.MEASUREMENT,
|
334 |
+
),
|
335 |
+
PlugwiseSensorEntityDescription(
|
336 |
+
key="net_electricity_cumulative",
|
337 |
+
translation_key="net_electricity_cumulative",
|
338 |
+
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
339 |
+
device_class=SensorDeviceClass.ENERGY,
|
340 |
+
state_class=SensorStateClass.TOTAL,
|
341 |
+
),
|
342 |
+
PlugwiseSensorEntityDescription(
|
343 |
+
key="battery",
|
344 |
+
native_unit_of_measurement=PERCENTAGE,
|
345 |
+
device_class=SensorDeviceClass.BATTERY,
|
346 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
347 |
+
state_class=SensorStateClass.MEASUREMENT,
|
348 |
+
),
|
349 |
+
PlugwiseSensorEntityDescription(
|
350 |
+
key="illuminance",
|
351 |
+
native_unit_of_measurement=LIGHT_LUX,
|
352 |
+
device_class=SensorDeviceClass.ILLUMINANCE,
|
353 |
+
state_class=SensorStateClass.MEASUREMENT,
|
354 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
355 |
+
),
|
356 |
+
PlugwiseSensorEntityDescription(
|
357 |
+
key="modulation_level",
|
358 |
+
translation_key="modulation_level",
|
359 |
+
native_unit_of_measurement=PERCENTAGE,
|
360 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
361 |
+
state_class=SensorStateClass.MEASUREMENT,
|
362 |
+
),
|
363 |
+
PlugwiseSensorEntityDescription(
|
364 |
+
key="valve_position",
|
365 |
+
translation_key="valve_position",
|
366 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
367 |
+
native_unit_of_measurement=PERCENTAGE,
|
368 |
+
state_class=SensorStateClass.MEASUREMENT,
|
369 |
+
),
|
370 |
+
PlugwiseSensorEntityDescription(
|
371 |
+
key="water_pressure",
|
372 |
+
translation_key="water_pressure",
|
373 |
+
native_unit_of_measurement=UnitOfPressure.BAR,
|
374 |
+
device_class=SensorDeviceClass.PRESSURE,
|
375 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
376 |
+
state_class=SensorStateClass.MEASUREMENT,
|
377 |
+
),
|
378 |
+
PlugwiseSensorEntityDescription(
|
379 |
+
key="humidity",
|
380 |
+
native_unit_of_measurement=PERCENTAGE,
|
381 |
+
device_class=SensorDeviceClass.HUMIDITY,
|
382 |
+
state_class=SensorStateClass.MEASUREMENT,
|
383 |
+
),
|
384 |
+
PlugwiseSensorEntityDescription(
|
385 |
+
key="dhw_temperature",
|
386 |
+
translation_key="dhw_temperature",
|
387 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
388 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
389 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
390 |
+
state_class=SensorStateClass.MEASUREMENT,
|
391 |
+
),
|
392 |
+
PlugwiseSensorEntityDescription(
|
393 |
+
key="domestic_hot_water_setpoint",
|
394 |
+
translation_key="domestic_hot_water_setpoint",
|
395 |
+
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
396 |
+
device_class=SensorDeviceClass.TEMPERATURE,
|
397 |
+
entity_category=EntityCategory.DIAGNOSTIC,
|
398 |
+
state_class=SensorStateClass.MEASUREMENT,
|
399 |
+
),
|
400 |
+
)
|
401 |
|
402 |
|
403 |
async def async_setup_entry(
|
404 |
hass: HomeAssistant,
|
405 |
+
entry: PlugwiseConfigEntry,
|
406 |
async_add_entities: AddEntitiesCallback,
|
407 |
) -> None:
|
408 |
"""Set up the Smile sensors from a config entry."""
|
409 |
+
coordinator = entry.runtime_data
|
410 |
|
411 |
+
@callback
|
412 |
+
def _add_entities() -> None:
|
413 |
+
"""Add Entities."""
|
414 |
+
if not coordinator.new_devices:
|
415 |
+
return
|
416 |
+
|
417 |
+
async_add_entities(
|
418 |
+
PlugwiseSensorEntity(coordinator, device_id, description)
|
419 |
+
for device_id in coordinator.new_devices
|
420 |
+
if (sensors := coordinator.data.devices[device_id].get("sensors"))
|
421 |
+
for description in SENSORS
|
422 |
+
if description.key in sensors
|
423 |
+
)
|
|
|
|
|
|
|
|
|
424 |
|
425 |
+
_add_entities()
|
426 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
427 |
|
428 |
|
429 |
class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
|
430 |
"""Represent Plugwise Sensors."""
|
431 |
|
432 |
+
entity_description: PlugwiseSensorEntityDescription
|
433 |
+
|
434 |
def __init__(
|
435 |
self,
|
436 |
coordinator: PlugwiseDataUpdateCoordinator,
|
|
|
443 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
444 |
|
445 |
@property
|
446 |
+
def native_value(self) -> int | float:
|
447 |
"""Return the value reported by the sensor."""
|
448 |
+
return self.device["sensors"][self.entity_description.key]
|
@@ -1,61 +1,25 @@
|
|
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)",
|
13 |
-
"homekit_emulation": "Homekit emulation (i.e. on hvac_off => Away)",
|
14 |
-
"refresh_interval": "Frontend refresh-time (1.5 - 5 seconds)"
|
15 |
-
}
|
16 |
-
}
|
17 |
-
}
|
18 |
-
},
|
19 |
"config": {
|
20 |
"step": {
|
21 |
"user": {
|
22 |
-
"title": "
|
23 |
-
"description": "Please
|
24 |
-
"data": {
|
25 |
-
"flow_type": "Connection type"
|
26 |
-
}
|
27 |
-
},
|
28 |
-
"user_gateway": {
|
29 |
-
"title": "Connect to the Plugwise Adam/Smile/Stretch",
|
30 |
-
"description": "Please enter:",
|
31 |
"data": {
|
32 |
-
"password": "ID",
|
33 |
-
"
|
34 |
-
"
|
35 |
-
"
|
36 |
-
}
|
37 |
-
|
38 |
-
|
39 |
-
"title": "Connect to Plugwise Stick",
|
40 |
-
"description": "Please enter:",
|
41 |
-
"data": {
|
42 |
-
"usb_path": "[%key:common::config_flow::data::usb_path%]"
|
43 |
-
}
|
44 |
-
},
|
45 |
-
"manual_path": {
|
46 |
-
"data": {
|
47 |
-
"usb_path": "[%key:common::config_flow::data::usb_path%]"
|
48 |
}
|
49 |
}
|
50 |
},
|
51 |
"error": {
|
52 |
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
53 |
-
"invalid_auth": "
|
54 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
55 |
-
"network_down": "Plugwise Zigbee network is down",
|
56 |
-
"network_timeout": "Network communication timeout",
|
57 |
"response_error": "Invalid XML data, or error indication received",
|
58 |
-
"stick_init": "Initialization of Plugwise USB-stick failed",
|
59 |
"unknown": "[%key:common::config_flow::error::unknown%]",
|
60 |
"unsupported": "Device with unsupported firmware"
|
61 |
},
|
@@ -65,39 +29,260 @@
|
|
65 |
}
|
66 |
},
|
67 |
"entity": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
"climate": {
|
69 |
"plugwise": {
|
70 |
"state_attributes": {
|
|
|
|
|
|
|
71 |
"preset_mode": {
|
72 |
"state": {
|
73 |
"asleep": "Night",
|
74 |
-
"away": "
|
75 |
-
"home": "
|
76 |
"no_frost": "Anti-frost",
|
77 |
"vacation": "Vacation"
|
78 |
}
|
|
|
|
|
|
|
79 |
}
|
80 |
}
|
81 |
}
|
82 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
"select": {
|
84 |
"dhw_mode": {
|
|
|
85 |
"state": {
|
86 |
-
"off": "
|
87 |
"auto": "Auto",
|
88 |
-
"boost": "
|
89 |
-
"comfort": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
}
|
91 |
},
|
92 |
"regulation_mode": {
|
|
|
93 |
"state": {
|
94 |
"bleeding_cold": "Bleeding cold",
|
95 |
"bleeding_hot": "Bleeding hot",
|
96 |
-
"cooling": "
|
97 |
-
"heating": "
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
"off": "Off"
|
99 |
}
|
100 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
}
|
102 |
}
|
103 |
}
|
|
|
1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
"config": {
|
3 |
"step": {
|
4 |
"user": {
|
5 |
+
"title": "Connect to the Smile",
|
6 |
+
"description": "Please enter",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
"data": {
|
8 |
+
"password": "Smile ID",
|
9 |
+
"host": "[%key:common::config_flow::data::ip%]",
|
10 |
+
"port": "[%key:common::config_flow::data::port%]",
|
11 |
+
"username": "Smile Username"
|
12 |
+
},
|
13 |
+
"data_description": {
|
14 |
+
"host": "Leave empty if using Auto Discovery"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
}
|
16 |
}
|
17 |
},
|
18 |
"error": {
|
19 |
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
20 |
+
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
21 |
"invalid_setup": "Add your Adam instead of your Anna, see the documentation",
|
|
|
|
|
22 |
"response_error": "Invalid XML data, or error indication received",
|
|
|
23 |
"unknown": "[%key:common::config_flow::error::unknown%]",
|
24 |
"unsupported": "Device with unsupported firmware"
|
25 |
},
|
|
|
29 |
}
|
30 |
},
|
31 |
"entity": {
|
32 |
+
"binary_sensor": {
|
33 |
+
"low_battery": {
|
34 |
+
"name": "Battery state"
|
35 |
+
},
|
36 |
+
"compressor_state": {
|
37 |
+
"name": "Compressor state"
|
38 |
+
},
|
39 |
+
"cooling_enabled": {
|
40 |
+
"name": "Cooling enabled"
|
41 |
+
},
|
42 |
+
"dhw_state": {
|
43 |
+
"name": "DHW state"
|
44 |
+
},
|
45 |
+
"flame_state": {
|
46 |
+
"name": "Flame state"
|
47 |
+
},
|
48 |
+
"heating_state": {
|
49 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
|
50 |
+
},
|
51 |
+
"cooling_state": {
|
52 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
|
53 |
+
},
|
54 |
+
"secondary_boiler_state": {
|
55 |
+
"name": "Secondary boiler state"
|
56 |
+
},
|
57 |
+
"plugwise_notification": {
|
58 |
+
"name": "Plugwise notification"
|
59 |
+
}
|
60 |
+
},
|
61 |
+
"button": {
|
62 |
+
"reboot": {
|
63 |
+
"name": "Reboot"
|
64 |
+
}
|
65 |
+
},
|
66 |
"climate": {
|
67 |
"plugwise": {
|
68 |
"state_attributes": {
|
69 |
+
"available_schemas": {
|
70 |
+
"name": "Available schemas"
|
71 |
+
},
|
72 |
"preset_mode": {
|
73 |
"state": {
|
74 |
"asleep": "Night",
|
75 |
+
"away": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
76 |
+
"home": "[%key:common::state::home%]",
|
77 |
"no_frost": "Anti-frost",
|
78 |
"vacation": "Vacation"
|
79 |
}
|
80 |
+
},
|
81 |
+
"selected_schema": {
|
82 |
+
"name": "Selected schema"
|
83 |
}
|
84 |
}
|
85 |
}
|
86 |
},
|
87 |
+
"number": {
|
88 |
+
"maximum_boiler_temperature": {
|
89 |
+
"name": "Maximum boiler temperature setpoint"
|
90 |
+
},
|
91 |
+
"max_dhw_temperature": {
|
92 |
+
"name": "Domestic hot water setpoint"
|
93 |
+
},
|
94 |
+
"temperature_offset": {
|
95 |
+
"name": "Temperature offset"
|
96 |
+
}
|
97 |
+
},
|
98 |
"select": {
|
99 |
"dhw_mode": {
|
100 |
+
"name": "DHW mode",
|
101 |
"state": {
|
102 |
+
"off": "[%key:common::state::off%]",
|
103 |
"auto": "Auto",
|
104 |
+
"boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
|
105 |
+
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]"
|
106 |
+
}
|
107 |
+
},
|
108 |
+
"gateway_mode": {
|
109 |
+
"name": "Gateway mode",
|
110 |
+
"state": {
|
111 |
+
"away": "Pause",
|
112 |
+
"full": "Normal",
|
113 |
+
"vacation": "Vacation"
|
114 |
}
|
115 |
},
|
116 |
"regulation_mode": {
|
117 |
+
"name": "Regulation mode",
|
118 |
"state": {
|
119 |
"bleeding_cold": "Bleeding cold",
|
120 |
"bleeding_hot": "Bleeding hot",
|
121 |
+
"cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
|
122 |
+
"heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
|
123 |
+
"off": "[%key:common::state::off%]"
|
124 |
+
}
|
125 |
+
},
|
126 |
+
"select_schedule": {
|
127 |
+
"name": "Thermostat schedule",
|
128 |
+
"state": {
|
129 |
"off": "Off"
|
130 |
}
|
131 |
}
|
132 |
+
},
|
133 |
+
"sensor": {
|
134 |
+
"setpoint": {
|
135 |
+
"name": "Setpoint"
|
136 |
+
},
|
137 |
+
"cooling_setpoint": {
|
138 |
+
"name": "Cooling setpoint"
|
139 |
+
},
|
140 |
+
"heating_setpoint": {
|
141 |
+
"name": "Heating setpoint"
|
142 |
+
},
|
143 |
+
"intended_boiler_temperature": {
|
144 |
+
"name": "Intended boiler temperature"
|
145 |
+
},
|
146 |
+
"temperature_difference": {
|
147 |
+
"name": "Temperature difference"
|
148 |
+
},
|
149 |
+
"outdoor_temperature": {
|
150 |
+
"name": "Outdoor temperature"
|
151 |
+
},
|
152 |
+
"outdoor_air_temperature": {
|
153 |
+
"name": "Outdoor air temperature"
|
154 |
+
},
|
155 |
+
"water_temperature": {
|
156 |
+
"name": "Water temperature"
|
157 |
+
},
|
158 |
+
"return_temperature": {
|
159 |
+
"name": "Return temperature"
|
160 |
+
},
|
161 |
+
"electricity_consumed": {
|
162 |
+
"name": "Electricity consumed"
|
163 |
+
},
|
164 |
+
"electricity_produced": {
|
165 |
+
"name": "Electricity produced"
|
166 |
+
},
|
167 |
+
"electricity_consumed_interval": {
|
168 |
+
"name": "Electricity consumed interval"
|
169 |
+
},
|
170 |
+
"electricity_consumed_peak_interval": {
|
171 |
+
"name": "Electricity consumed peak interval"
|
172 |
+
},
|
173 |
+
"electricity_consumed_off_peak_interval": {
|
174 |
+
"name": "Electricity consumed off peak interval"
|
175 |
+
},
|
176 |
+
"electricity_produced_interval": {
|
177 |
+
"name": "Electricity produced interval"
|
178 |
+
},
|
179 |
+
"electricity_produced_peak_interval": {
|
180 |
+
"name": "Electricity produced peak interval"
|
181 |
+
},
|
182 |
+
"electricity_produced_off_peak_interval": {
|
183 |
+
"name": "Electricity produced off peak interval"
|
184 |
+
},
|
185 |
+
"electricity_consumed_point": {
|
186 |
+
"name": "Electricity consumed point"
|
187 |
+
},
|
188 |
+
"electricity_consumed_off_peak_point": {
|
189 |
+
"name": "Electricity consumed off peak point"
|
190 |
+
},
|
191 |
+
"electricity_consumed_peak_point": {
|
192 |
+
"name": "Electricity consumed peak point"
|
193 |
+
},
|
194 |
+
"electricity_consumed_off_peak_cumulative": {
|
195 |
+
"name": "Electricity consumed off peak cumulative"
|
196 |
+
},
|
197 |
+
"electricity_consumed_peak_cumulative": {
|
198 |
+
"name": "Electricity consumed peak cumulative"
|
199 |
+
},
|
200 |
+
"electricity_produced_point": {
|
201 |
+
"name": "Electricity produced point"
|
202 |
+
},
|
203 |
+
"electricity_produced_off_peak_point": {
|
204 |
+
"name": "Electricity produced off peak point"
|
205 |
+
},
|
206 |
+
"electricity_produced_peak_point": {
|
207 |
+
"name": "Electricity produced peak point"
|
208 |
+
},
|
209 |
+
"electricity_produced_off_peak_cumulative": {
|
210 |
+
"name": "Electricity produced off peak cumulative"
|
211 |
+
},
|
212 |
+
"electricity_produced_peak_cumulative": {
|
213 |
+
"name": "Electricity produced peak cumulative"
|
214 |
+
},
|
215 |
+
"electricity_phase_one_consumed": {
|
216 |
+
"name": "Electricity phase one consumed"
|
217 |
+
},
|
218 |
+
"electricity_phase_two_consumed": {
|
219 |
+
"name": "Electricity phase two consumed"
|
220 |
+
},
|
221 |
+
"electricity_phase_three_consumed": {
|
222 |
+
"name": "Electricity phase three consumed"
|
223 |
+
},
|
224 |
+
"electricity_phase_one_produced": {
|
225 |
+
"name": "Electricity phase one produced"
|
226 |
+
},
|
227 |
+
"electricity_phase_two_produced": {
|
228 |
+
"name": "Electricity phase two produced"
|
229 |
+
},
|
230 |
+
"electricity_phase_three_produced": {
|
231 |
+
"name": "Electricity phase three produced"
|
232 |
+
},
|
233 |
+
"voltage_phase_one": {
|
234 |
+
"name": "Voltage phase one"
|
235 |
+
},
|
236 |
+
"voltage_phase_two": {
|
237 |
+
"name": "Voltage phase two"
|
238 |
+
},
|
239 |
+
"voltage_phase_three": {
|
240 |
+
"name": "Voltage phase three"
|
241 |
+
},
|
242 |
+
"gas_consumed_interval": {
|
243 |
+
"name": "Gas consumed interval"
|
244 |
+
},
|
245 |
+
"gas_consumed_cumulative": {
|
246 |
+
"name": "Gas consumed cumulative"
|
247 |
+
},
|
248 |
+
"net_electricity_point": {
|
249 |
+
"name": "Net electricity point"
|
250 |
+
},
|
251 |
+
"net_electricity_cumulative": {
|
252 |
+
"name": "Net electricity cumulative"
|
253 |
+
},
|
254 |
+
"modulation_level": {
|
255 |
+
"name": "Modulation level"
|
256 |
+
},
|
257 |
+
"valve_position": {
|
258 |
+
"name": "Valve position"
|
259 |
+
},
|
260 |
+
"water_pressure": {
|
261 |
+
"name": "Water pressure"
|
262 |
+
},
|
263 |
+
"dhw_temperature": {
|
264 |
+
"name": "DHW temperature"
|
265 |
+
},
|
266 |
+
"domestic_hot_water_setpoint": {
|
267 |
+
"name": "DHW setpoint"
|
268 |
+
},
|
269 |
+
"maximum_boiler_temperature": {
|
270 |
+
"name": "Maximum boiler temperature"
|
271 |
+
}
|
272 |
+
},
|
273 |
+
"switch": {
|
274 |
+
"cooling_ena_switch": {
|
275 |
+
"name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
|
276 |
+
},
|
277 |
+
"dhw_cm_switch": {
|
278 |
+
"name": "DHW comfort mode"
|
279 |
+
},
|
280 |
+
"lock": {
|
281 |
+
"name": "[%key:component::lock::title%]"
|
282 |
+
},
|
283 |
+
"relay": {
|
284 |
+
"name": "Relay"
|
285 |
+
}
|
286 |
}
|
287 |
}
|
288 |
}
|
@@ -1,44 +1,90 @@
|
|
1 |
"""Plugwise Switch component for HomeAssistant."""
|
|
|
2 |
from __future__ import annotations
|
3 |
|
|
|
4 |
from typing import Any
|
5 |
|
6 |
-
from
|
7 |
-
|
8 |
-
from homeassistant.
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
10 |
|
11 |
-
from .
|
12 |
from .coordinator import PlugwiseDataUpdateCoordinator
|
13 |
from .entity import PlugwiseEntity
|
14 |
-
from .models import PW_SWITCH_TYPES, PlugwiseSwitchEntityDescription
|
15 |
from .util import plugwise_command
|
16 |
|
17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
async def async_setup_entry(
|
19 |
hass: HomeAssistant,
|
20 |
-
|
21 |
async_add_entities: AddEntitiesCallback,
|
22 |
) -> None:
|
23 |
"""Set up the Smile switches from a config entry."""
|
24 |
-
coordinator =
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
|
39 |
class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
|
40 |
"""Representation of a Plugwise plug."""
|
41 |
|
|
|
|
|
42 |
def __init__(
|
43 |
self,
|
44 |
coordinator: PlugwiseDataUpdateCoordinator,
|
@@ -51,9 +97,9 @@
|
|
51 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
52 |
|
53 |
@property
|
54 |
-
def is_on(self) -> bool
|
55 |
"""Return True if entity is on."""
|
56 |
-
return self.device["switches"]
|
57 |
|
58 |
@plugwise_command
|
59 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
|
1 |
"""Plugwise Switch component for HomeAssistant."""
|
2 |
+
|
3 |
from __future__ import annotations
|
4 |
|
5 |
+
from dataclasses import dataclass
|
6 |
from typing import Any
|
7 |
|
8 |
+
from plugwise.constants import SwitchType
|
9 |
+
|
10 |
+
from homeassistant.components.switch import (
|
11 |
+
SwitchDeviceClass,
|
12 |
+
SwitchEntity,
|
13 |
+
SwitchEntityDescription,
|
14 |
+
)
|
15 |
+
from homeassistant.const import EntityCategory
|
16 |
+
from homeassistant.core import HomeAssistant, callback
|
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
|
23 |
|
24 |
|
25 |
+
@dataclass(frozen=True)
|
26 |
+
class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
|
27 |
+
"""Describes Plugwise switch entity."""
|
28 |
+
|
29 |
+
key: SwitchType
|
30 |
+
|
31 |
+
|
32 |
+
SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
|
33 |
+
PlugwiseSwitchEntityDescription(
|
34 |
+
key="dhw_cm_switch",
|
35 |
+
translation_key="dhw_cm_switch",
|
36 |
+
entity_category=EntityCategory.CONFIG,
|
37 |
+
),
|
38 |
+
PlugwiseSwitchEntityDescription(
|
39 |
+
key="lock",
|
40 |
+
translation_key="lock",
|
41 |
+
entity_category=EntityCategory.CONFIG,
|
42 |
+
),
|
43 |
+
PlugwiseSwitchEntityDescription(
|
44 |
+
key="relay",
|
45 |
+
translation_key="relay",
|
46 |
+
device_class=SwitchDeviceClass.SWITCH,
|
47 |
+
),
|
48 |
+
PlugwiseSwitchEntityDescription(
|
49 |
+
key="cooling_ena_switch",
|
50 |
+
translation_key="cooling_ena_switch",
|
51 |
+
name="Cooling",
|
52 |
+
entity_category=EntityCategory.CONFIG,
|
53 |
+
),
|
54 |
+
)
|
55 |
+
|
56 |
+
|
57 |
async def async_setup_entry(
|
58 |
hass: HomeAssistant,
|
59 |
+
entry: PlugwiseConfigEntry,
|
60 |
async_add_entities: AddEntitiesCallback,
|
61 |
) -> None:
|
62 |
"""Set up the Smile switches from a config entry."""
|
63 |
+
coordinator = entry.runtime_data
|
64 |
+
|
65 |
+
@callback
|
66 |
+
def _add_entities() -> None:
|
67 |
+
"""Add Entities."""
|
68 |
+
if not coordinator.new_devices:
|
69 |
+
return
|
70 |
+
|
71 |
+
async_add_entities(
|
72 |
+
PlugwiseSwitchEntity(coordinator, device_id, description)
|
73 |
+
for device_id in coordinator.new_devices
|
74 |
+
if (switches := coordinator.data.devices[device_id].get("switches"))
|
75 |
+
for description in SWITCHES
|
76 |
+
if description.key in switches
|
77 |
+
)
|
78 |
+
|
79 |
+
_add_entities()
|
80 |
+
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
81 |
|
82 |
|
83 |
class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
|
84 |
"""Representation of a Plugwise plug."""
|
85 |
|
86 |
+
entity_description: PlugwiseSwitchEntityDescription
|
87 |
+
|
88 |
def __init__(
|
89 |
self,
|
90 |
coordinator: PlugwiseDataUpdateCoordinator,
|
|
|
97 |
self._attr_unique_id = f"{device_id}-{description.key}"
|
98 |
|
99 |
@property
|
100 |
+
def is_on(self) -> bool:
|
101 |
"""Return True if entity is on."""
|
102 |
+
return self.device["switches"][self.entity_description.key]
|
103 |
|
104 |
@plugwise_command
|
105 |
async def async_turn_on(self, **kwargs: Any) -> None:
|
@@ -1,21 +1,17 @@
|
|
1 |
"""Utilities for Plugwise."""
|
|
|
2 |
from collections.abc import Awaitable, Callable, Coroutine
|
3 |
-
from typing import Any, Concatenate
|
4 |
|
5 |
-
from
|
6 |
|
7 |
from homeassistant.exceptions import HomeAssistantError
|
8 |
-
from plugwise.exceptions import PlugwiseException
|
9 |
|
10 |
from .entity import PlugwiseEntity
|
11 |
|
12 |
-
_PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
|
13 |
-
_R = TypeVar("_R")
|
14 |
-
_P = ParamSpec("_P")
|
15 |
-
|
16 |
|
17 |
-
def plugwise_command(
|
18 |
-
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]]
|
19 |
) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
|
20 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
21 |
|
|
|
1 |
"""Utilities for Plugwise."""
|
2 |
+
|
3 |
from collections.abc import Awaitable, Callable, Coroutine
|
4 |
+
from typing import Any, Concatenate
|
5 |
|
6 |
+
from plugwise.exceptions import PlugwiseException
|
7 |
|
8 |
from homeassistant.exceptions import HomeAssistantError
|
|
|
9 |
|
10 |
from .entity import PlugwiseEntity
|
11 |
|
|
|
|
|
|
|
|
|
12 |
|
13 |
+
def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
|
14 |
+
func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
|
15 |
) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
|
16 |
"""Decorate Plugwise calls that send commands/make changes to the device.
|
17 |
|