Diff to HTML by rtfpessoa

Files changed (15) hide show
  1. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py +34 -74
  2. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py +39 -81
  3. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py +47 -78
  4. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py +64 -172
  5. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py +40 -39
  6. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py +73 -104
  7. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py +4 -10
  8. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py +2 -2
  9. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json +5 -6
  10. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py +26 -54
  11. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py +45 -71
  12. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py +101 -73
  13. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json +57 -68
  14. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py +22 -28
  15. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py +3 -8
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py RENAMED
@@ -1,52 +1,29 @@
1
  """Plugwise platform for Home Assistant Core."""
 
2
  from __future__ import annotations
3
 
4
  from typing import Any
5
 
6
- from plugwise.exceptions import PlugwiseError
7
- import voluptuous as vol
8
-
9
  from homeassistant.config_entries import ConfigEntry
10
  from homeassistant.const import Platform
11
- from homeassistant.core import HomeAssistant, ServiceCall, callback
12
  from homeassistant.helpers import device_registry as dr, entity_registry as er
13
 
14
- from .const import (
15
- CONF_REFRESH_INTERVAL, # pw-beta options
16
- COORDINATOR,
17
- DOMAIN,
18
- LOGGER,
19
- PLATFORMS,
20
- SERVICE_DELETE,
21
- UNDO_UPDATE_LISTENER,
22
- )
23
  from .coordinator import PlugwiseDataUpdateCoordinator
24
 
 
 
25
 
26
- async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
27
- """Set up the Plugwise Device from a config entry."""
28
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
29
 
30
- cooldown = 1.5 # pw-beta frontend refresh-interval
31
- if (
32
- custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
33
- ) is not None: # pragma: no cover
34
- cooldown = custom_refresh
35
- LOGGER.debug("DUC cooldown interval: %s", cooldown)
36
-
37
- coordinator = PlugwiseDataUpdateCoordinator(
38
- hass, entry, cooldown
39
- ) # pw-beta - cooldown, update_interval as extra
40
  await coordinator.async_config_entry_first_refresh()
41
- # Migrate a changed sensor unique_id
42
  migrate_sensor_entities(hass, coordinator)
43
 
44
- undo_listener = entry.add_update_listener(_update_listener) # pw-beta
45
-
46
- hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
47
- COORDINATOR: coordinator, # pw-beta
48
- UNDO_UPDATE_LISTENER: undo_listener, # pw-beta
49
- }
50
 
51
  device_registry = dr.async_get(hass)
52
  device_registry.async_get_or_create(
@@ -54,66 +31,43 @@
54
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
55
  manufacturer="Plugwise",
56
  model=coordinator.api.smile_model,
 
57
  name=coordinator.api.smile_name,
58
- sw_version=coordinator.api.smile_version[0],
59
- )
60
-
61
- async def delete_notification(
62
- call: ServiceCall,
63
- ) -> None: # pragma: no cover # pw-beta: HA service - delete_notification
64
- """Service: delete the Plugwise Notification."""
65
- LOGGER.debug(
66
- "Service delete PW Notification called for %s",
67
- coordinator.api.smile_name,
68
- )
69
- try:
70
- await coordinator.api.delete_notification()
71
- LOGGER.debug("PW Notification 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)
79
 
80
- for component in PLATFORMS: # 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 _update_listener(
90
- hass: HomeAssistant, entry: ConfigEntry
91
- ) -> None: # pragma: no cover # pw-beta
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) -> bool:
97
- """Unload a config entry."""
98
- if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
99
- hass.data[DOMAIN].pop(entry.entry_id)
100
- return unload_ok
101
 
102
 
103
  @callback
104
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
105
  """Migrate Plugwise entity entries.
106
 
107
- - Migrates unique ID from old relay switches or relative_humidity sensor to the new unique ID
108
  """
109
- if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"):
110
- return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
 
 
 
 
 
 
111
  if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
112
  "-relative_humidity"
113
  ):
114
  return {
115
  "new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
116
  }
 
 
117
 
118
  # No migration needed
119
  return None
@@ -126,10 +80,10 @@
126
  """Migrate Sensors if needed."""
127
  ent_reg = er.async_get(hass)
128
 
129
- # Migrate opentherm_outdoor_temperature # pw-beta add to Core
130
  # to opentherm_outdoor_air_temperature sensor
131
  for device_id, device in coordinator.data.devices.items():
132
- if device["dev_class"] != "heater_central": # pw-beta add to Core
133
  continue
134
 
135
  old_unique_id = f"{device_id}-outdoor_temperature"
@@ -137,4 +91,10 @@
137
  Platform.SENSOR, DOMAIN, old_unique_id
138
  ):
139
  new_unique_id = f"{device_id}-outdoor_air_temperature"
 
 
 
 
 
 
140
  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
73
  return None
 
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)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py RENAMED
@@ -1,4 +1,5 @@
1
  """Plugwise Binary Sensor component for Home Assistant."""
 
2
  from __future__ import annotations
3
 
4
  from collections.abc import Mapping
@@ -8,88 +9,74 @@
8
  from plugwise.constants import BinarySensorType
9
 
10
  from homeassistant.components.binary_sensor import (
 
11
  BinarySensorEntity,
12
  BinarySensorEntityDescription,
13
  )
14
- from homeassistant.config_entries import ConfigEntry
15
  from homeassistant.const import EntityCategory
16
- from homeassistant.core import HomeAssistant
17
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
18
 
19
- from .const import (
20
- COORDINATOR, # pw-beta
21
- DOMAIN,
22
- LOGGER,
23
- SEVERITIES,
24
- )
25
  from .coordinator import PlugwiseDataUpdateCoordinator
26
  from .entity import PlugwiseEntity
27
 
28
- PARALLEL_UPDATES = 0
29
 
30
 
31
- @dataclass
32
  class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
33
  """Describes a Plugwise binary sensor entity."""
34
 
35
  key: BinarySensorType
36
- icon_off: str | None = None
37
 
38
 
39
  BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
40
  PlugwiseBinarySensorEntityDescription(
 
 
 
 
 
 
41
  key="compressor_state",
42
  translation_key="compressor_state",
43
- icon="mdi:hvac",
44
- icon_off="mdi:hvac-off",
45
  entity_category=EntityCategory.DIAGNOSTIC,
46
  ),
47
  PlugwiseBinarySensorEntityDescription(
48
  key="cooling_enabled",
49
  translation_key="cooling_enabled",
50
- icon="mdi:snowflake-thermometer",
51
  entity_category=EntityCategory.DIAGNOSTIC,
52
  ),
53
  PlugwiseBinarySensorEntityDescription(
54
  key="dhw_state",
55
  translation_key="dhw_state",
56
- icon="mdi:water-pump",
57
- icon_off="mdi:water-pump-off",
58
  entity_category=EntityCategory.DIAGNOSTIC,
59
  ),
60
  PlugwiseBinarySensorEntityDescription(
61
  key="flame_state",
62
  translation_key="flame_state",
63
- icon="mdi:fire",
64
- icon_off="mdi:fire-off",
65
  entity_category=EntityCategory.DIAGNOSTIC,
66
  ),
67
  PlugwiseBinarySensorEntityDescription(
68
  key="heating_state",
69
  translation_key="heating_state",
70
- icon="mdi:radiator",
71
- icon_off="mdi:radiator-off",
72
  entity_category=EntityCategory.DIAGNOSTIC,
73
  ),
74
  PlugwiseBinarySensorEntityDescription(
75
  key="cooling_state",
76
  translation_key="cooling_state",
77
- icon="mdi:snowflake",
78
- icon_off="mdi:snowflake-off",
79
  entity_category=EntityCategory.DIAGNOSTIC,
80
  ),
81
  PlugwiseBinarySensorEntityDescription(
82
- key="slave_boiler_state",
83
- translation_key="slave_boiler_state",
84
- icon="mdi:fire",
85
- icon_off="mdi:circle-off-outline",
86
  entity_category=EntityCategory.DIAGNOSTIC,
87
  ),
88
  PlugwiseBinarySensorEntityDescription(
89
  key="plugwise_notification",
90
  translation_key="plugwise_notification",
91
- icon="mdi:mailbox-up-outline",
92
- icon_off="mdi:mailbox-outline",
93
  entity_category=EntityCategory.DIAGNOSTIC,
94
  ),
95
  )
@@ -97,33 +84,32 @@
97
 
98
  async def async_setup_entry(
99
  hass: HomeAssistant,
100
- config_entry: ConfigEntry,
101
  async_add_entities: AddEntitiesCallback,
102
  ) -> None:
103
- """Set up the Plugwise binary sensor based on config_entry."""
104
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
105
- config_entry.entry_id
106
- ][COORDINATOR]
107
-
108
- entities: list[PlugwiseBinarySensorEntity] = []
109
- for device_id, device in coordinator.data.devices.items():
110
- if not (binary_sensors := device.get("binary_sensors")):
111
- continue
112
- for description in BINARY_SENSORS:
113
- if description.key not in binary_sensors:
114
- continue
115
- entities.append(
116
- PlugwiseBinarySensorEntity(
117
- coordinator,
118
- device_id,
119
- description,
120
  )
121
  )
122
- LOGGER.debug(
123
- "Add %s %s binary sensor", device["name"], description.translation_key
124
- )
125
 
126
- async_add_entities(entities)
 
127
 
128
 
129
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
@@ -141,53 +127,25 @@
141
  super().__init__(coordinator, device_id)
142
  self.entity_description = description
143
  self._attr_unique_id = f"{device_id}-{description.key}"
144
- self._notification: dict[str, str] = {} # pw-beta
145
 
146
  @property
147
  def is_on(self) -> bool:
148
  """Return true if the binary sensor is on."""
149
- # pw-beta: show Plugwise notifications as HA persistent notifications
150
- if self._notification:
151
- for notify_id, message in self._notification.items():
152
- self.hass.components.persistent_notification.async_create(
153
- message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
154
- )
155
-
156
- # return self.device["binary_sensors"][self.entity_description.key] # type: ignore [literal-required]
157
  return self.device["binary_sensors"][self.entity_description.key]
158
 
159
  @property
160
- def icon(self) -> str | None:
161
- """Return the icon to use in the frontend, if any."""
162
- if (icon_off := self.entity_description.icon_off) and self.is_on is False:
163
- return icon_off
164
- return self.entity_description.icon
165
-
166
- @property
167
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
168
  """Return entity specific state attributes."""
169
  if self.entity_description.key != "plugwise_notification":
170
  return None
171
 
172
- # pw-beta adjustment with attrs is to only represent severities *with* content
173
- # not all severities including those without content as empty lists
174
- attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
175
- self._notification = {} # pw-beta
176
  if notify := self.coordinator.data.gateway["notifications"]:
177
- for notify_id, details in notify.items(): # pw-beta uses notify_id
178
  for msg_type, msg in details.items():
179
  msg_type = msg_type.lower()
180
  if msg_type not in SEVERITIES:
181
- msg_type = "other" # pragma: no cover
182
-
183
- if (
184
- f"{msg_type}_msg" not in attrs
185
- ): # pw-beta Re-evaluate against Core
186
- attrs[f"{msg_type}_msg"] = []
187
  attrs[f"{msg_type}_msg"].append(msg)
188
 
189
- self._notification[
190
- notify_id
191
- ] = f"{msg_type.title()}: {msg}" # pw-beta
192
-
193
  return attrs
 
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
+
3
  from __future__ import annotations
4
 
5
  from collections.abc import Mapping
 
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
  )
 
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:
138
  """Return entity specific state attributes."""
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
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py RENAMED
@@ -1,4 +1,5 @@
1
  """Plugwise Climate component for Home Assistant."""
 
2
  from __future__ import annotations
3
 
4
  from typing import Any
@@ -12,22 +13,13 @@
12
  HVACAction,
13
  HVACMode,
14
  )
15
- from homeassistant.components.climate.const import (
16
- PRESET_AWAY, # pw-beta homekit emulation
17
- PRESET_HOME, # pw-beta homekit emulation
18
- )
19
- from homeassistant.config_entries import ConfigEntry
20
  from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
21
- from homeassistant.core import HomeAssistant
22
  from homeassistant.exceptions import HomeAssistantError
23
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
24
 
25
- from .const import (
26
- CONF_HOMEKIT_EMULATION, # pw-beta homekit emulation
27
- COORDINATOR, # pw-beta
28
- DOMAIN,
29
- MASTER_THERMOSTATS,
30
- )
31
  from .coordinator import PlugwiseDataUpdateCoordinator
32
  from .entity import PlugwiseEntity
33
  from .util import plugwise_command
@@ -35,25 +27,26 @@
35
 
36
  async def async_setup_entry(
37
  hass: HomeAssistant,
38
- config_entry: ConfigEntry,
39
  async_add_entities: AddEntitiesCallback,
40
  ) -> None:
41
  """Set up the Smile Thermostats from a config entry."""
42
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
43
- config_entry.entry_id
44
- ][COORDINATOR]
45
-
46
- homekit_enabled: bool = config_entry.options.get(
47
- CONF_HOMEKIT_EMULATION, False
48
- ) # pw-beta homekit emulation
49
-
50
- async_add_entities(
51
- PlugwiseClimateEntity(
52
- coordinator, device_id, homekit_enabled
53
- ) # pw-beta homekit emulation
54
- for device_id, device in coordinator.data.devices.items()
55
- if device["dev_class"] in MASTER_THERMOSTATS
56
- )
 
57
 
58
 
59
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
@@ -63,29 +56,23 @@
63
  _attr_name = None
64
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
65
  _attr_translation_key = DOMAIN
 
66
 
67
- _homekit_mode: str | None = None # pw-beta homekit emulation
68
  _previous_mode: str = "heating"
69
 
70
  def __init__(
71
  self,
72
  coordinator: PlugwiseDataUpdateCoordinator,
73
  device_id: str,
74
- homekit_enabled: bool, # pw-beta homekit emulation
75
  ) -> None:
76
  """Set up the Plugwise API."""
77
  super().__init__(coordinator, device_id)
78
-
79
- self._attr_max_temp = self.device["thermostat"]["upper_bound"]
80
- self._attr_min_temp = self.device["thermostat"]["lower_bound"]
81
- # Ensure we don't drop below 0.1
82
- self._attr_target_temperature_step = max(
83
- self.device["thermostat"]["resolution"], 0.1
84
- )
85
  self._attr_unique_id = f"{device_id}-climate"
86
-
87
- # Determine supported features
88
  self.cdr_gateway = coordinator.data.gateway
 
 
 
89
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
90
  if (
91
  self.cdr_gateway["cooling_present"]
@@ -94,15 +81,20 @@
94
  self._attr_supported_features = (
95
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
96
  )
 
 
 
 
97
  if presets := self.device.get("preset_modes"):
98
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
99
  self._attr_preset_modes = presets
100
 
101
- # Determine stable hvac_modes
102
- self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
103
- gateway_id: str = coordinator.data.gateway["gateway_id"]
104
- self.gateway_data = coordinator.data.devices[gateway_id]
105
- self._hvac_modes: list[HVACMode] = []
 
106
 
107
  def _previous_action_mode(self, coordinator: PlugwiseDataUpdateCoordinator) -> None:
108
  """Return the previous action-mode when the regulation-mode is not heating or cooling.
@@ -151,27 +143,18 @@
151
  @property
152
  def hvac_mode(self) -> HVACMode:
153
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
154
- if (
155
- mode := self.device["mode"]
156
- ) is None or mode not in self.hvac_modes: # pw-beta add to Core
157
- return HVACMode.HEAT # pragma: no cover
158
- # pw-beta homekit emulation
159
- if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
160
- mode = HVACMode.OFF # pragma: no cover
161
-
162
  return HVACMode(mode)
163
 
164
  @property
165
  def hvac_modes(self) -> list[HVACMode]:
166
  """Return a list of available HVACModes."""
167
  hvac_modes: list[HVACMode] = []
168
- if (
169
- self._homekit_enabled # pw-beta homekit emulation
170
- or "regulation_modes" in self.gateway_data
171
- ):
172
  hvac_modes.append(HVACMode.OFF)
173
 
174
- if self.device["available_schedules"] != ["None"]:
175
  hvac_modes.append(HVACMode.AUTO)
176
 
177
  if self.cdr_gateway["cooling_present"]:
@@ -188,7 +171,7 @@
188
  return hvac_modes
189
 
190
  @property
191
- def hvac_action(self) -> HVACAction: # pw-beta add to Core
192
  """Return the current running hvac operation if supported."""
193
  # Keep track of the previous action-mode
194
  self._previous_action_mode(self.coordinator)
@@ -203,7 +186,6 @@
203
  if control_state == "off":
204
  return HVACAction.IDLE
205
 
206
- # Anna
207
  heater: str = self.coordinator.data.gateway["heater_id"]
208
  heater_data = self.coordinator.data.devices[heater]
209
  if heater_data["binary_sensors"]["heating_state"]:
@@ -216,7 +198,7 @@
216
  @property
217
  def preset_mode(self) -> str | None:
218
  """Return the current preset mode."""
219
- return self.device["active_preset"]
220
 
221
  @plugwise_command
222
  async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -241,7 +223,7 @@
241
  await self.coordinator.api.set_temperature(self.device["location"], data)
242
 
243
  @plugwise_command
244
- async def async_set_hvac_mode(self, hvac_mode: str) -> None:
245
  """Set the hvac mode."""
246
  if hvac_mode not in self.hvac_modes:
247
  raise HomeAssistantError("Unsupported hvac_mode")
@@ -249,28 +231,15 @@
249
  if hvac_mode == self.hvac_mode:
250
  return
251
 
252
- if hvac_mode != HVACMode.OFF:
 
 
253
  await self.coordinator.api.set_schedule_state(
254
  self.device["location"],
255
  "on" if hvac_mode == HVACMode.AUTO else "off",
256
  )
257
-
258
- if (
259
- not self._homekit_enabled
260
- ): # pw-beta: feature request - mimic HomeKit behavior
261
- if hvac_mode == HVACMode.OFF:
262
- await self.coordinator.api.set_regulation_mode(hvac_mode)
263
- elif self.hvac_mode == HVACMode.OFF:
264
  await self.coordinator.api.set_regulation_mode(self._previous_mode)
265
- else:
266
- self._homekit_mode = hvac_mode # pragma: no cover
267
- if self._homekit_mode == HVACMode.OFF: # pragma: no cover
268
- await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
269
- if (
270
- self._homekit_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]
271
- and self.device["active_preset"] == PRESET_AWAY
272
- ): # pragma: no cover
273
- await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
274
 
275
  @plugwise_command
276
  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
25
  from .util import plugwise_command
 
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):
 
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"]
 
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.
 
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"]:
 
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)
 
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"]:
 
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:
 
223
  await self.coordinator.api.set_temperature(self.device["location"], data)
224
 
225
  @plugwise_command
226
+ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
227
  """Set the hvac mode."""
228
  if hvac_mode not in self.hvac_modes:
229
  raise HomeAssistantError("Unsupported hvac_mode")
 
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:
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py RENAMED
@@ -1,8 +1,8 @@
1
  """Config flow for Plugwise integration."""
 
2
  from __future__ import annotations
3
 
4
- import datetime as dt # pw-beta options
5
- from typing import Any
6
 
7
  from plugwise import Smile
8
  from plugwise.exceptions import (
@@ -15,29 +15,22 @@
15
  )
16
  import voluptuous as vol
17
 
18
- from homeassistant import config_entries
19
  from homeassistant.components.zeroconf import ZeroconfServiceInfo
20
- from homeassistant.config_entries import ConfigEntry, ConfigFlow
21
  from homeassistant.const import (
 
22
  CONF_BASE,
23
  CONF_HOST,
24
  CONF_NAME,
25
  CONF_PASSWORD,
26
  CONF_PORT,
27
- CONF_SCAN_INTERVAL,
28
  CONF_USERNAME,
29
  )
30
- from homeassistant.core import HomeAssistant, callback
31
- from homeassistant.data_entry_flow import FlowResult
32
- from homeassistant.helpers import config_validation as cv
33
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
34
 
35
  from .const import (
36
- CONF_HOMEKIT_EMULATION, # pw-beta option
37
- CONF_REFRESH_INTERVAL, # pw-beta option
38
- COORDINATOR,
39
  DEFAULT_PORT,
40
- DEFAULT_SCAN_INTERVAL, # pw-beta option
41
  DEFAULT_USERNAME,
42
  DOMAIN,
43
  FLOW_SMILE,
@@ -49,41 +42,28 @@
49
  )
50
 
51
 
52
- def _base_schema(
53
- discovery_info: ZeroconfServiceInfo | None,
54
- user_input: dict[str, Any] | None,
55
- ) -> vol.Schema:
56
  """Generate base schema for gateways."""
 
 
57
  if not discovery_info:
58
- if not user_input:
59
- return vol.Schema(
60
- {
61
- vol.Required(CONF_PASSWORD): str,
62
- vol.Required(CONF_HOST): str,
63
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
64
- vol.Required(CONF_USERNAME, default=SMILE): vol.In(
65
- {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
- ),
67
- }
68
- )
69
- return vol.Schema(
70
  {
71
- vol.Required(CONF_PASSWORD, default=user_input[CONF_PASSWORD]): str,
72
- vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
73
- vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): int,
74
- vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): vol.In(
75
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
76
  ),
77
  }
78
  )
79
 
80
- return vol.Schema({vol.Required(CONF_PASSWORD): str})
81
 
82
 
83
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
84
  """Validate whether the user input allows us to connect to the gateway.
85
 
86
- Data has the keys from _base_schema() with values provided by the user.
87
  """
88
  websession = async_get_clientsession(hass, verify_ssl=False)
89
  api = Smile(
@@ -91,7 +71,6 @@
91
  password=data[CONF_PASSWORD],
92
  port=data[CONF_PORT],
93
  username=data[CONF_USERNAME],
94
- timeout=30,
95
  websession=websession,
96
  )
97
  await api.connect()
@@ -104,11 +83,12 @@
104
  VERSION = 1
105
 
106
  discovery_info: ZeroconfServiceInfo | None = None
 
107
  _username: str = DEFAULT_USERNAME
108
 
109
  async def async_step_zeroconf(
110
  self, discovery_info: ZeroconfServiceInfo
111
- ) -> FlowResult:
112
  """Prepare configuration for a discovered Plugwise Smile."""
113
  self.discovery_info = discovery_info
114
  _properties = discovery_info.properties
@@ -125,7 +105,7 @@
125
  CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
126
  },
127
  )
128
- except Exception: # pylint: disable=broad-except
129
  self._abort_if_unique_id_configured()
130
  else:
131
  self._abort_if_unique_id_configured(
@@ -137,7 +117,7 @@
137
 
138
  if DEFAULT_USERNAME not in unique_id:
139
  self._username = STRETCH_USERNAME
140
- _product = _properties.get("product", None)
141
  _version = _properties.get("version", "n/a")
142
  _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
143
 
@@ -149,155 +129,67 @@
149
  # If we have discovered an Adam or Anna, both might be on the network.
150
  # In that case, we need to cancel the Anna flow, as the Adam should
151
  # be added.
152
- for flow in self._async_in_progress():
153
- # This is an Anna, and there is already an Adam flow in progress
154
- if (
155
- _product == "smile_thermo"
156
- and "context" in flow
157
- and flow["context"].get("product") == "smile_open_therm"
158
- ):
159
- return self.async_abort(reason="anna_with_adam")
160
-
161
- # This is an Adam, and there is already an Anna flow in progress
162
- if (
163
- _product == "smile_open_therm"
164
- and "context" in flow
165
- and flow["context"].get("product") == "smile_thermo"
166
- and "flow_id" in flow
167
- ):
168
- self.hass.config_entries.flow.async_abort(flow["flow_id"])
169
 
170
  self.context.update(
171
  {
172
- "title_placeholders": {
173
- CONF_HOST: discovery_info.host,
174
- CONF_NAME: _name,
175
- CONF_PORT: discovery_info.port,
176
- CONF_USERNAME: self._username,
177
- },
178
- "configuration_url": (
179
  f"http://{discovery_info.host}:{discovery_info.port}"
180
  ),
181
- "product": _product,
182
  }
183
  )
184
  return await self.async_step_user()
185
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  async def async_step_user(
187
  self, user_input: dict[str, Any] | None = None
188
- ) -> FlowResult:
189
  """Handle the initial step when using network/gateway setups."""
190
  errors: dict[str, str] = {}
191
 
192
- if not user_input:
193
- return self.async_show_form(
194
- step_id="user",
195
- data_schema=_base_schema(self.discovery_info, None),
196
- errors=errors,
197
- )
198
-
199
- if self.discovery_info:
200
- user_input[CONF_HOST] = self.discovery_info.host
201
- user_input[CONF_PORT] = self.discovery_info.port
202
- user_input[CONF_USERNAME] = self._username
203
- try:
204
- api = await validate_input(self.hass, user_input)
205
- except ConnectionFailedError:
206
- errors[CONF_BASE] = "cannot_connect"
207
- except InvalidAuthentication:
208
- errors[CONF_BASE] = "invalid_auth"
209
- except InvalidSetupError:
210
- errors[CONF_BASE] = "invalid_setup"
211
- except (InvalidXMLError, ResponseError):
212
- errors[CONF_BASE] = "response_error"
213
- except UnsupportedDeviceError:
214
- errors[CONF_BASE] = "unsupported"
215
- except Exception: # pylint: disable=broad-except
216
- errors[CONF_BASE] = "unknown"
217
-
218
- if errors:
219
- return self.async_show_form(
220
- step_id="user",
221
- data_schema=_base_schema(None, user_input),
222
- errors=errors,
223
- )
224
-
225
- await self.async_set_unique_id(
226
- api.smile_hostname or api.gateway_id, raise_on_progress=False
227
- )
228
- self._abort_if_unique_id_configured()
229
-
230
- return self.async_create_entry(title=api.smile_name, data=user_input)
231
-
232
- @staticmethod
233
- @callback
234
- def async_get_options_flow(
235
- config_entry: ConfigEntry,
236
- ) -> config_entries.OptionsFlow: # pw-beta options
237
- """Get the options flow for this handler."""
238
- return PlugwiseOptionsFlowHandler(config_entry)
239
-
240
-
241
- # pw-beta - change the scan-interval via CONFIGURE
242
- # pw-beta - add homekit emulation via CONFIGURE
243
- # pw-beta - change the frontend refresh interval via CONFIGURE
244
- class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): # pw-beta options
245
- """Plugwise option flow."""
246
-
247
- def __init__(self, config_entry: ConfigEntry) -> None: # pragma: no cover
248
- """Initialize options flow."""
249
- self.config_entry = config_entry
250
-
251
- async def async_step_none(
252
- self, user_input: dict[str, Any] | None = None
253
- ) -> FlowResult: # pragma: no cover
254
- """No options available."""
255
  if user_input is not None:
256
- # Apparently not possible to abort an options flow at the moment
257
- return self.async_create_entry(title="", data=self.config_entry.options)
258
-
259
- return self.async_show_form(step_id="none")
260
 
261
- async def async_step_init(
262
- self, user_input: dict[str, Any] | None = None
263
- ) -> FlowResult: # pragma: no cover
264
- """Manage the Plugwise options."""
265
- if not self.config_entry.data.get(CONF_HOST):
266
- return await self.async_step_none(user_input)
267
-
268
- if user_input is not None:
269
- return self.async_create_entry(title="", data=user_input)
270
-
271
- coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][COORDINATOR]
272
- interval: dt.timedelta = DEFAULT_SCAN_INTERVAL[
273
- coordinator.api.smile_type
274
- ] # pw-beta options
275
-
276
- data = {
277
- vol.Optional(
278
- CONF_SCAN_INTERVAL,
279
- default=self.config_entry.options.get(
280
- CONF_SCAN_INTERVAL, interval.seconds
281
- ),
282
- ): vol.All(cv.positive_int, vol.Clamp(min=10)),
283
- } # pw-beta
284
-
285
- if coordinator.api.smile_type != "thermostat":
286
- return self.async_show_form(step_id="init", data_schema=vol.Schema(data))
287
 
288
- data.update(
289
- {
290
- vol.Optional(
291
- CONF_HOMEKIT_EMULATION,
292
- default=self.config_entry.options.get(
293
- CONF_HOMEKIT_EMULATION, False
294
- ),
295
- ): cv.boolean,
296
- vol.Optional(
297
- CONF_REFRESH_INTERVAL,
298
- default=self.config_entry.options.get(CONF_REFRESH_INTERVAL, 1.5),
299
- ): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
300
- }
301
- ) # pw-beta
302
 
303
- 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 (
 
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,
 
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
 
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
+ )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py RENAMED
@@ -1,4 +1,7 @@
1
  """Constants for Plugwise component."""
 
 
 
2
  from datetime import timedelta
3
  import logging
4
  from typing import Final, Literal
@@ -10,53 +13,27 @@
10
  LOGGER = logging.getLogger(__package__)
11
 
12
  API: Final = "api"
13
- COORDINATOR: Final = "coordinator"
14
- CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
15
- CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
16
- CONF_MANUAL_PATH: Final = "Enter Manually"
 
 
 
 
17
  SMILE: Final = "smile"
18
  STRETCH: Final = "stretch"
19
  STRETCH_USERNAME: Final = "stretch"
20
- UNIQUE_IDS: Final = "unique_ids"
21
-
22
- FLOW_NET: Final = "Network: Smile/Stretch"
23
- FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
24
- FLOW_STRETCH: Final = "Stretch (Stretch)"
25
- FLOW_TYPE: Final = "flow_type"
26
-
27
- UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
28
 
29
- # Default directives
30
- DEFAULT_PORT: Final = 80
31
- DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
32
- "power": timedelta(seconds=10),
33
- "stretch": timedelta(seconds=60),
34
- "thermostat": timedelta(seconds=60),
35
- }
36
- DEFAULT_TIMEOUT: Final = 10
37
- DEFAULT_USERNAME: Final = "smile"
38
-
39
- # --- Const for Plugwise Smile and Stretch
40
  PLATFORMS: Final[list[str]] = [
41
  Platform.BINARY_SENSOR,
 
42
  Platform.CLIMATE,
43
  Platform.NUMBER,
44
  Platform.SELECT,
45
  Platform.SENSOR,
46
  Platform.SWITCH,
47
  ]
48
- SERVICE_DELETE: Final = "delete_notification"
49
- SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
50
-
51
- # Climate const:
52
- MASTER_THERMOSTATS: Final[list[str]] = [
53
- "thermostat",
54
- "zone_thermometer",
55
- "zone_thermostat",
56
- "thermostatic_radiator_valve",
57
- ]
58
-
59
- # Config_flow const:
60
  ZEROCONF_MAP: Final[dict[str, str]] = {
61
  "smile": "Smile P1",
62
  "smile_thermo": "Smile Anna",
@@ -64,15 +41,39 @@
64
  "stretch": "Stretch",
65
  }
66
 
67
- SelectType = Literal[
68
- "active_preset",
 
 
 
 
 
69
  "select_dhw_mode",
 
70
  "select_regulation_mode",
71
  "select_schedule",
72
  ]
73
- SelectOptionsType = Literal[
74
  "dhw_modes",
75
- "preset_modes",
76
  "regulation_modes",
77
  "available_schedules",
78
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
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
+ ]
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py RENAMED
@@ -1,23 +1,20 @@
1
  """DataUpdateCoordinator for Plugwise."""
 
2
  from datetime import timedelta
3
 
 
4
  from plugwise import PlugwiseData, Smile
5
  from plugwise.exceptions import (
6
  ConnectionFailedError,
7
  InvalidAuthentication,
8
  InvalidXMLError,
 
9
  ResponseError,
10
  UnsupportedDeviceError,
11
  )
12
 
13
  from homeassistant.config_entries import ConfigEntry
14
- from homeassistant.const import (
15
- CONF_HOST,
16
- CONF_PASSWORD,
17
- CONF_PORT,
18
- CONF_SCAN_INTERVAL, # pw-beta options
19
- CONF_USERNAME,
20
- )
21
  from homeassistant.core import HomeAssistant
22
  from homeassistant.exceptions import ConfigEntryError
23
  from homeassistant.helpers import device_registry as dr
@@ -25,48 +22,7 @@
25
  from homeassistant.helpers.debounce import Debouncer
26
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
27
 
28
- from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER
29
-
30
-
31
- def remove_stale_devices(
32
- api: Smile,
33
- device_registry: dr.DeviceRegistry,
34
- via_id: str,
35
- ) -> None:
36
- """Process the Plugwise devices present in the device_registry connected to a specific Gateway."""
37
- for dev_id, device_entry in list(device_registry.devices.items()):
38
- if device_entry.via_device_id == via_id:
39
- for item in device_entry.identifiers:
40
- if item[0] == DOMAIN and item[1] in api.device_list:
41
- continue
42
-
43
- device_registry.async_remove_device(dev_id)
44
- LOGGER.debug(
45
- "Removed device %s %s %s from device_registry",
46
- DOMAIN,
47
- device_entry.model,
48
- dev_id,
49
- )
50
-
51
-
52
- def cleanup_device_registry(
53
- hass: HomeAssistant,
54
- api: Smile,
55
- ) -> None:
56
- """Remove deleted devices from device-registry."""
57
- device_registry = dr.async_get(hass)
58
- via_id_list: list[list[str]] = []
59
- # Collect the required data of the Plugwise Gateway's
60
- for device_entry in list(device_registry.devices.values()):
61
- if device_entry.manufacturer == "Plugwise" and device_entry.model == "Gateway":
62
- for item in device_entry.identifiers:
63
- via_id_list.append([item[1], device_entry.id])
64
-
65
- for via_id in via_id_list:
66
- if via_id[0] != api.gateway_id:
67
- continue # pragma: no cover
68
-
69
- remove_stale_devices(api, device_registry, via_id[1])
70
 
71
 
72
  class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
@@ -74,90 +30,103 @@
74
 
75
  _connected: bool = False
76
 
77
- def __init__(
78
- self,
79
- hass: HomeAssistant,
80
- entry: ConfigEntry,
81
- cooldown: float,
82
- update_interval: timedelta = timedelta(seconds=60),
83
- ) -> None: # pw-beta cooldown
84
  """Initialize the coordinator."""
85
  super().__init__(
86
  hass,
87
  LOGGER,
88
  name=DOMAIN,
89
- # Core directly updates from const's DEFAULT_SCAN_INTERVAL
90
- update_interval=update_interval,
91
  # Don't refresh immediately, give the device time to process
92
  # the change in state before we query it.
93
  request_refresh_debouncer=Debouncer(
94
  hass,
95
  LOGGER,
96
- cooldown=cooldown,
97
  immediate=False,
98
  ),
99
  )
100
 
101
  self.api = Smile(
102
- host=entry.data[CONF_HOST],
103
- username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
104
- password=entry.data[CONF_PASSWORD],
105
- port=entry.data.get(CONF_PORT, DEFAULT_PORT),
106
- timeout=30,
107
  websession=async_get_clientsession(hass, verify_ssl=False),
108
  )
109
- self.hass = hass
110
- self._entry = entry
111
- self._unavailable_logged = False
112
- self.current_unique_ids: set[tuple[str, str]] = {("dummy", "dummy_id")}
113
- self.update_interval = update_interval
114
 
115
  async def _connect(self) -> None:
116
  """Connect to the Plugwise Smile."""
117
- self._connected = await self.api.connect()
118
- self.api.get_all_devices()
119
-
120
- self.update_interval = DEFAULT_SCAN_INTERVAL.get(
121
- self.api.smile_type, timedelta(seconds=60)
122
- ) # pw-beta options scan-interval
123
- if (custom_time := self._entry.options.get(CONF_SCAN_INTERVAL)) is not None:
124
- self.update_interval = timedelta(
125
- seconds=int(custom_time)
126
- ) # pragma: no cover # pw-beta options
127
-
128
- LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
129
 
130
  async def _async_update_data(self) -> PlugwiseData:
131
  """Fetch data from Plugwise."""
132
- data = PlugwiseData(gateway={}, devices={})
133
-
134
  try:
135
  if not self._connected:
136
  await self._connect()
137
  data = await self.api.async_update()
138
- LOGGER.debug(f"{self.api.smile_name} data: %s", data)
139
- if self._unavailable_logged:
140
- self._unavailable_logged = False
141
  except InvalidAuthentication as err:
142
- if not self._unavailable_logged: # pw-beta add to Core
143
- self._unavailable_logged = True
144
- raise ConfigEntryError("Authentication failed") from err
145
  except (InvalidXMLError, ResponseError) as err:
146
- if not self._unavailable_logged: # pw-beta add to Core
147
- self._unavailable_logged = True
148
- raise UpdateFailed(
149
- "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
150
- ) from err
151
  except UnsupportedDeviceError as err:
152
- if not self._unavailable_logged: # pw-beta add to Core
153
- self._unavailable_logged = True
154
- raise ConfigEntryError("Device with unsupported firmware") from err
155
- except ConnectionFailedError as err:
156
- if not self._unavailable_logged: # pw-beta add to Core
157
- self._unavailable_logged = True
158
- raise UpdateFailed("Failed to connect") from err
159
-
160
- # Clean-up removed devices
161
- cleanup_device_registry(self.hass, self.api)
162
 
163
  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
 
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
+ )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py RENAMED
@@ -1,25 +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 .const import (
10
- COORDINATOR, # pw-beta
11
- DOMAIN,
12
- )
13
- from .coordinator import PlugwiseDataUpdateCoordinator
14
 
15
 
16
  async def async_get_config_entry_diagnostics(
17
- hass: HomeAssistant, entry: ConfigEntry
18
  ) -> dict[str, Any]:
19
  """Return diagnostics for a config entry."""
20
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
21
- COORDINATOR
22
- ]
23
  return {
24
  "gateway": coordinator.data.gateway,
25
  "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,
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py RENAMED
@@ -1,4 +1,5 @@
1
  """Generic Plugwise Entity Class."""
 
2
  from __future__ import annotations
3
 
4
  from plugwise.constants import DeviceData
@@ -46,6 +47,7 @@
46
  connections=connections,
47
  manufacturer=data.get("vendor"),
48
  model=data.get("model"),
 
49
  name=coordinator.data.gateway["smile_name"],
50
  sw_version=data.get("firmware"),
51
  hw_version=data.get("hardware"),
@@ -67,8 +69,6 @@
67
  """Return if entity is available."""
68
  return (
69
  self._dev_id in self.coordinator.data.devices
70
- # Do not change the below line: some Plugwise devices
71
- # do not provide their availability-status!
72
  and ("available" not in self.device or self.device["available"] is True)
73
  and super().available
74
  )
 
1
  """Generic Plugwise Entity Class."""
2
+
3
  from __future__ import annotations
4
 
5
  from plugwise.constants import DeviceData
 
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
  )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json RENAMED
@@ -1,13 +1,12 @@
1
  {
2
  "domain": "plugwise",
3
- "name": "Plugwise Smile/Stretch Beta",
4
- "after_dependencies": ["zeroconf"],
5
- "codeowners": ["@CoMPaTech", "@bouwew"],
6
  "config_flow": true,
7
- "documentation": "https://github.com/plugwise/plugwise-beta",
8
  "integration_type": "hub",
9
  "iot_class": "local_polling",
10
  "loggers": ["plugwise"],
11
- "requirements": ["plugwise==0.35.1"],
12
- "version": "0.45.0"
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
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py RENAMED
@@ -1,43 +1,28 @@
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 plugwise import Smile
8
- from plugwise.constants import NumberType
9
-
10
  from homeassistant.components.number import (
11
  NumberDeviceClass,
12
  NumberEntity,
13
  NumberEntityDescription,
14
  NumberMode,
15
  )
16
- from homeassistant.config_entries import ConfigEntry
17
  from homeassistant.const import EntityCategory, UnitOfTemperature
18
- from homeassistant.core import HomeAssistant
19
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
20
 
21
- from .const import (
22
- COORDINATOR, # pw-beta
23
- DOMAIN,
24
- LOGGER,
25
- )
26
  from .coordinator import PlugwiseDataUpdateCoordinator
27
  from .entity import PlugwiseEntity
 
28
 
29
 
30
- @dataclass
31
- class PlugwiseEntityDescriptionMixin:
32
- """Mixin values for Plugwise entities."""
33
-
34
- command: Callable[[Smile, str, str, float], Awaitable[None]]
35
-
36
-
37
- @dataclass
38
- class PlugwiseNumberEntityDescription(
39
- NumberEntityDescription, PlugwiseEntityDescriptionMixin
40
- ):
41
  """Class describing Plugwise Number entities."""
42
 
43
  key: NumberType
@@ -47,9 +32,6 @@
47
  PlugwiseNumberEntityDescription(
48
  key="maximum_boiler_temperature",
49
  translation_key="maximum_boiler_temperature",
50
- command=lambda api, number, dev_id, value: api.set_number_setpoint(
51
- number, dev_id, value
52
- ),
53
  device_class=NumberDeviceClass.TEMPERATURE,
54
  entity_category=EntityCategory.CONFIG,
55
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -57,9 +39,6 @@
57
  PlugwiseNumberEntityDescription(
58
  key="max_dhw_temperature",
59
  translation_key="max_dhw_temperature",
60
- command=lambda api, number, dev_id, value: api.set_number_setpoint(
61
- number, dev_id, value
62
- ),
63
  device_class=NumberDeviceClass.TEMPERATURE,
64
  entity_category=EntityCategory.CONFIG,
65
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -67,9 +46,6 @@
67
  PlugwiseNumberEntityDescription(
68
  key="temperature_offset",
69
  translation_key="temperature_offset",
70
- command=lambda api, number, dev_id, value: api.set_temperature_offset(
71
- number, dev_id, value
72
- ),
73
  device_class=NumberDeviceClass.TEMPERATURE,
74
  entity_category=EntityCategory.CONFIG,
75
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -79,27 +55,27 @@
79
 
80
  async def async_setup_entry(
81
  hass: HomeAssistant,
82
- config_entry: ConfigEntry,
83
  async_add_entities: AddEntitiesCallback,
84
  ) -> None:
85
  """Set up Plugwise number platform."""
 
86
 
87
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
88
- config_entry.entry_id
89
- ][COORDINATOR]
90
-
91
- entities: list[PlugwiseNumberEntity] = []
92
- for device_id, device in coordinator.data.devices.items():
93
- for description in NUMBER_TYPES:
94
- if description.key in device:
95
- entities.append(
96
- PlugwiseNumberEntity(coordinator, device_id, description)
97
- )
98
- LOGGER.debug(
99
- "Add %s %s number", device["name"], description.translation_key
100
- )
101
 
102
- async_add_entities(entities)
 
103
 
104
 
105
  class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
@@ -115,7 +91,6 @@
115
  ) -> None:
116
  """Initiate Plugwise Number."""
117
  super().__init__(coordinator, device_id)
118
- self.actuator = self.device[description.key]
119
  self.device_id = device_id
120
  self.entity_description = description
121
  self._attr_unique_id = f"{device_id}-{description.key}"
@@ -133,12 +108,9 @@
133
  """Return the present setpoint value."""
134
  return self.device[self.entity_description.key]["setpoint"]
135
 
 
136
  async def async_set_native_value(self, value: float) -> None:
137
  """Change to the new setpoint value."""
138
- await self.entity_description.command(
139
- self.coordinator.api, self.entity_description.key, self.device_id, value
140
- )
141
- LOGGER.debug(
142
- "Setting %s to %s was successful", self.entity_description.name, value
143
  )
144
- 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 (
8
  NumberDeviceClass,
9
  NumberEntity,
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
 
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,
 
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,
 
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,
 
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}"
 
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
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py RENAMED
@@ -1,105 +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
 
7
- from plugwise import Smile
8
-
9
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
10
- from homeassistant.config_entries import ConfigEntry
11
  from homeassistant.const import STATE_ON, EntityCategory
12
- from homeassistant.core import HomeAssistant
13
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
14
 
15
- from .const import (
16
- COORDINATOR, # pw-beta
17
- DOMAIN,
18
- LOGGER,
19
- SelectOptionsType,
20
- SelectType,
21
- )
22
  from .coordinator import PlugwiseDataUpdateCoordinator
23
  from .entity import PlugwiseEntity
 
24
 
25
- PARALLEL_UPDATES = 0
26
-
27
-
28
- @dataclass
29
- class PlugwiseSelectDescriptionMixin:
30
- """Mixin values for Plugwise Select entities."""
31
 
32
- command: Callable[[Smile, str, str], Awaitable[None]]
33
- options_key: SelectOptionsType
34
-
35
-
36
- @dataclass
37
- class PlugwiseSelectEntityDescription(
38
- SelectEntityDescription, PlugwiseSelectDescriptionMixin
39
- ):
40
  """Class describing Plugwise Select entities."""
41
 
42
  key: SelectType
 
43
 
44
 
45
  SELECT_TYPES = (
46
  PlugwiseSelectEntityDescription(
47
  key="select_schedule",
48
- translation_key="thermostat_schedule",
49
- icon="mdi:calendar-clock",
50
- command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt),
51
  options_key="available_schedules",
52
  ),
53
  PlugwiseSelectEntityDescription(
54
- key="active_preset",
55
- translation_key="thermostat_preset",
56
- icon="mdi:room_preferences",
57
- command=lambda api, loc, opt: api.set_preset(loc, opt),
58
- options_key="preset_modes",
59
- ),
60
- PlugwiseSelectEntityDescription(
61
  key="select_regulation_mode",
62
  translation_key="regulation_mode",
63
- icon="mdi:hvac",
64
  entity_category=EntityCategory.CONFIG,
65
- command=lambda api, loc, opt: api.set_regulation_mode(opt),
66
  options_key="regulation_modes",
67
  ),
68
  PlugwiseSelectEntityDescription(
69
  key="select_dhw_mode",
70
  translation_key="dhw_mode",
71
- icon="mdi:shower",
72
  entity_category=EntityCategory.CONFIG,
73
- command=lambda api, loc, opt: api.set_dhw_mode(opt),
74
  options_key="dhw_modes",
75
  ),
 
 
 
 
 
 
76
  )
77
 
78
 
79
  async def async_setup_entry(
80
  hass: HomeAssistant,
81
- config_entry: ConfigEntry,
82
  async_add_entities: AddEntitiesCallback,
83
  ) -> None:
84
  """Set up the Smile selector from a config entry."""
85
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
86
- config_entry.entry_id
87
- ][COORDINATOR]
88
-
89
- entities: list[PlugwiseSelectEntity] = []
90
- for device_id, device in coordinator.data.devices.items():
91
- for description in SELECT_TYPES:
92
- LOGGER.debug("HOI device: %s", device)
93
- if description.options_key in device:
94
- LOGGER.debug("HOI options_key: %s", description.options_key)
95
- entities.append(
96
- PlugwiseSelectEntity(coordinator, device_id, description)
97
- )
98
- LOGGER.debug(
99
- "Add %s %s selector", device["name"], description.translation_key
100
- )
101
 
102
- async_add_entities(entities)
 
103
 
104
 
105
  class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
@@ -117,23 +91,23 @@
117
  super().__init__(coordinator, device_id)
118
  self.entity_description = entity_description
119
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
120
- self._attr_options = []
121
- if (options := self.device[entity_description.options_key]) is not None:
122
- self._attr_options = options
123
 
124
  @property
125
- def current_option(self) -> str | None:
126
  """Return the selected entity option to represent the entity state."""
127
  return self.device[self.entity_description.key]
128
 
 
 
 
 
 
 
129
  async def async_select_option(self, option: str) -> None:
130
- """Change to the selected entity option."""
131
- await self.entity_description.command(
132
- self.coordinator.api, self.device["location"], option
133
- )
134
- LOGGER.debug(
135
- "Set %s to %s was successful.",
136
- self.entity_description.name,
137
- option,
138
  )
139
- 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):
 
91
  super().__init__(coordinator, device_id)
92
  self.entity_description = entity_description
93
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
 
 
 
94
 
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
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py RENAMED
@@ -1,4 +1,5 @@
1
  """Plugwise Sensor component for Home Assistant."""
 
2
  from __future__ import annotations
3
 
4
  from dataclasses import dataclass
@@ -11,7 +12,6 @@
11
  SensorEntityDescription,
12
  SensorStateClass,
13
  )
14
- from homeassistant.config_entries import ConfigEntry
15
  from homeassistant.const import (
16
  LIGHT_LUX,
17
  PERCENTAGE,
@@ -24,178 +24,191 @@
24
  UnitOfVolume,
25
  UnitOfVolumeFlowRate,
26
  )
27
- from homeassistant.core import HomeAssistant
28
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
29
 
30
- from .const import (
31
- COORDINATOR, # pw-beta
32
- DOMAIN,
33
- LOGGER,
34
- )
35
  from .coordinator import PlugwiseDataUpdateCoordinator
36
  from .entity import PlugwiseEntity
37
 
38
- PARALLEL_UPDATES = 0
39
-
40
 
41
- @dataclass
42
  class PlugwiseSensorEntityDescription(SensorEntityDescription):
43
  """Describes Plugwise sensor entity."""
44
 
45
  key: SensorType
46
- state_class: str | None = SensorStateClass.MEASUREMENT
47
 
48
 
49
  SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
50
  PlugwiseSensorEntityDescription(
51
  key="setpoint",
52
  translation_key="setpoint",
 
53
  device_class=SensorDeviceClass.TEMPERATURE,
 
54
  entity_category=EntityCategory.DIAGNOSTIC,
55
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
56
  ),
57
  PlugwiseSensorEntityDescription(
58
  key="setpoint_high",
59
  translation_key="cooling_setpoint",
 
60
  device_class=SensorDeviceClass.TEMPERATURE,
 
61
  entity_category=EntityCategory.DIAGNOSTIC,
62
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
63
  ),
64
  PlugwiseSensorEntityDescription(
65
  key="setpoint_low",
66
  translation_key="heating_setpoint",
 
67
  device_class=SensorDeviceClass.TEMPERATURE,
 
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
70
  ),
71
  PlugwiseSensorEntityDescription(
72
  key="temperature",
 
73
  device_class=SensorDeviceClass.TEMPERATURE,
74
  entity_category=EntityCategory.DIAGNOSTIC,
75
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
76
  ),
77
  PlugwiseSensorEntityDescription(
78
  key="intended_boiler_temperature",
79
  translation_key="intended_boiler_temperature",
 
80
  device_class=SensorDeviceClass.TEMPERATURE,
81
  entity_category=EntityCategory.DIAGNOSTIC,
82
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
83
  ),
84
  PlugwiseSensorEntityDescription(
85
  key="temperature_difference",
86
  translation_key="temperature_difference",
87
- entity_category=EntityCategory.DIAGNOSTIC,
88
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
 
 
89
  ),
90
  PlugwiseSensorEntityDescription(
91
  key="outdoor_temperature",
92
  translation_key="outdoor_temperature",
93
- device_class=SensorDeviceClass.TEMPERATURE,
94
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
95
- suggested_display_precision=1,
 
96
  ),
97
  PlugwiseSensorEntityDescription(
98
  key="outdoor_air_temperature",
99
  translation_key="outdoor_air_temperature",
 
100
  device_class=SensorDeviceClass.TEMPERATURE,
101
  entity_category=EntityCategory.DIAGNOSTIC,
102
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
103
- suggested_display_precision=1,
104
  ),
105
  PlugwiseSensorEntityDescription(
106
  key="water_temperature",
107
  translation_key="water_temperature",
 
108
  device_class=SensorDeviceClass.TEMPERATURE,
109
  entity_category=EntityCategory.DIAGNOSTIC,
110
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
111
  ),
112
  PlugwiseSensorEntityDescription(
113
  key="return_temperature",
114
  translation_key="return_temperature",
 
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  entity_category=EntityCategory.DIAGNOSTIC,
117
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
118
  ),
119
  PlugwiseSensorEntityDescription(
120
  key="electricity_consumed",
121
  translation_key="electricity_consumed",
122
- device_class=SensorDeviceClass.POWER,
123
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
124
  ),
125
  PlugwiseSensorEntityDescription(
126
  key="electricity_produced",
127
  translation_key="electricity_produced",
128
- device_class=SensorDeviceClass.POWER,
129
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
130
  entity_registry_enabled_default=False,
131
  ),
132
  PlugwiseSensorEntityDescription(
133
  key="electricity_consumed_interval",
134
  translation_key="electricity_consumed_interval",
135
- icon="mdi:lightning-bolt",
136
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
137
  ),
138
  PlugwiseSensorEntityDescription(
139
  key="electricity_consumed_peak_interval",
140
  translation_key="electricity_consumed_peak_interval",
141
- icon="mdi:lightning-bolt",
142
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
143
  ),
144
  PlugwiseSensorEntityDescription(
145
  key="electricity_consumed_off_peak_interval",
146
  translation_key="electricity_consumed_off_peak_interval",
147
- icon="mdi:lightning-bolt",
148
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
149
  ),
150
  PlugwiseSensorEntityDescription(
151
  key="electricity_produced_interval",
152
  translation_key="electricity_produced_interval",
153
- icon="mdi:lightning-bolt",
154
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
155
  entity_registry_enabled_default=False,
156
  ),
157
  PlugwiseSensorEntityDescription(
158
  key="electricity_produced_peak_interval",
159
  translation_key="electricity_produced_peak_interval",
160
- icon="mdi:lightning-bolt",
161
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
162
  ),
163
  PlugwiseSensorEntityDescription(
164
  key="electricity_produced_off_peak_interval",
165
  translation_key="electricity_produced_off_peak_interval",
166
- icon="mdi:lightning-bolt",
167
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
168
  ),
169
  PlugwiseSensorEntityDescription(
170
  key="electricity_consumed_point",
171
  translation_key="electricity_consumed_point",
172
  device_class=SensorDeviceClass.POWER,
173
  native_unit_of_measurement=UnitOfPower.WATT,
 
174
  ),
175
  PlugwiseSensorEntityDescription(
176
  key="electricity_consumed_off_peak_point",
177
  translation_key="electricity_consumed_off_peak_point",
178
- device_class=SensorDeviceClass.POWER,
179
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
180
  ),
181
  PlugwiseSensorEntityDescription(
182
  key="electricity_consumed_peak_point",
183
  translation_key="electricity_consumed_peak_point",
184
- device_class=SensorDeviceClass.POWER,
185
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
186
  ),
187
  PlugwiseSensorEntityDescription(
188
  key="electricity_consumed_off_peak_cumulative",
189
  translation_key="electricity_consumed_off_peak_cumulative",
190
- device_class=SensorDeviceClass.ENERGY,
191
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
192
  state_class=SensorStateClass.TOTAL_INCREASING,
193
  ),
194
  PlugwiseSensorEntityDescription(
195
  key="electricity_consumed_peak_cumulative",
196
  translation_key="electricity_consumed_peak_cumulative",
197
- device_class=SensorDeviceClass.ENERGY,
198
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
199
  state_class=SensorStateClass.TOTAL_INCREASING,
200
  ),
201
  PlugwiseSensorEntityDescription(
@@ -203,24 +216,27 @@
203
  translation_key="electricity_produced_point",
204
  device_class=SensorDeviceClass.POWER,
205
  native_unit_of_measurement=UnitOfPower.WATT,
 
206
  ),
207
  PlugwiseSensorEntityDescription(
208
  key="electricity_produced_off_peak_point",
209
  translation_key="electricity_produced_off_peak_point",
210
- device_class=SensorDeviceClass.POWER,
211
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
212
  ),
213
  PlugwiseSensorEntityDescription(
214
  key="electricity_produced_peak_point",
215
  translation_key="electricity_produced_peak_point",
216
- device_class=SensorDeviceClass.POWER,
217
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
218
  ),
219
  PlugwiseSensorEntityDescription(
220
  key="electricity_produced_off_peak_cumulative",
221
  translation_key="electricity_produced_off_peak_cumulative",
222
- device_class=SensorDeviceClass.ENERGY,
223
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
224
  state_class=SensorStateClass.TOTAL_INCREASING,
225
  ),
226
  PlugwiseSensorEntityDescription(
@@ -233,45 +249,51 @@
233
  PlugwiseSensorEntityDescription(
234
  key="electricity_phase_one_consumed",
235
  translation_key="electricity_phase_one_consumed",
236
- name="Electricity phase one consumed",
237
  device_class=SensorDeviceClass.POWER,
238
  native_unit_of_measurement=UnitOfPower.WATT,
 
239
  ),
240
  PlugwiseSensorEntityDescription(
241
  key="electricity_phase_two_consumed",
242
  translation_key="electricity_phase_two_consumed",
243
  device_class=SensorDeviceClass.POWER,
244
  native_unit_of_measurement=UnitOfPower.WATT,
 
245
  ),
246
  PlugwiseSensorEntityDescription(
247
  key="electricity_phase_three_consumed",
248
  translation_key="electricity_phase_three_consumed",
249
  device_class=SensorDeviceClass.POWER,
250
  native_unit_of_measurement=UnitOfPower.WATT,
 
251
  ),
252
  PlugwiseSensorEntityDescription(
253
  key="electricity_phase_one_produced",
254
  translation_key="electricity_phase_one_produced",
255
  device_class=SensorDeviceClass.POWER,
256
  native_unit_of_measurement=UnitOfPower.WATT,
 
257
  ),
258
  PlugwiseSensorEntityDescription(
259
  key="electricity_phase_two_produced",
260
  translation_key="electricity_phase_two_produced",
261
  device_class=SensorDeviceClass.POWER,
262
  native_unit_of_measurement=UnitOfPower.WATT,
 
263
  ),
264
  PlugwiseSensorEntityDescription(
265
  key="electricity_phase_three_produced",
266
  translation_key="electricity_phase_three_produced",
267
  device_class=SensorDeviceClass.POWER,
268
  native_unit_of_measurement=UnitOfPower.WATT,
 
269
  ),
270
  PlugwiseSensorEntityDescription(
271
  key="voltage_phase_one",
272
  translation_key="voltage_phase_one",
273
  device_class=SensorDeviceClass.VOLTAGE,
274
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
275
  entity_registry_enabled_default=False,
276
  ),
277
  PlugwiseSensorEntityDescription(
@@ -279,6 +301,7 @@
279
  translation_key="voltage_phase_two",
280
  device_class=SensorDeviceClass.VOLTAGE,
281
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
282
  entity_registry_enabled_default=False,
283
  ),
284
  PlugwiseSensorEntityDescription(
@@ -286,116 +309,121 @@
286
  translation_key="voltage_phase_three",
287
  device_class=SensorDeviceClass.VOLTAGE,
288
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
289
  entity_registry_enabled_default=False,
290
  ),
291
  PlugwiseSensorEntityDescription(
292
  key="gas_consumed_interval",
293
  translation_key="gas_consumed_interval",
294
- icon="mdi:meter-gas",
295
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
 
296
  ),
297
  PlugwiseSensorEntityDescription(
298
  key="gas_consumed_cumulative",
299
  translation_key="gas_consumed_cumulative",
300
- device_class=SensorDeviceClass.GAS,
301
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
302
- state_class=SensorStateClass.TOTAL_INCREASING,
 
303
  ),
304
  PlugwiseSensorEntityDescription(
305
  key="net_electricity_point",
306
  translation_key="net_electricity_point",
307
- device_class=SensorDeviceClass.POWER,
308
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
309
  ),
310
  PlugwiseSensorEntityDescription(
311
  key="net_electricity_cumulative",
312
  translation_key="net_electricity_cumulative",
313
- device_class=SensorDeviceClass.ENERGY,
314
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
315
  state_class=SensorStateClass.TOTAL,
316
  ),
317
  PlugwiseSensorEntityDescription(
318
  key="battery",
 
319
  device_class=SensorDeviceClass.BATTERY,
320
  entity_category=EntityCategory.DIAGNOSTIC,
321
- native_unit_of_measurement=PERCENTAGE,
322
  ),
323
  PlugwiseSensorEntityDescription(
324
  key="illuminance",
 
325
  device_class=SensorDeviceClass.ILLUMINANCE,
 
326
  entity_category=EntityCategory.DIAGNOSTIC,
327
- native_unit_of_measurement=LIGHT_LUX,
328
  ),
329
  PlugwiseSensorEntityDescription(
330
  key="modulation_level",
331
  translation_key="modulation_level",
332
- entity_category=EntityCategory.DIAGNOSTIC,
333
  native_unit_of_measurement=PERCENTAGE,
334
- icon="mdi:percent",
 
335
  ),
336
  PlugwiseSensorEntityDescription(
337
  key="valve_position",
338
  translation_key="valve_position",
339
- icon="mdi:valve",
340
  entity_category=EntityCategory.DIAGNOSTIC,
341
  native_unit_of_measurement=PERCENTAGE,
 
342
  ),
343
  PlugwiseSensorEntityDescription(
344
  key="water_pressure",
345
  translation_key="water_pressure",
 
346
  device_class=SensorDeviceClass.PRESSURE,
347
  entity_category=EntityCategory.DIAGNOSTIC,
348
- native_unit_of_measurement=UnitOfPressure.BAR,
349
  ),
350
  PlugwiseSensorEntityDescription(
351
  key="humidity",
352
- device_class=SensorDeviceClass.HUMIDITY,
353
  native_unit_of_measurement=PERCENTAGE,
 
 
354
  ),
355
  PlugwiseSensorEntityDescription(
356
  key="dhw_temperature",
357
  translation_key="dhw_temperature",
 
358
  device_class=SensorDeviceClass.TEMPERATURE,
359
  entity_category=EntityCategory.DIAGNOSTIC,
360
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
361
  ),
362
  PlugwiseSensorEntityDescription(
363
  key="domestic_hot_water_setpoint",
364
  translation_key="domestic_hot_water_setpoint",
 
365
  device_class=SensorDeviceClass.TEMPERATURE,
366
  entity_category=EntityCategory.DIAGNOSTIC,
367
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
368
  ),
369
  )
370
 
371
 
372
  async def async_setup_entry(
373
  hass: HomeAssistant,
374
- config_entry: ConfigEntry,
375
  async_add_entities: AddEntitiesCallback,
376
  ) -> None:
377
  """Set up the Smile sensors from a config entry."""
378
- coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
379
 
380
- entities: list[PlugwiseSensorEntity] = []
381
- for device_id, device in coordinator.data.devices.items():
382
- if not (sensors := device.get("sensors")):
383
- continue
384
- for description in SENSORS:
385
- if description.key not in sensors:
386
- continue
387
- entities.append(
388
- PlugwiseSensorEntity(
389
- coordinator,
390
- device_id,
391
- description,
392
- )
393
- )
394
- LOGGER.debug(
395
- "Add %s %s sensor", device["name"], description.translation_key
396
- )
397
 
398
- async_add_entities(entities)
 
399
 
400
 
401
  class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
 
1
  """Plugwise Sensor component for Home Assistant."""
2
+
3
  from __future__ import annotations
4
 
5
  from dataclasses import dataclass
 
12
  SensorEntityDescription,
13
  SensorStateClass,
14
  )
 
15
  from homeassistant.const import (
16
  LIGHT_LUX,
17
  PERCENTAGE,
 
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(
 
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(
 
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(
 
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(
 
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):
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json RENAMED
@@ -1,52 +1,38 @@
1
  {
2
- "options": {
3
- "step": {
4
- "none": {
5
- "title": "No Options available",
6
- "description": "This Integration does not provide any Options"
7
- },
8
- "init": {
9
- "description": "Adjust Smile/Stretch Options",
10
- "data": {
11
- "cooling_on": "Anna: cooling-mode is on",
12
- "scan_interval": "Scan Interval (seconds) *) beta-only option",
13
- "homekit_emulation": "Homekit emulation (i.e. on hvac_off => Away) *) beta-only option",
14
- "refresh_interval": "Frontend refresh-time (1.5 - 5 seconds) *) beta-only option"
15
- }
16
- }
17
- }
18
- },
19
  "config": {
20
  "step": {
21
  "user": {
22
- "title": "Connect to the Plugwise Adam/Smile/Stretch",
23
- "description": "Please enter:",
24
  "data": {
25
- "password": "ID",
26
- "username": "Username",
27
- "host": "IP-address",
28
- "port": "Port number"
 
 
 
29
  }
30
  }
31
  },
32
  "error": {
33
- "cannot_connect": "Failed to connect",
34
- "invalid_auth": "Authentication failed",
35
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
36
- "network_down": "Plugwise Zigbee network is down",
37
- "network_timeout": "Network communication timeout",
38
  "response_error": "Invalid XML data, or error indication received",
39
- "stick_init": "Initialization of Plugwise USB-stick failed",
40
- "unknown": "Unknown error!",
41
  "unsupported": "Device with unsupported firmware"
42
  },
43
  "abort": {
44
- "already_configured": "This device is already configured",
45
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna"
46
  }
47
  },
48
  "entity": {
49
  "binary_sensor": {
 
 
 
50
  "compressor_state": {
51
  "name": "Compressor state"
52
  },
@@ -60,29 +46,40 @@
60
  "name": "Flame state"
61
  },
62
  "heating_state": {
63
- "name": "Heating"
64
  },
65
  "cooling_state": {
66
- "name": "Cooling"
67
  },
68
- "slave_boiler_state": {
69
  "name": "Secondary boiler state"
70
  },
71
  "plugwise_notification": {
72
  "name": "Plugwise notification"
73
  }
74
  },
 
 
 
 
 
75
  "climate": {
76
  "plugwise": {
77
  "state_attributes": {
 
 
 
78
  "preset_mode": {
79
  "state": {
80
  "asleep": "Night",
81
- "away": "Away",
82
- "home": "Home",
83
  "no_frost": "Anti-frost",
84
  "vacation": "Vacation"
85
  }
 
 
 
86
  }
87
  }
88
  }
@@ -102,10 +99,18 @@
102
  "dhw_mode": {
103
  "name": "DHW mode",
104
  "state": {
 
105
  "auto": "Auto",
106
- "boost": "Boost",
107
- "comfort": "Comfort",
108
- "off": "Off"
 
 
 
 
 
 
 
109
  }
110
  },
111
  "regulation_mode": {
@@ -113,26 +118,16 @@
113
  "state": {
114
  "bleeding_cold": "Bleeding cold",
115
  "bleeding_hot": "Bleeding hot",
116
- "cooling": "Cooling",
117
- "heating": "Heating",
118
- "off": "Off"
119
  }
120
  },
121
- "thermostat_schedule": {
122
  "name": "Thermostat schedule",
123
  "state": {
124
  "off": "Off"
125
  }
126
- },
127
- "thermostat_preset": {
128
- "name": "Thermostat preset",
129
- "state": {
130
- "asleep": "Night",
131
- "away": "Away",
132
- "home": "Home",
133
- "no_frost": "Anti-frost",
134
- "vacation": "Vacation"
135
- }
136
  }
137
  },
138
  "sensor": {
@@ -143,7 +138,7 @@
143
  "name": "Cooling setpoint"
144
  },
145
  "heating_setpoint": {
146
- "name": " Heating setpoint"
147
  },
148
  "intended_boiler_temperature": {
149
  "name": "Intended boiler temperature"
@@ -169,12 +164,6 @@
169
  "electricity_produced": {
170
  "name": "Electricity produced"
171
  },
172
- "electricity_consumed_point": {
173
- "name": "Electricity consumed point"
174
- },
175
- "electricity_produced_point": {
176
- "name": "Electricity produced point"
177
- },
178
  "electricity_consumed_interval": {
179
  "name": "Electricity consumed interval"
180
  },
@@ -193,6 +182,9 @@
193
  "electricity_produced_off_peak_interval": {
194
  "name": "Electricity produced off peak interval"
195
  },
 
 
 
196
  "electricity_consumed_off_peak_point": {
197
  "name": "Electricity consumed off peak point"
198
  },
@@ -205,6 +197,9 @@
205
  "electricity_consumed_peak_cumulative": {
206
  "name": "Electricity consumed peak cumulative"
207
  },
 
 
 
208
  "electricity_produced_off_peak_point": {
209
  "name": "Electricity produced off peak point"
210
  },
@@ -272,28 +267,22 @@
272
  "name": "DHW setpoint"
273
  },
274
  "maximum_boiler_temperature": {
275
- "name": "Maximum boiler temperature setpoint"
276
  }
277
  },
278
  "switch": {
279
  "cooling_ena_switch": {
280
- "name": "Cooling"
281
  },
282
  "dhw_cm_switch": {
283
  "name": "DHW comfort mode"
284
  },
285
  "lock": {
286
- "name": "Lock"
287
  },
288
  "relay": {
289
  "name": "Relay"
290
  }
291
  }
292
- },
293
- "services": {
294
- "delete_notification": {
295
- "name": "Delete Plugwise notification",
296
- "description": "Deletes a Plugwise Notification"
297
- }
298
  }
299
  }
 
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
  },
26
  "abort": {
27
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
28
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna"
29
  }
30
  },
31
  "entity": {
32
  "binary_sensor": {
33
+ "low_battery": {
34
+ "name": "Battery state"
35
+ },
36
  "compressor_state": {
37
  "name": "Compressor state"
38
  },
 
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
  }
 
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": {
 
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": {
 
138
  "name": "Cooling setpoint"
139
  },
140
  "heating_setpoint": {
141
+ "name": "Heating setpoint"
142
  },
143
  "intended_boiler_temperature": {
144
  "name": "Intended boiler temperature"
 
164
  "electricity_produced": {
165
  "name": "Electricity produced"
166
  },
 
 
 
 
 
 
167
  "electricity_consumed_interval": {
168
  "name": "Electricity consumed interval"
169
  },
 
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
  },
 
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
  },
 
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
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py RENAMED
@@ -1,4 +1,5 @@
1
  """Plugwise Switch component for HomeAssistant."""
 
2
  from __future__ import annotations
3
 
4
  from dataclasses import dataclass
@@ -11,22 +12,17 @@
11
  SwitchEntity,
12
  SwitchEntityDescription,
13
  )
14
- from homeassistant.config_entries import ConfigEntry
15
  from homeassistant.const import EntityCategory
16
- from homeassistant.core import HomeAssistant
17
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
18
 
19
- from .const import (
20
- COORDINATOR, # pw-beta
21
- DOMAIN,
22
- LOGGER,
23
- )
24
  from .coordinator import PlugwiseDataUpdateCoordinator
25
  from .entity import PlugwiseEntity
26
  from .util import plugwise_command
27
 
28
 
29
- @dataclass
30
  class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
31
  """Describes Plugwise switch entity."""
32
 
@@ -37,15 +33,11 @@
37
  PlugwiseSwitchEntityDescription(
38
  key="dhw_cm_switch",
39
  translation_key="dhw_cm_switch",
40
- icon="mdi:water-plus",
41
- device_class=SwitchDeviceClass.SWITCH,
42
  entity_category=EntityCategory.CONFIG,
43
  ),
44
  PlugwiseSwitchEntityDescription(
45
  key="lock",
46
  translation_key="lock",
47
- icon="mdi:lock",
48
- device_class=SwitchDeviceClass.SWITCH,
49
  entity_category=EntityCategory.CONFIG,
50
  ),
51
  PlugwiseSwitchEntityDescription(
@@ -56,8 +48,7 @@
56
  PlugwiseSwitchEntityDescription(
57
  key="cooling_ena_switch",
58
  translation_key="cooling_ena_switch",
59
- icon="mdi:snowflake-thermometer",
60
- device_class=SwitchDeviceClass.SWITCH,
61
  entity_category=EntityCategory.CONFIG,
62
  ),
63
  )
@@ -65,25 +56,28 @@
65
 
66
  async def async_setup_entry(
67
  hass: HomeAssistant,
68
- config_entry: ConfigEntry,
69
  async_add_entities: AddEntitiesCallback,
70
  ) -> None:
71
  """Set up the Smile switches from a config entry."""
72
- coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
73
 
74
- entities: list[PlugwiseSwitchEntity] = []
75
- for device_id, device in coordinator.data.devices.items():
76
- if not (switches := device.get("switches")):
77
- continue
78
- for description in SWITCHES:
79
- if description.key not in switches:
80
- continue
81
- entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
82
- LOGGER.debug(
83
- "Add %s %s switch", device["name"], description.translation_key
84
- )
 
 
85
 
86
- async_add_entities(entities)
 
87
 
88
 
89
  class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
 
1
  """Plugwise Switch component for HomeAssistant."""
2
+
3
  from __future__ import annotations
4
 
5
  from dataclasses import dataclass
 
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
 
 
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(
 
48
  PlugwiseSwitchEntityDescription(
49
  key="cooling_ena_switch",
50
  translation_key="cooling_ena_switch",
51
+ name="Cooling",
 
52
  entity_category=EntityCategory.CONFIG,
53
  ),
54
  )
 
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):
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py RENAMED
@@ -1,8 +1,7 @@
1
  """Utilities for Plugwise."""
2
- from __future__ import annotations
3
 
4
  from collections.abc import Awaitable, Callable, Coroutine
5
- from typing import Any, Concatenate, ParamSpec, TypeVar
6
 
7
  from plugwise.exceptions import PlugwiseException
8
 
@@ -10,13 +9,9 @@
10
 
11
  from .entity import PlugwiseEntity
12
 
13
- _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
14
- _R = TypeVar("_R")
15
- _P = ParamSpec("_P")
16
 
17
-
18
- def plugwise_command(
19
- func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]]
20
  ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
21
  """Decorate Plugwise calls that send commands/make changes to the device.
22
 
 
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
 
 
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