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 +35 -80
  2. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py +40 -87
  3. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py +205 -104
  4. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py +127 -172
  5. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py +58 -34
  6. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py +152 -63
  7. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py +4 -15
  8. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py +32 -33
  9. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json +5 -5
  10. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py +31 -60
  11. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py +68 -65
  12. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py +105 -86
  13. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json +200 -154
  14. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py +26 -33
  15. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py +12 -10
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py RENAMED
@@ -1,119 +1,68 @@
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(
53
  config_entry_id=entry.entry_id,
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 +75,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 +86,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 typing import Any
4
 
 
 
 
 
5
  from homeassistant.const import Platform
6
+ from homeassistant.core import HomeAssistant, callback
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
8
 
9
+ from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
10
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
 
 
11
 
12
 
13
+ async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
14
+ """Set up Plugwise components from a config entry."""
15
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
16
 
17
+ coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
 
 
 
 
 
 
 
 
 
18
  await coordinator.async_config_entry_first_refresh()
 
19
  migrate_sensor_entities(hass, coordinator)
20
 
21
+ entry.runtime_data = coordinator
 
 
 
 
 
22
 
23
  device_registry = dr.async_get(hass)
24
  device_registry.async_get_or_create(
25
  config_entry_id=entry.entry_id,
26
  identifiers={(DOMAIN, str(coordinator.api.gateway_id))},
27
  manufacturer="Plugwise",
28
+ model=coordinator.api.smile.model,
29
+ model_id=coordinator.api.smile.model_id,
30
+ name=coordinator.api.smile.name,
31
+ sw_version=str(coordinator.api.smile.version),
32
+ ) # required for adding the entity-less P1 Gateway
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
35
 
 
 
 
 
 
 
36
  return True
37
 
38
 
39
+ async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
40
+ """Unload the Plugwise components."""
41
+ return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  @callback
45
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
46
  """Migrate Plugwise entity entries.
47
 
48
+ Migrates old unique ID's from old binary_sensors and switches to the new unique ID's.
49
  """
50
+ if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
51
+ "-slave_boiler_state"
52
+ ):
53
+ return {
54
+ "new_unique_id": entry.unique_id.replace(
55
+ "-slave_boiler_state", "-secondary_boiler_state"
56
+ )
57
+ }
58
  if entry.domain == Platform.SENSOR and entry.unique_id.endswith(
59
  "-relative_humidity"
60
  ):
61
  return {
62
  "new_unique_id": entry.unique_id.replace("-relative_humidity", "-humidity")
63
  }
64
+ if entry.domain == Platform.SWITCH and entry.unique_id.endswith("-plug"):
65
+ return {"new_unique_id": entry.unique_id.replace("-plug", "-relay")}
66
 
67
  # No migration needed
68
  return None
 
75
  """Migrate Sensors if needed."""
76
  ent_reg = er.async_get(hass)
77
 
78
+ # Migrating opentherm_outdoor_temperature
79
  # to opentherm_outdoor_air_temperature sensor
80
+ for device_id, device in coordinator.data.items():
81
+ if device[DEV_CLASS] != "heater_central":
82
  continue
83
 
84
  old_unique_id = f"{device_id}-outdoor_temperature"
 
86
  Platform.SENSOR, DOMAIN, old_unique_id
87
  ):
88
  new_unique_id = f"{device_id}-outdoor_air_temperature"
89
+ LOGGER.debug(
90
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
91
+ entity_id,
92
+ old_unique_id,
93
+ new_unique_id,
94
+ )
95
  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,5 +1,4 @@
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
- from __future__ import annotations
3
 
4
  from collections.abc import Mapping
5
  from dataclasses import dataclass
@@ -8,88 +7,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 +82,28 @@
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 +121,26 @@
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 collections.abc import Mapping
4
  from dataclasses import dataclass
 
7
  from plugwise.constants import BinarySensorType
8
 
9
  from homeassistant.components.binary_sensor import (
10
+ BinarySensorDeviceClass,
11
  BinarySensorEntity,
12
  BinarySensorEntityDescription,
13
  )
 
14
  from homeassistant.const import EntityCategory
15
+ from homeassistant.core import HomeAssistant, callback
16
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
18
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
 
19
  from .entity import PlugwiseEntity
20
 
21
+ SEVERITIES = ["other", "info", "warning", "error"]
22
+
23
+ # Coordinator is used to centralize the data updates
24
  PARALLEL_UPDATES = 0
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
+ device_class=BinarySensorDeviceClass.BATTERY,
38
+ entity_category=EntityCategory.DIAGNOSTIC,
39
+ ),
40
+ PlugwiseBinarySensorEntityDescription(
41
  key="compressor_state",
42
  translation_key="compressor_state",
 
 
43
  entity_category=EntityCategory.DIAGNOSTIC,
44
  ),
45
  PlugwiseBinarySensorEntityDescription(
46
  key="cooling_enabled",
47
  translation_key="cooling_enabled",
 
48
  entity_category=EntityCategory.DIAGNOSTIC,
49
  ),
50
  PlugwiseBinarySensorEntityDescription(
51
  key="dhw_state",
52
  translation_key="dhw_state",
 
 
53
  entity_category=EntityCategory.DIAGNOSTIC,
54
  ),
55
  PlugwiseBinarySensorEntityDescription(
56
  key="flame_state",
57
  translation_key="flame_state",
 
 
58
  entity_category=EntityCategory.DIAGNOSTIC,
59
  ),
60
  PlugwiseBinarySensorEntityDescription(
61
  key="heating_state",
62
  translation_key="heating_state",
 
 
63
  entity_category=EntityCategory.DIAGNOSTIC,
64
  ),
65
  PlugwiseBinarySensorEntityDescription(
66
  key="cooling_state",
67
  translation_key="cooling_state",
 
 
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
  ),
70
  PlugwiseBinarySensorEntityDescription(
71
+ key="secondary_boiler_state",
72
+ translation_key="secondary_boiler_state",
 
 
73
  entity_category=EntityCategory.DIAGNOSTIC,
74
  ),
75
  PlugwiseBinarySensorEntityDescription(
76
  key="plugwise_notification",
77
  translation_key="plugwise_notification",
 
 
78
  entity_category=EntityCategory.DIAGNOSTIC,
79
  ),
80
  )
 
82
 
83
  async def async_setup_entry(
84
  hass: HomeAssistant,
85
+ entry: PlugwiseConfigEntry,
86
+ async_add_entities: AddConfigEntryEntitiesCallback,
87
  ) -> None:
88
+ """Set up the Smile binary_sensors from a config entry."""
89
+ coordinator = entry.runtime_data
90
+
91
+ @callback
92
+ def _add_entities() -> None:
93
+ """Add Entities."""
94
+ if not coordinator.new_devices:
95
+ return
96
+
97
+ async_add_entities(
98
+ PlugwiseBinarySensorEntity(coordinator, device_id, description)
99
+ for device_id in coordinator.new_devices
100
+ if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
101
+ for description in BINARY_SENSORS
102
+ if description.key in binary_sensors
103
+ )
 
 
 
 
 
 
104
 
105
+ _add_entities()
106
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
107
 
108
 
109
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
 
121
  super().__init__(coordinator, device_id)
122
  self.entity_description = description
123
  self._attr_unique_id = f"{device_id}-{description.key}"
 
124
 
125
  @property
126
  def is_on(self) -> bool:
127
  """Return true if the binary sensor is on."""
 
 
 
 
 
 
 
 
128
  return self.device["binary_sensors"][self.entity_description.key]
129
 
130
  @property
 
 
 
 
 
 
 
131
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
132
  """Return entity specific state attributes."""
133
  if self.entity_description.key != "plugwise_notification":
134
  return None
135
 
136
+ attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
137
+ gateway_id = self.coordinator.api.gateway_id
138
+ if notify := self.coordinator.data[gateway_id]["notifications"]:
139
+ for details in notify.values():
 
 
140
  for msg_type, msg in details.items():
141
  msg_type = msg_type.lower()
142
  if msg_type not in SEVERITIES:
143
+ msg_type = "other"
 
 
 
 
 
144
  attrs[f"{msg_type}_msg"].append(msg)
145
 
 
 
 
 
146
  return attrs
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py RENAMED
@@ -1,6 +1,6 @@
1
  """Plugwise Climate component for Home Assistant."""
2
- from __future__ import annotations
3
 
 
4
  from typing import Any
5
 
6
  from homeassistant.components.climate import (
@@ -12,94 +12,132 @@
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
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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):
 
60
  """Representation of a Plugwise thermostat."""
61
 
62
- _attr_has_entity_name = True
63
  _attr_name = None
64
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
65
  _attr_translation_key = DOMAIN
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def __init__(
68
  self,
69
  coordinator: PlugwiseDataUpdateCoordinator,
70
  device_id: str,
71
- homekit_enabled: bool, # pw-beta homekit emulation
72
  ) -> None:
73
  """Set up the Plugwise API."""
74
  super().__init__(coordinator, device_id)
75
- self._homekit_enabled = homekit_enabled # pw-beta homekit emulation
76
- self._homekit_mode: str | None = None # pw-beta homekit emulation
77
  self._attr_unique_id = f"{device_id}-climate"
78
 
 
 
 
 
 
 
79
  # Determine supported features
80
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
81
- if self.coordinator.data.gateway["cooling_present"]:
 
 
 
82
  self._attr_supported_features = (
83
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
84
  )
 
 
 
 
85
  if presets := self.device.get("preset_modes"):
86
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
87
  self._attr_preset_modes = presets
88
 
89
- # Determine hvac modes and current hvac mode
90
- self._attr_hvac_modes = [HVACMode.HEAT]
91
- if self.coordinator.data.gateway["cooling_present"]:
92
- self._attr_hvac_modes = [HVACMode.HEAT_COOL]
93
- if self.device["available_schedules"] != ["None"]:
94
- self._attr_hvac_modes.append(HVACMode.AUTO)
95
- if self._homekit_enabled: # pw-beta homekit emulation
96
- self._attr_hvac_modes.append(HVACMode.OFF) # pragma: no cover
97
-
98
  self._attr_min_temp = self.device["thermostat"]["lower_bound"]
99
- self._attr_max_temp = self.device["thermostat"]["upper_bound"]
100
- # Fix unpractical resolution provided by Plugwise
101
  self._attr_target_temperature_step = max(
102
- self.device["thermostat"]["resolution"], 0.5
103
  )
104
 
105
  @property
@@ -108,6 +146,14 @@
108
  return self.device["sensors"]["temperature"]
109
 
110
  @property
 
 
 
 
 
 
 
 
111
  def target_temperature(self) -> float:
112
  """Return the temperature we try to reach.
113
 
@@ -134,44 +180,58 @@
134
 
135
  @property
136
  def hvac_mode(self) -> HVACMode:
137
- """Return HVAC operation ie. auto, heat, heat_cool, or off mode."""
138
  if (
139
- mode := self.device["mode"]
140
- ) is None or mode not in self.hvac_modes: # pw-beta add to Core
141
- return HVACMode.HEAT # pragma: no cover
142
- # pw-beta homekit emulation
143
- if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
144
- mode = HVACMode.OFF # pragma: no cover
145
-
146
  return HVACMode(mode)
147
 
148
  @property
149
- def hvac_action(self) -> HVACAction: # pw-beta add to Core
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  """Return the current running hvac operation if supported."""
151
- # When control_state is present, prefer this data
152
- if (control_state := self.device.get("control_state")) == "cooling":
153
- return HVACAction.COOLING
154
- # Support preheating state as heating,
155
- # until preheating is added as a separate state
156
- if control_state in ["heating", "preheating"]:
157
- return HVACAction.HEATING
158
- if control_state == "off":
159
- return HVACAction.IDLE
160
-
161
- heater: str | None = self.coordinator.data.gateway["heater_id"]
162
- if heater:
163
- heater_data = self.coordinator.data.devices[heater]
164
- if heater_data["binary_sensors"]["heating_state"]:
165
- return HVACAction.HEATING
166
- if heater_data["binary_sensors"].get("cooling_state", False):
167
- return HVACAction.COOLING
168
 
169
  return HVACAction.IDLE
170
 
171
  @property
172
  def preset_mode(self) -> str | None:
173
  """Return the current preset mode."""
174
- return self.device["active_preset"]
175
 
176
  @plugwise_command
177
  async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -184,41 +244,82 @@
184
  if ATTR_TARGET_TEMP_LOW in kwargs:
185
  data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
186
 
187
- for temperature in data.values():
188
- if temperature is None or not (
189
- self._attr_min_temp <= temperature <= self._attr_max_temp
190
- ):
191
- raise ValueError("Invalid temperature change requested")
192
-
193
  if mode := kwargs.get(ATTR_HVAC_MODE):
194
  await self.async_set_hvac_mode(mode)
195
 
196
- await self.coordinator.api.set_temperature(self.device["location"], data)
 
 
 
 
 
 
 
 
197
 
198
  @plugwise_command
199
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
200
- """Set the hvac mode."""
201
- if hvac_mode not in self.hvac_modes:
202
- raise HomeAssistantError("Unsupported hvac_mode")
203
-
204
- await self.coordinator.api.set_schedule_state(
205
- self.device["location"],
206
- self.device["last_used"],
207
- "on" if hvac_mode == HVACMode.AUTO else "off",
208
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
- # pw-beta: feature request - mimic HomeKit behavior
211
- self._homekit_mode = hvac_mode
212
- if self._homekit_enabled:
213
- if self._homekit_mode == HVACMode.OFF: # pragma: no cover
214
- await self.async_set_preset_mode(PRESET_AWAY) # pragma: no cover
215
  if (
216
- self._homekit_mode in [HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
217
- and self.device["active_preset"] == PRESET_AWAY
218
- ): # pragma: no cover
219
- await self.async_set_preset_mode(PRESET_HOME) # pragma: no cover
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  @plugwise_command
222
  async def async_set_preset_mode(self, preset_mode: str) -> None:
223
  """Set the preset mode."""
224
- await self.coordinator.api.set_preset(self.device["location"], preset_mode)
 
1
  """Plugwise Climate component for Home Assistant."""
 
2
 
3
+ from dataclasses import asdict, dataclass
4
  from typing import Any
5
 
6
  from homeassistant.components.climate import (
 
12
  HVACAction,
13
  HVACMode,
14
  )
15
+ from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
16
+ from homeassistant.core import HomeAssistant, callback
 
 
 
 
 
17
  from homeassistant.exceptions import HomeAssistantError
18
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19
+ from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
20
 
21
+ from .const import DOMAIN, MASTER_THERMOSTATS
22
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
23
  from .entity import PlugwiseEntity
24
  from .util import plugwise_command
25
 
26
+ ERROR_NO_SCHEDULE = "set_schedule_first"
27
+ PARALLEL_UPDATES = 0
28
+
29
+
30
+ @dataclass
31
+ class PlugwiseClimateExtraStoredData(ExtraStoredData):
32
+ """Object to hold extra stored data."""
33
+
34
+ last_active_schedule: str | None
35
+ previous_action_mode: str | None
36
+
37
+ def as_dict(self) -> dict[str, Any]:
38
+ """Return a dict representation of the text data."""
39
+ return asdict(self)
40
+
41
+ @classmethod
42
+ def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
43
+ """Initialize a stored data object from a dict."""
44
+ return cls(
45
+ last_active_schedule=restored.get("last_active_schedule"),
46
+ previous_action_mode=restored.get("previous_action_mode"),
47
+ )
48
+
49
 
50
  async def async_setup_entry(
51
  hass: HomeAssistant,
52
+ entry: PlugwiseConfigEntry,
53
+ async_add_entities: AddConfigEntryEntitiesCallback,
54
  ) -> None:
55
  """Set up the Smile Thermostats from a config entry."""
56
+ coordinator = entry.runtime_data
57
+
58
+ @callback
59
+ def _add_entities() -> None:
60
+ """Add Entities."""
61
+ if not coordinator.new_devices:
62
+ return
63
+
64
+ if coordinator.api.smile.name == "Adam":
65
+ async_add_entities(
66
+ PlugwiseClimateEntity(coordinator, device_id)
67
+ for device_id in coordinator.new_devices
68
+ if coordinator.data[device_id]["dev_class"] == "climate"
69
+ )
70
+ else:
71
+ async_add_entities(
72
+ PlugwiseClimateEntity(coordinator, device_id)
73
+ for device_id in coordinator.new_devices
74
+ if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
75
+ )
76
 
77
+ _add_entities()
78
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
79
 
80
+
81
+ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
82
  """Representation of a Plugwise thermostat."""
83
 
 
84
  _attr_name = None
85
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
86
  _attr_translation_key = DOMAIN
87
 
88
+ _last_active_schedule: str | None = None
89
+ _previous_action_mode: str | None = HVACAction.HEATING.value
90
+
91
+ async def async_added_to_hass(self) -> None:
92
+ """Run when entity about to be added."""
93
+ await super().async_added_to_hass()
94
+
95
+ if extra_data := await self.async_get_last_extra_data():
96
+ plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
97
+ extra_data.as_dict()
98
+ )
99
+ self._last_active_schedule = plugwise_extra_data.last_active_schedule
100
+ self._previous_action_mode = (
101
+ plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
102
+ )
103
+
104
  def __init__(
105
  self,
106
  coordinator: PlugwiseDataUpdateCoordinator,
107
  device_id: str,
 
108
  ) -> None:
109
  """Set up the Plugwise API."""
110
  super().__init__(coordinator, device_id)
 
 
111
  self._attr_unique_id = f"{device_id}-climate"
112
 
113
+ gateway_id: str = coordinator.api.gateway_id
114
+ self._gateway_data = coordinator.data[gateway_id]
115
+ self._location = device_id
116
+ if (location := self.device.get("location")) is not None:
117
+ self._location = location
118
+
119
  # Determine supported features
120
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
121
+ if (
122
+ self.coordinator.api.cooling_present
123
+ and coordinator.api.smile.name != "Adam"
124
+ ):
125
  self._attr_supported_features = (
126
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
127
  )
128
+ if HVACMode.OFF in self.hvac_modes:
129
+ self._attr_supported_features |= (
130
+ ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
131
+ )
132
  if presets := self.device.get("preset_modes"):
133
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
134
  self._attr_preset_modes = presets
135
 
 
 
 
 
 
 
 
 
 
136
  self._attr_min_temp = self.device["thermostat"]["lower_bound"]
137
+ self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
138
+ # Ensure we don't drop below 0.1
139
  self._attr_target_temperature_step = max(
140
+ self.device["thermostat"]["resolution"], 0.1
141
  )
142
 
143
  @property
 
146
  return self.device["sensors"]["temperature"]
147
 
148
  @property
149
+ def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
150
+ """Return text specific state data to be restored."""
151
+ return PlugwiseClimateExtraStoredData(
152
+ last_active_schedule=self._last_active_schedule,
153
+ previous_action_mode=self._previous_action_mode,
154
+ )
155
+
156
+ @property
157
  def target_temperature(self) -> float:
158
  """Return the temperature we try to reach.
159
 
 
180
 
181
  @property
182
  def hvac_mode(self) -> HVACMode:
183
+ """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
184
  if (
185
+ mode := self.device.get("climate_mode")
186
+ ) is None or mode not in self.hvac_modes:
187
+ return HVACMode.HEAT
 
 
 
 
188
  return HVACMode(mode)
189
 
190
  @property
191
+ def hvac_modes(self) -> list[HVACMode]:
192
+ """Return a list of available HVACModes."""
193
+ hvac_modes: list[HVACMode] = []
194
+ if "regulation_modes" in self._gateway_data:
195
+ hvac_modes.append(HVACMode.OFF)
196
+
197
+ if self.device.get("available_schedules"):
198
+ hvac_modes.append(HVACMode.AUTO)
199
+
200
+ if self.coordinator.api.cooling_present:
201
+ if "regulation_modes" in self._gateway_data:
202
+ if "heating" in self._gateway_data["regulation_modes"]:
203
+ hvac_modes.append(HVACMode.HEAT)
204
+ if "cooling" in self._gateway_data["regulation_modes"]:
205
+ hvac_modes.append(HVACMode.COOL)
206
+ else:
207
+ hvac_modes.append(HVACMode.HEAT_COOL)
208
+ else:
209
+ hvac_modes.append(HVACMode.HEAT)
210
+
211
+ return hvac_modes
212
+
213
+ @property
214
+ def hvac_action(self) -> HVACAction:
215
  """Return the current running hvac operation if supported."""
216
+ # Keep track of the previous hvac_action mode.
217
+ # When no cooling available, _previous_action_mode is always heating
218
+ if (
219
+ "regulation_modes" in self._gateway_data
220
+ and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
221
+ ):
222
+ mode = self._gateway_data["select_regulation_mode"]
223
+ if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
224
+ self._previous_action_mode = mode
225
+
226
+ if (action := self.device.get("control_state")) is not None:
227
+ return HVACAction(action)
 
 
 
 
 
228
 
229
  return HVACAction.IDLE
230
 
231
  @property
232
  def preset_mode(self) -> str | None:
233
  """Return the current preset mode."""
234
+ return self.device.get("active_preset")
235
 
236
  @plugwise_command
237
  async def async_set_temperature(self, **kwargs: Any) -> None:
 
244
  if ATTR_TARGET_TEMP_LOW in kwargs:
245
  data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
246
 
 
 
 
 
 
 
247
  if mode := kwargs.get(ATTR_HVAC_MODE):
248
  await self.async_set_hvac_mode(mode)
249
 
250
+ await self.coordinator.api.set_temperature(self._location, data)
251
+
252
+ def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str | None:
253
+ """Return the API regulation value for a manual HVAC mode, or None."""
254
+ if hvac_mode == HVACMode.HEAT:
255
+ return HVACAction.HEATING.value
256
+ if hvac_mode == HVACMode.COOL:
257
+ return HVACAction.COOLING.value
258
+ return None
259
 
260
  @plugwise_command
261
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
262
+ """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
263
+ if hvac_mode == self.hvac_mode:
264
+ return
265
+
266
+ api = self.coordinator.api
267
+ current_schedule = self.device.get("select_schedule")
268
+
269
+ # OFF: single API call
270
+ if hvac_mode == HVACMode.OFF:
271
+ await api.set_regulation_mode(hvac_mode.value)
272
+ return
273
+
274
+ # Manual mode (heat/cool/heat_cool) without a schedule: set regulation only
275
+ if (
276
+ current_schedule is None
277
+ and hvac_mode != HVACMode.AUTO
278
+ and (
279
+ regulation := self._regulation_mode_for_hvac(hvac_mode)
280
+ or self._previous_action_mode
281
+ )
282
+ ):
283
+ await api.set_regulation_mode(regulation)
284
+ return
285
+
286
+ # Manual mode: ensure regulation and turn off schedule when needed
287
+ if hvac_mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL):
288
+ regulation = self._regulation_mode_for_hvac(hvac_mode) or (
289
+ self._previous_action_mode
290
+ if self.hvac_mode in (HVACMode.HEAT_COOL, HVACMode.OFF)
291
+ else None
292
+ )
293
+ if regulation:
294
+ await api.set_regulation_mode(regulation)
295
 
 
 
 
 
 
296
  if (
297
+ self.hvac_mode == HVACMode.OFF and current_schedule not in (None, "off")
298
+ ) or (self.hvac_mode == HVACMode.AUTO and current_schedule is not None):
299
+ await api.set_schedule_state(
300
+ self._location, STATE_OFF, current_schedule
301
+ )
302
+ return
303
+
304
+ # AUTO: restore schedule and regulation
305
+ desired_schedule = current_schedule
306
+ if desired_schedule and desired_schedule != "off":
307
+ self._last_active_schedule = desired_schedule
308
+ elif desired_schedule == "off":
309
+ desired_schedule = self._last_active_schedule
310
+
311
+ if not desired_schedule:
312
+ raise HomeAssistantError(
313
+ translation_domain=DOMAIN,
314
+ translation_key=ERROR_NO_SCHEDULE,
315
+ )
316
+
317
+ if self._previous_action_mode:
318
+ if self.hvac_mode == HVACMode.OFF:
319
+ await api.set_regulation_mode(self._previous_action_mode)
320
+ await api.set_schedule_state(self._location, STATE_ON, desired_schedule)
321
 
322
  @plugwise_command
323
  async def async_set_preset_mode(self, preset_mode: str) -> None:
324
  """Set the preset mode."""
325
+ await self.coordinator.api.set_preset(self._location, preset_mode)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py RENAMED
@@ -1,8 +1,7 @@
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,75 +14,66 @@
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,
44
  FLOW_STRETCH,
45
  SMILE,
 
 
46
  STRETCH,
47
  STRETCH_USERNAME,
 
48
  ZEROCONF_MAP,
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,24 +81,50 @@
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()
98
  return api
99
 
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
102
  """Handle a config flow for Plugwise Smile."""
103
 
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 +141,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,167 +153,106 @@
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
 
144
  # This is an Anna, but we already have config entries.
145
  # Assuming that the user has already configured Adam, aborting discovery.
146
- if self._async_current_entries() and _product == "smile_thermo":
147
- return self.async_abort(reason="anna_with_adam")
148
 
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
+ import logging
4
+ from typing import Any, Self
5
 
6
  from plugwise import Smile
7
  from plugwise.exceptions import (
 
14
  )
15
  import voluptuous as vol
16
 
17
+ from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
18
  from homeassistant.const import (
19
+ ATTR_CONFIGURATION_URL,
20
  CONF_BASE,
21
  CONF_HOST,
22
  CONF_NAME,
23
  CONF_PASSWORD,
24
  CONF_PORT,
 
25
  CONF_USERNAME,
26
  )
27
+ from homeassistant.core import HomeAssistant
 
 
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
+ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
30
 
31
  from .const import (
32
+ ANNA_WITH_ADAM,
 
 
33
  DEFAULT_PORT,
 
34
  DEFAULT_USERNAME,
35
  DOMAIN,
36
  FLOW_SMILE,
37
  FLOW_STRETCH,
38
  SMILE,
39
+ SMILE_OPEN_THERM,
40
+ SMILE_THERMO,
41
  STRETCH,
42
  STRETCH_USERNAME,
43
+ UNKNOWN_SMILE,
44
  ZEROCONF_MAP,
45
  )
46
 
47
+ _LOGGER = logging.getLogger(__name__)
48
 
49
+ SMILE_RECONF_SCHEMA = vol.Schema(
50
+ {
51
+ vol.Required(CONF_HOST): str,
52
+ }
53
+ )
54
+
55
+
56
+ def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
57
  """Generate base schema for gateways."""
58
+ schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
59
+
60
  if not discovery_info:
61
+ schema = schema.extend(
 
 
 
 
 
 
 
 
 
 
 
62
  {
63
+ vol.Required(CONF_HOST): str,
64
+ vol.Required(CONF_USERNAME, default=SMILE): vol.In(
 
 
65
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
  ),
67
  }
68
  )
69
 
70
+ return schema
71
 
72
 
73
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
74
  """Validate whether the user input allows us to connect to the gateway.
75
 
76
+ Data has the keys from the schema with values provided by the user.
77
  """
78
  websession = async_get_clientsession(hass, verify_ssl=False)
79
  api = Smile(
 
81
  password=data[CONF_PASSWORD],
82
  port=data[CONF_PORT],
83
  username=data[CONF_USERNAME],
 
84
  websession=websession,
85
  )
86
  await api.connect()
87
  return api
88
 
89
 
90
+ async def verify_connection(
91
+ hass: HomeAssistant, user_input: dict[str, Any]
92
+ ) -> tuple[Smile | None, dict[str, str]]:
93
+ """Verify and return the gateway connection or an error."""
94
+ errors: dict[str, str] = {}
95
+
96
+ try:
97
+ return (await validate_input(hass, user_input), errors)
98
+ except ConnectionFailedError:
99
+ errors[CONF_BASE] = "cannot_connect"
100
+ except InvalidAuthentication:
101
+ errors[CONF_BASE] = "invalid_auth"
102
+ except InvalidSetupError:
103
+ errors[CONF_BASE] = "invalid_setup"
104
+ except InvalidXMLError, ResponseError:
105
+ errors[CONF_BASE] = "response_error"
106
+ except UnsupportedDeviceError:
107
+ errors[CONF_BASE] = "unsupported"
108
+ except Exception:
109
+ _LOGGER.exception(
110
+ "Unknown exception while verifying connection with your Plugwise Smile"
111
+ )
112
+ errors[CONF_BASE] = "unknown"
113
+ return (None, errors)
114
+
115
+
116
  class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
+ product: str = UNKNOWN_SMILE
123
  _username: str = DEFAULT_USERNAME
124
 
125
  async def async_step_zeroconf(
126
  self, discovery_info: ZeroconfServiceInfo
127
+ ) -> ConfigFlowResult:
128
  """Prepare configuration for a discovered Plugwise Smile."""
129
  self.discovery_info = discovery_info
130
  _properties = discovery_info.properties
 
141
  CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
142
  },
143
  )
144
+ except Exception: # noqa: BLE001
145
  self._abort_if_unique_id_configured()
146
  else:
147
  self._abort_if_unique_id_configured(
 
153
 
154
  if DEFAULT_USERNAME not in unique_id:
155
  self._username = STRETCH_USERNAME
156
+ self.product = _product = _properties.get("product", UNKNOWN_SMILE)
157
  _version = _properties.get("version", "n/a")
158
  _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
159
 
160
  # This is an Anna, but we already have config entries.
161
  # Assuming that the user has already configured Adam, aborting discovery.
162
+ if self._async_current_entries() and _product == SMILE_THERMO:
163
+ return self.async_abort(reason=ANNA_WITH_ADAM)
164
 
165
  # If we have discovered an Adam or Anna, both might be on the network.
166
  # In that case, we need to cancel the Anna flow, as the Adam should
167
  # be added.
168
+ if self.hass.config_entries.flow.async_has_matching_flow(self):
169
+ return self.async_abort(reason=ANNA_WITH_ADAM)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  self.context.update(
172
  {
173
+ "title_placeholders": {CONF_NAME: _name},
174
+ ATTR_CONFIGURATION_URL: (
 
 
 
 
 
175
  f"http://{discovery_info.host}:{discovery_info.port}"
176
  ),
 
177
  }
178
  )
179
  return await self.async_step_user()
180
 
181
+ def is_matching(self, other_flow: Self) -> bool:
182
+ """Return True if other_flow is matching this flow."""
183
+ # This is an Anna, and there is already an Adam flow in progress
184
+ if self.product == SMILE_THERMO and other_flow.product == SMILE_OPEN_THERM:
185
+ return True
186
+
187
+ # This is an Adam, and there is already an Anna flow in progress
188
+ if self.product == SMILE_OPEN_THERM and other_flow.product == SMILE_THERMO:
189
+ self.hass.config_entries.flow.async_abort(other_flow.flow_id)
190
+
191
+ return False
192
+
193
  async def async_step_user(
194
  self, user_input: dict[str, Any] | None = None
195
+ ) -> ConfigFlowResult:
196
  """Handle the initial step when using network/gateway setups."""
197
  errors: dict[str, str] = {}
198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  if user_input is not None:
200
+ user_input[CONF_PORT] = DEFAULT_PORT
201
+ if self.discovery_info:
202
+ user_input[CONF_HOST] = self.discovery_info.host
203
+ user_input[CONF_PORT] = self.discovery_info.port
204
+ user_input[CONF_USERNAME] = self._username
205
+
206
+ api, errors = await verify_connection(self.hass, user_input)
207
+ if api:
208
+ await self.async_set_unique_id(
209
+ api.smile.hostname or api.gateway_id,
210
+ raise_on_progress=False,
211
+ )
212
+ self._abort_if_unique_id_configured()
213
+ return self.async_create_entry(title=api.smile.name, data=user_input)
214
 
215
+ return self.async_show_form(
216
+ step_id=SOURCE_USER,
217
+ data_schema=smile_user_schema(self.discovery_info),
218
+ errors=errors,
219
+ )
220
 
221
+ async def async_step_reconfigure(
222
  self, user_input: dict[str, Any] | None = None
223
+ ) -> ConfigFlowResult:
224
+ """Handle reconfiguration of the integration."""
225
+ errors: dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ reconfigure_entry = self._get_reconfigure_entry()
 
228
 
229
+ if user_input:
230
+ # Keep current username and password
231
+ full_input = {
232
+ CONF_HOST: user_input.get(CONF_HOST),
233
+ CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
234
+ CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
235
+ CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
 
 
 
 
 
236
  }
 
237
 
238
+ api, errors = await verify_connection(self.hass, full_input)
239
+ if api:
240
+ await self.async_set_unique_id(
241
+ api.smile.hostname or api.gateway_id,
242
+ raise_on_progress=False,
243
+ )
244
+ self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
245
+ return self.async_update_reload_and_abort(
246
+ reconfigure_entry,
247
+ data_updates=full_input,
248
+ )
249
+
250
+ return self.async_show_form(
251
+ step_id="reconfigure",
252
+ data_schema=self.add_suggested_values_to_schema(
253
+ data_schema=SMILE_RECONF_SCHEMA,
254
+ suggested_values=reconfigure_entry.data,
255
+ ),
256
+ description_placeholders={"title": reconfigure_entry.title},
257
+ errors=errors,
258
+ )
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py RENAMED
@@ -1,7 +1,8 @@
1
  """Constants for Plugwise component."""
 
2
  from datetime import timedelta
3
  import logging
4
- from typing import Final
5
 
6
  from homeassistant.const import Platform
7
 
@@ -9,56 +10,79 @@
9
 
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
 
21
- FLOW_NET: Final = "Network: Smile/Stretch"
22
- FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
23
- FLOW_STRETCH: Final = "Stretch (Stretch)"
24
- FLOW_TYPE: Final = "flow_type"
25
-
26
- UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
27
-
28
- # Default directives
29
- DEFAULT_PORT: Final = 80
30
- DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = {
31
- "power": timedelta(seconds=10),
32
- "stretch": timedelta(seconds=60),
33
- "thermostat": timedelta(seconds=60),
34
- }
35
- DEFAULT_TIMEOUT: Final = 10
36
- DEFAULT_USERNAME: Final = "smile"
37
-
38
- # --- Const for Plugwise Smile and Stretch
39
  PLATFORMS: Final[list[str]] = [
40
  Platform.BINARY_SENSOR,
 
41
  Platform.CLIMATE,
42
  Platform.NUMBER,
43
  Platform.SELECT,
44
  Platform.SENSOR,
45
  Platform.SWITCH,
46
  ]
47
- SERVICE_DELETE: Final = "delete_notification"
48
- SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- # Climate const:
51
  MASTER_THERMOSTATS: Final[list[str]] = [
52
  "thermostat",
 
53
  "zone_thermometer",
54
  "zone_thermostat",
55
- "thermostatic_radiator_valve",
56
  ]
57
 
58
- # Config_flow const:
59
- ZEROCONF_MAP: Final[dict[str, str]] = {
60
- "smile": "Smile P1",
61
- "smile_thermo": "Smile Anna",
62
- "smile_open_therm": "Adam",
63
- "stretch": "Stretch",
64
- }
 
1
  """Constants for Plugwise component."""
2
+
3
  from datetime import timedelta
4
  import logging
5
+ from typing import Final, Literal
6
 
7
  from homeassistant.const import Platform
8
 
 
10
 
11
  LOGGER = logging.getLogger(__package__)
12
 
13
+ ANNA_WITH_ADAM: Final = "anna_with_adam"
14
  API: Final = "api"
15
+ AVAILABLE: Final = "available"
16
+ DEV_CLASS: Final = "dev_class"
17
+ FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
18
+ FLOW_STRETCH: Final = "stretch (Stretch)"
19
+ FLOW_TYPE: Final = "flow_type"
20
+ GATEWAY: Final = "gateway"
21
+ LOCATION: Final = "location"
22
+ PW_TYPE: Final = "plugwise_type"
23
+ REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
+ SMILE_OPEN_THERM: Final = "smile_open_therm"
26
+ SMILE_THERMO: Final = "smile_thermo"
27
  STRETCH: Final = "stretch"
28
  STRETCH_USERNAME: Final = "stretch"
29
+ UNKNOWN_SMILE: Final = "Unknown Smile"
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  PLATFORMS: Final[list[str]] = [
32
  Platform.BINARY_SENSOR,
33
+ Platform.BUTTON,
34
  Platform.CLIMATE,
35
  Platform.NUMBER,
36
  Platform.SELECT,
37
  Platform.SENSOR,
38
  Platform.SWITCH,
39
  ]
40
+ ZEROCONF_MAP: Final[dict[str, str]] = {
41
+ "smile": "Smile P1",
42
+ "smile_thermo": "Smile Anna",
43
+ "smile_open_therm": "Adam",
44
+ "stretch": "Stretch",
45
+ }
46
+
47
+ type NumberType = Literal[
48
+ "maximum_boiler_temperature",
49
+ "max_dhw_temperature",
50
+ "temperature_offset",
51
+ ]
52
+
53
+ type SelectType = Literal[
54
+ "select_dhw_mode",
55
+ "select_gateway_mode",
56
+ "select_regulation_mode",
57
+ "select_schedule",
58
+ "select_zone_profile",
59
+ ]
60
+ type SelectOptionsType = Literal[
61
+ "available_schedules",
62
+ "dhw_modes",
63
+ "gateway_modes",
64
+ "regulation_modes",
65
+ "zone_profiles",
66
+ ]
67
+
68
+ # Default directives
69
+ DEFAULT_MAX_TEMP: Final = 30
70
+ DEFAULT_MIN_TEMP: Final = 4
71
+ DEFAULT_PORT: Final = 80
72
+ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
73
+ DEFAULT_USERNAME: Final = "smile"
74
+ P1_UPDATE_INTERVAL = timedelta(seconds=10)
75
 
 
76
  MASTER_THERMOSTATS: Final[list[str]] = [
77
  "thermostat",
78
+ "thermostatic_radiator_valve",
79
  "zone_thermometer",
80
  "zone_thermostat",
 
81
  ]
82
 
83
+ # Select constants
84
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
85
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
86
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
87
+ SELECT_SCHEDULE: Final = "select_schedule"
88
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py RENAMED
@@ -1,116 +1,205 @@
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.aiohttp_client import async_get_clientsession
24
  from homeassistant.helpers.debounce import Debouncer
25
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
26
 
27
- from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_USERNAME, DOMAIN, LOGGER
 
 
 
 
 
 
 
 
 
28
 
29
 
30
- class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
31
  """Class to manage fetching Plugwise data from single endpoint."""
32
 
33
- _connected: bool = False
34
 
35
- def __init__(
36
- self,
37
- hass: HomeAssistant,
38
- entry: ConfigEntry,
39
- cooldown: float,
40
- update_interval: timedelta = timedelta(seconds=60),
41
- ) -> None: # pw-beta cooldown
42
  """Initialize the coordinator."""
43
  super().__init__(
44
  hass,
45
  LOGGER,
 
46
  name=DOMAIN,
47
- # Core directly updates from const's DEFAULT_SCAN_INTERVAL
48
- update_interval=update_interval,
49
  # Don't refresh immediately, give the device time to process
50
  # the change in state before we query it.
51
  request_refresh_debouncer=Debouncer(
52
  hass,
53
  LOGGER,
54
- cooldown=cooldown,
55
  immediate=False,
56
  ),
57
  )
58
 
59
  self.api = Smile(
60
- host=entry.data[CONF_HOST],
61
- username=entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
62
- password=entry.data[CONF_PASSWORD],
63
- port=entry.data.get(CONF_PORT, DEFAULT_PORT),
64
- timeout=30,
65
  websession=async_get_clientsession(hass, verify_ssl=False),
66
  )
67
- self._entry = entry
68
- self._unavailable_logged = False
69
- self.update_interval = update_interval
 
 
70
 
71
  async def _connect(self) -> None:
72
- """Connect to the Plugwise Smile."""
73
- self._connected = await self.api.connect()
74
- self.api.get_all_devices()
75
-
76
- self.update_interval = DEFAULT_SCAN_INTERVAL.get(
77
- self.api.smile_type, timedelta(seconds=60)
78
- ) # pw-beta options scan-interval
79
- if (custom_time := self._entry.options.get(CONF_SCAN_INTERVAL)) is not None:
80
- self.update_interval = timedelta(
81
- seconds=int(custom_time)
82
- ) # pragma: no cover # pw-beta options
83
 
84
- LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- async def _async_update_data(self) -> PlugwiseData:
87
  """Fetch data from Plugwise."""
88
- data = PlugwiseData(gateway={}, devices={})
89
-
90
  try:
91
  if not self._connected:
92
  await self._connect()
93
  data = await self.api.async_update()
94
- LOGGER.debug(f"{self.api.smile_name} data: %s", data)
95
- if self._unavailable_logged:
96
- self._unavailable_logged = False
 
 
97
  except InvalidAuthentication as err:
98
- if not self._unavailable_logged: # pw-beta add to Core
99
- self._unavailable_logged = True
100
- raise ConfigEntryError("Authentication failed") from err
 
 
 
 
 
 
101
  except (InvalidXMLError, ResponseError) as err:
102
- if not self._unavailable_logged: # pw-beta add to Core
103
- self._unavailable_logged = True
104
- raise UpdateFailed(
105
- "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
106
- ) from err
 
 
 
 
107
  except UnsupportedDeviceError as err:
108
- if not self._unavailable_logged: # pw-beta add to Core
109
- self._unavailable_logged = True
110
- raise ConfigEntryError("Device with unsupported firmware") from err
111
- except ConnectionFailedError as err:
112
- if not self._unavailable_logged: # pw-beta add to Core
113
- self._unavailable_logged = True
114
- raise UpdateFailed("Failed to connect") from err
115
 
 
 
116
  return data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """DataUpdateCoordinator for Plugwise."""
 
2
 
3
+ from packaging.version import Version
4
+ from plugwise import GwEntityData, Smile
5
  from plugwise.exceptions import (
6
  ConnectionFailedError,
7
  InvalidAuthentication,
8
+ InvalidSetupError,
9
  InvalidXMLError,
10
+ PlugwiseError,
11
  ResponseError,
12
  UnsupportedDeviceError,
13
  )
14
 
15
  from homeassistant.config_entries import ConfigEntry
16
+ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
17
  from homeassistant.core import HomeAssistant
18
  from homeassistant.exceptions import ConfigEntryError
19
+ from homeassistant.helpers import device_registry as dr
20
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
21
  from homeassistant.helpers.debounce import Debouncer
22
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
23
 
24
+ from .const import (
25
+ DEFAULT_PORT,
26
+ DEFAULT_UPDATE_INTERVAL,
27
+ DEFAULT_USERNAME,
28
+ DOMAIN,
29
+ LOGGER,
30
+ P1_UPDATE_INTERVAL,
31
+ )
32
+
33
+ type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
34
 
35
 
36
+ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData]]):
37
  """Class to manage fetching Plugwise data from single endpoint."""
38
 
39
+ config_entry: PlugwiseConfigEntry
40
 
41
+ def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
 
42
  """Initialize the coordinator."""
43
  super().__init__(
44
  hass,
45
  LOGGER,
46
+ config_entry=config_entry,
47
  name=DOMAIN,
48
+ update_interval=DEFAULT_UPDATE_INTERVAL,
 
49
  # Don't refresh immediately, give the device time to process
50
  # the change in state before we query it.
51
  request_refresh_debouncer=Debouncer(
52
  hass,
53
  LOGGER,
54
+ cooldown=1.5,
55
  immediate=False,
56
  ),
57
  )
58
 
59
  self.api = Smile(
60
+ host=self.config_entry.data[CONF_HOST],
61
+ username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
62
+ password=self.config_entry.data[CONF_PASSWORD],
63
+ port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
64
  websession=async_get_clientsession(hass, verify_ssl=False),
65
  )
66
+ self._connected: bool = False
67
+ self._current_devices: set[str] = set()
68
+ self._firmware_list: dict[str, str | None] = {}
69
+ self._stored_devices: set[str] = set()
70
+ self.new_devices: set[str] = set()
71
 
72
  async def _connect(self) -> None:
73
+ """Connect to the Plugwise Smile.
 
 
 
 
 
 
 
 
 
 
74
 
75
+ A Version object is received when the connection succeeds.
76
+ """
77
+ version = await self.api.connect()
78
+ self._connected = isinstance(version, Version)
79
+ if self._connected and self.api.smile.type == "power":
80
+ self.update_interval = P1_UPDATE_INTERVAL
81
+
82
+ async def _async_setup(self) -> None:
83
+ """Initialize the update_data process."""
84
+ device_reg = dr.async_get(self.hass)
85
+ device_entries = dr.async_entries_for_config_entry(
86
+ device_reg, self.config_entry.entry_id
87
+ )
88
+ self._stored_devices = {
89
+ identifier[1]
90
+ for device_entry in device_entries
91
+ for identifier in device_entry.identifiers
92
+ if identifier[0] == DOMAIN
93
+ }
94
 
95
+ async def _async_update_data(self) -> dict[str, GwEntityData]:
96
  """Fetch data from Plugwise."""
 
 
97
  try:
98
  if not self._connected:
99
  await self._connect()
100
  data = await self.api.async_update()
101
+ except ConnectionFailedError as err:
102
+ raise UpdateFailed(
103
+ translation_domain=DOMAIN,
104
+ translation_key="failed_to_connect",
105
+ ) from err
106
  except InvalidAuthentication as err:
107
+ raise ConfigEntryError(
108
+ translation_domain=DOMAIN,
109
+ translation_key="authentication_failed",
110
+ ) from err
111
+ except InvalidSetupError as err:
112
+ raise ConfigEntryError(
113
+ translation_domain=DOMAIN,
114
+ translation_key="invalid_setup",
115
+ ) from err
116
  except (InvalidXMLError, ResponseError) as err:
117
+ raise UpdateFailed(
118
+ translation_domain=DOMAIN,
119
+ translation_key="response_error",
120
+ ) from err
121
+ except PlugwiseError as err:
122
+ raise UpdateFailed(
123
+ translation_domain=DOMAIN,
124
+ translation_key="data_incomplete_or_missing",
125
+ ) from err
126
  except UnsupportedDeviceError as err:
127
+ raise ConfigEntryError(
128
+ translation_domain=DOMAIN,
129
+ translation_key="unsupported_firmware",
130
+ ) from err
 
 
 
131
 
132
+ self._add_remove_devices(data)
133
+ self._update_device_firmware(data)
134
  return data
135
+
136
+ def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
137
+ """Add new Plugwise devices, remove non-existing devices."""
138
+ set_of_data = set(data)
139
+ # Check for new or removed devices,
140
+ # 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
141
+ # this is required for the proper initialization of all the present platform entities.
142
+ self.new_devices = set_of_data - self._current_devices
143
+ for device_id in self.new_devices:
144
+ self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
145
+
146
+ current_devices = (
147
+ self._stored_devices if not self._current_devices else self._current_devices
148
+ )
149
+ self._current_devices = set_of_data
150
+ if removed_devices := (current_devices - set_of_data): # device(s) to remove
151
+ self._remove_devices(removed_devices)
152
+
153
+ def _remove_devices(self, removed_devices: set[str]) -> None:
154
+ """Clean registries when removed devices found."""
155
+ device_reg = dr.async_get(self.hass)
156
+ for device_id in removed_devices:
157
+ if (
158
+ device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
159
+ ) is not None:
160
+ device_reg.async_update_device(
161
+ device_entry.id, remove_config_entry_id=self.config_entry.entry_id
162
+ )
163
+ LOGGER.debug(
164
+ "%s %s %s removed from device_registry",
165
+ DOMAIN,
166
+ device_entry.model,
167
+ device_id,
168
+ )
169
+
170
+ self._firmware_list.pop(device_id, None)
171
+
172
+ def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
173
+ """Detect firmware changes and update the device registry."""
174
+ for device_id, device in data.items():
175
+ # Only update firmware when the key is present and not None, to avoid
176
+ # wiping stored firmware on partial or transient updates.
177
+ if "firmware" not in device:
178
+ continue
179
+ new_firmware = device.get("firmware")
180
+ if new_firmware is None:
181
+ continue
182
+ if (
183
+ device_id in self._firmware_list
184
+ and new_firmware != self._firmware_list[device_id]
185
+ ):
186
+ updated = self._update_firmware_in_dr(device_id, new_firmware)
187
+ if updated:
188
+ self._firmware_list[device_id] = new_firmware
189
+
190
+ def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
191
+ """Update device sw_version in device_registry."""
192
+ device_reg = dr.async_get(self.hass)
193
+ if (
194
+ device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
195
+ ) is not None:
196
+ device_reg.async_update_device(device_entry.id, sw_version=firmware)
197
+ LOGGER.debug(
198
+ "Firmware in device_registry updated for %s %s %s",
199
+ DOMAIN,
200
+ device_entry.model,
201
+ device_id,
202
+ )
203
+ return True
204
+
205
+ return False # pragma: no cover
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/diagnostics.py RENAMED
@@ -1,26 +1,15 @@
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,
26
- }
 
1
  """Diagnostics support for Plugwise."""
 
2
 
3
  from typing import Any
4
 
 
5
  from homeassistant.core import HomeAssistant
6
 
7
+ from .coordinator import PlugwiseConfigEntry
 
 
 
 
8
 
9
 
10
  async def async_get_config_entry_diagnostics(
11
+ hass: HomeAssistant, entry: PlugwiseConfigEntry
12
  ) -> dict[str, Any]:
13
  """Return diagnostics for a config entry."""
14
+ coordinator = entry.runtime_data
15
+ return coordinator.data
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py RENAMED
@@ -1,17 +1,16 @@
1
  """Generic Plugwise Entity Class."""
2
- from __future__ import annotations
3
 
4
- from plugwise.constants import DeviceData
5
 
6
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
7
  from homeassistant.helpers.device_registry import (
8
  CONNECTION_NETWORK_MAC,
9
  CONNECTION_ZIGBEE,
 
10
  )
11
- from homeassistant.helpers.entity import DeviceInfo
12
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
13
 
14
- from .const import DOMAIN
15
  from .coordinator import PlugwiseDataUpdateCoordinator
16
 
17
 
@@ -29,36 +28,43 @@
29
  super().__init__(coordinator)
30
  self._dev_id = device_id
31
 
32
- configuration_url: str | None = None
33
- if entry := self.coordinator.config_entry:
34
- configuration_url = f"http://{entry.data[CONF_HOST]}"
 
 
 
 
 
 
 
35
 
36
- data = coordinator.data.devices[device_id]
37
  connections = set()
38
- if mac := data.get("mac_address"):
39
  connections.add((CONNECTION_NETWORK_MAC, mac))
40
- if mac := data.get("zigbee_mac_address"):
41
- connections.add((CONNECTION_ZIGBEE, mac))
42
 
 
43
  self._attr_device_info = DeviceInfo(
44
  configuration_url=configuration_url,
45
  identifiers={(DOMAIN, device_id)},
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"),
 
52
  )
53
 
54
- if device_id != coordinator.data.gateway["gateway_id"]:
 
55
  self._attr_device_info.update(
56
  {
57
- ATTR_NAME: data.get("name"),
58
- ATTR_VIA_DEVICE: (
59
- DOMAIN,
60
- str(self.coordinator.data.gateway["gateway_id"]),
61
- ),
62
  }
63
  )
64
 
@@ -66,19 +72,12 @@
66
  def available(self) -> bool:
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
  )
75
 
76
  @property
77
- def device(self) -> DeviceData:
78
  """Return data for this device."""
79
- return self.coordinator.data.devices[self._dev_id]
80
-
81
- async def async_added_to_hass(self) -> None:
82
- """Subscribe to updates."""
83
- self._handle_coordinator_update()
84
- await super().async_added_to_hass()
 
1
  """Generic Plugwise Entity Class."""
 
2
 
3
+ from plugwise import GwEntityData
4
 
5
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
6
  from homeassistant.helpers.device_registry import (
7
  CONNECTION_NETWORK_MAC,
8
  CONNECTION_ZIGBEE,
9
+ DeviceInfo,
10
  )
 
11
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
12
 
13
+ from .const import AVAILABLE, DOMAIN
14
  from .coordinator import PlugwiseDataUpdateCoordinator
15
 
16
 
 
28
  super().__init__(coordinator)
29
  self._dev_id = device_id
30
 
31
+ api = coordinator.api
32
+ gateway_id = api.gateway_id
33
+ entry = coordinator.config_entry
34
+
35
+ # Link configuration-URL for the gateway device
36
+ configuration_url = (
37
+ f"http://{entry.data[CONF_HOST]}"
38
+ if device_id == gateway_id and entry
39
+ else None
40
+ )
41
 
42
+ # Build connections set
43
  connections = set()
44
+ if mac := self.device.get("mac_address"):
45
  connections.add((CONNECTION_NETWORK_MAC, mac))
46
+ if zigbee_mac := self.device.get("zigbee_mac_address"):
47
+ connections.add((CONNECTION_ZIGBEE, zigbee_mac))
48
 
49
+ # Set base device info
50
  self._attr_device_info = DeviceInfo(
51
  configuration_url=configuration_url,
52
  identifiers={(DOMAIN, device_id)},
53
  connections=connections,
54
+ manufacturer=self.device.get("vendor"),
55
+ model=self.device.get("model"),
56
+ model_id=self.device.get("model_id"),
57
+ name=api.smile.name,
58
+ sw_version=self.device.get("firmware"),
59
+ hw_version=self.device.get("hardware"),
60
  )
61
 
62
+ # Add extra info if not the gateway device
63
+ if device_id != gateway_id:
64
  self._attr_device_info.update(
65
  {
66
+ ATTR_NAME: self.device.get(ATTR_NAME),
67
+ ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
 
 
 
68
  }
69
  )
70
 
 
72
  def available(self) -> bool:
73
  """Return if entity is available."""
74
  return (
75
+ self._dev_id in self.coordinator.data
76
+ and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
 
 
77
  and super().available
78
  )
79
 
80
  @property
81
+ def device(self) -> GwEntityData:
82
  """Return data for this device."""
83
+ return self.coordinator.data[self._dev_id]
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json RENAMED
@@ -1,13 +1,13 @@
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": ["https://test-files.pythonhosted.org/packages/e1/dc/a1eb174d6052189a701dc5286f561996e731de23aee746ac5a9f537663d8/plugwise-0.33.0a0.tar.gz#plugwise==0.33.0a0"],
12
- "version": "0.42.0a0"
 
13
  }
 
1
  {
2
  "domain": "plugwise",
3
+ "name": "Plugwise",
 
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
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
+ "quality_scale": "platinum",
11
+ "requirements": ["plugwise==1.11.3"],
12
+ "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py RENAMED
@@ -1,43 +1,27 @@
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 +31,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 +38,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 +45,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 +54,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,13 +90,12 @@
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}"
122
  self._attr_mode = NumberMode.BOX
123
  self._attr_native_max_value = self.device[description.key]["upper_bound"]
124
  self._attr_native_min_value = self.device[description.key]["lower_bound"]
 
 
 
125
 
126
  native_step = self.device[description.key]["resolution"]
127
  if description.key != "temperature_offset":
@@ -133,12 +107,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 dataclasses import dataclass
4
 
 
 
 
5
  from homeassistant.components.number import (
6
  NumberDeviceClass,
7
  NumberEntity,
8
  NumberEntityDescription,
9
  NumberMode,
10
  )
 
11
  from homeassistant.const import EntityCategory, UnitOfTemperature
12
+ from homeassistant.core import HomeAssistant, callback
13
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
14
 
15
+ from .const import NumberType
16
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
17
  from .entity import PlugwiseEntity
18
+ from .util import plugwise_command
19
 
20
+ PARALLEL_UPDATES = 0
21
 
 
 
 
 
 
22
 
23
+ @dataclass(frozen=True, kw_only=True)
24
+ class PlugwiseNumberEntityDescription(NumberEntityDescription):
 
 
 
25
  """Class describing Plugwise Number entities."""
26
 
27
  key: NumberType
 
31
  PlugwiseNumberEntityDescription(
32
  key="maximum_boiler_temperature",
33
  translation_key="maximum_boiler_temperature",
 
 
 
34
  device_class=NumberDeviceClass.TEMPERATURE,
35
  entity_category=EntityCategory.CONFIG,
36
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
38
  PlugwiseNumberEntityDescription(
39
  key="max_dhw_temperature",
40
  translation_key="max_dhw_temperature",
 
 
 
41
  device_class=NumberDeviceClass.TEMPERATURE,
42
  entity_category=EntityCategory.CONFIG,
43
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
45
  PlugwiseNumberEntityDescription(
46
  key="temperature_offset",
47
  translation_key="temperature_offset",
 
 
 
48
  device_class=NumberDeviceClass.TEMPERATURE,
49
  entity_category=EntityCategory.CONFIG,
50
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
54
 
55
  async def async_setup_entry(
56
  hass: HomeAssistant,
57
+ entry: PlugwiseConfigEntry,
58
+ async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
  """Set up Plugwise number platform."""
61
+ coordinator = entry.runtime_data
62
 
63
+ @callback
64
+ def _add_entities() -> None:
65
+ """Add Entities."""
66
+ if not coordinator.new_devices:
67
+ return
68
+
69
+ async_add_entities(
70
+ PlugwiseNumberEntity(coordinator, device_id, description)
71
+ for device_id in coordinator.new_devices
72
+ for description in NUMBER_TYPES
73
+ if description.key in coordinator.data[device_id]
74
+ )
 
 
75
 
76
+ _add_entities()
77
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
78
 
79
 
80
  class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity):
 
90
  ) -> None:
91
  """Initiate Plugwise Number."""
92
  super().__init__(coordinator, device_id)
 
 
 
 
93
  self._attr_mode = NumberMode.BOX
94
  self._attr_native_max_value = self.device[description.key]["upper_bound"]
95
  self._attr_native_min_value = self.device[description.key]["lower_bound"]
96
+ self._attr_unique_id = f"{device_id}-{description.key}"
97
+ self.device_id = device_id
98
+ self.entity_description = description
99
 
100
  native_step = self.device[description.key]["resolution"]
101
  if description.key != "temperature_offset":
 
107
  """Return the present setpoint value."""
108
  return self.device[self.entity_description.key]["setpoint"]
109
 
110
+ @plugwise_command
111
  async def async_set_native_value(self, value: float) -> None:
112
  """Change to the new setpoint value."""
113
+ await self.coordinator.api.set_number(
114
+ self.device_id, self.entity_description.key, value
 
 
 
115
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py RENAMED
@@ -1,95 +1,92 @@
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
- from plugwise.constants import SelectOptionsType, SelectType
9
-
10
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
11
- from homeassistant.config_entries import ConfigEntry
12
  from homeassistant.const import STATE_ON, EntityCategory
13
- from homeassistant.core import HomeAssistant
14
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
15
 
16
  from .const import (
17
- COORDINATOR, # pw-beta
18
- DOMAIN,
19
- LOGGER,
 
 
 
 
20
  )
21
- from .coordinator import PlugwiseDataUpdateCoordinator
22
  from .entity import PlugwiseEntity
 
23
 
24
  PARALLEL_UPDATES = 0
25
 
26
 
27
- @dataclass
28
- class PlugwiseSelectDescriptionMixin:
29
- """Mixin values for Plugwise Select entities."""
30
-
31
- command: Callable[[Smile, str, str], Awaitable[None]]
32
- options_key: SelectOptionsType
33
-
34
-
35
- @dataclass
36
- class PlugwiseSelectEntityDescription(
37
- SelectEntityDescription, PlugwiseSelectDescriptionMixin
38
- ):
39
  """Class describing Plugwise Select entities."""
40
 
41
  key: SelectType
 
42
 
43
 
44
  SELECT_TYPES = (
45
  PlugwiseSelectEntityDescription(
46
- key="select_schedule",
47
- translation_key="thermostat_schedule",
48
- icon="mdi:calendar-clock",
49
- command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON),
50
  options_key="available_schedules",
51
  ),
52
  PlugwiseSelectEntityDescription(
53
- key="select_regulation_mode",
54
- translation_key="regulation_mode",
55
- icon="mdi:hvac",
56
  entity_category=EntityCategory.CONFIG,
57
- command=lambda api, loc, opt: api.set_regulation_mode(opt),
58
  options_key="regulation_modes",
59
  ),
60
  PlugwiseSelectEntityDescription(
61
- key="select_dhw_mode",
62
- translation_key="dhw_mode",
63
- icon="mdi:shower",
64
  entity_category=EntityCategory.CONFIG,
65
- command=lambda api, loc, opt: api.set_dhw_mode(opt),
66
  options_key="dhw_modes",
67
  ),
 
 
 
 
 
 
 
 
 
 
 
 
68
  )
69
 
70
 
71
  async def async_setup_entry(
72
  hass: HomeAssistant,
73
- config_entry: ConfigEntry,
74
- async_add_entities: AddEntitiesCallback,
75
  ) -> None:
76
  """Set up the Smile selector from a config entry."""
77
- coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][
78
- config_entry.entry_id
79
- ][COORDINATOR]
80
-
81
- entities: list[PlugwiseSelectEntity] = []
82
- for device_id, device in coordinator.data.devices.items():
83
- for description in SELECT_TYPES:
84
- if description.options_key in device:
85
- entities.append(
86
- PlugwiseSelectEntity(coordinator, device_id, description)
87
- )
88
- LOGGER.debug(
89
- "Add %s %s selector", device["name"], description.translation_key
90
- )
91
 
92
- async_add_entities(entities)
 
93
 
94
 
95
  class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
@@ -105,23 +102,29 @@
105
  ) -> None:
106
  """Initialise the selector."""
107
  super().__init__(coordinator, device_id)
108
- self.entity_description = entity_description
109
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
110
- self._attr_options = self.device[entity_description.options_key]
 
 
 
 
111
 
112
  @property
113
- def current_option(self) -> str:
114
  """Return the selected entity option to represent the entity state."""
115
  return self.device[self.entity_description.key]
116
 
 
 
 
 
 
 
117
  async def async_select_option(self, option: str) -> None:
118
- """Change to the selected entity option."""
119
- await self.entity_description.command(
120
- self.coordinator.api, self.device["location"], option
121
- )
122
- LOGGER.debug(
123
- "Set %s to %s was successful.",
124
- self.entity_description.name,
125
- option,
126
  )
127
- await self.coordinator.async_request_refresh()
 
1
  """Plugwise Select component for Home Assistant."""
 
2
 
 
3
  from dataclasses import dataclass
4
 
 
 
 
5
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
 
6
  from homeassistant.const import STATE_ON, EntityCategory
7
+ from homeassistant.core import HomeAssistant, callback
8
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
  from .const import (
11
+ SELECT_DHW_MODE,
12
+ SELECT_GATEWAY_MODE,
13
+ SELECT_REGULATION_MODE,
14
+ SELECT_SCHEDULE,
15
+ SELECT_ZONE_PROFILE,
16
+ SelectOptionsType,
17
+ SelectType,
18
  )
19
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
20
  from .entity import PlugwiseEntity
21
+ from .util import plugwise_command
22
 
23
  PARALLEL_UPDATES = 0
24
 
25
 
26
+ @dataclass(frozen=True, kw_only=True)
27
+ class PlugwiseSelectEntityDescription(SelectEntityDescription):
 
 
 
 
 
 
 
 
 
 
28
  """Class describing Plugwise Select entities."""
29
 
30
  key: SelectType
31
+ options_key: SelectOptionsType
32
 
33
 
34
  SELECT_TYPES = (
35
  PlugwiseSelectEntityDescription(
36
+ key=SELECT_SCHEDULE,
37
+ translation_key=SELECT_SCHEDULE,
 
 
38
  options_key="available_schedules",
39
  ),
40
  PlugwiseSelectEntityDescription(
41
+ key=SELECT_REGULATION_MODE,
42
+ translation_key=SELECT_REGULATION_MODE,
 
43
  entity_category=EntityCategory.CONFIG,
 
44
  options_key="regulation_modes",
45
  ),
46
  PlugwiseSelectEntityDescription(
47
+ key=SELECT_DHW_MODE,
48
+ translation_key=SELECT_DHW_MODE,
 
49
  entity_category=EntityCategory.CONFIG,
 
50
  options_key="dhw_modes",
51
  ),
52
+ PlugwiseSelectEntityDescription(
53
+ key=SELECT_GATEWAY_MODE,
54
+ translation_key=SELECT_GATEWAY_MODE,
55
+ entity_category=EntityCategory.CONFIG,
56
+ options_key="gateway_modes",
57
+ ),
58
+ PlugwiseSelectEntityDescription(
59
+ key=SELECT_ZONE_PROFILE,
60
+ translation_key=SELECT_ZONE_PROFILE,
61
+ entity_category=EntityCategory.CONFIG,
62
+ options_key="zone_profiles",
63
+ ),
64
  )
65
 
66
 
67
  async def async_setup_entry(
68
  hass: HomeAssistant,
69
+ entry: PlugwiseConfigEntry,
70
+ async_add_entities: AddConfigEntryEntitiesCallback,
71
  ) -> None:
72
  """Set up the Smile selector from a config entry."""
73
+ coordinator = entry.runtime_data
74
+
75
+ @callback
76
+ def _add_entities() -> None:
77
+ """Add Entities."""
78
+ if not coordinator.new_devices:
79
+ return
80
+
81
+ async_add_entities(
82
+ PlugwiseSelectEntity(coordinator, device_id, description)
83
+ for device_id in coordinator.new_devices
84
+ for description in SELECT_TYPES
85
+ if coordinator.data[device_id].get(description.options_key)
86
+ )
87
 
88
+ _add_entities()
89
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
90
 
91
 
92
  class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity):
 
102
  ) -> None:
103
  """Initialise the selector."""
104
  super().__init__(coordinator, device_id)
 
105
  self._attr_unique_id = f"{device_id}-{entity_description.key}"
106
+ self.entity_description = entity_description
107
+
108
+ self._location = device_id
109
+ if (location := self.device.get("location")) is not None:
110
+ self._location = location
111
 
112
  @property
113
+ def current_option(self) -> str | None:
114
  """Return the selected entity option to represent the entity state."""
115
  return self.device[self.entity_description.key]
116
 
117
+ @property
118
+ def options(self) -> list[str]:
119
+ """Return the available select-options."""
120
+ return self.device[self.entity_description.options_key]
121
+
122
+ @plugwise_command
123
  async def async_select_option(self, option: str) -> None:
124
+ """Change to the selected entity option.
125
+
126
+ self._location and STATE_ON are required for the thermostat-schedule select.
127
+ """
128
+ await self.coordinator.api.set_select(
129
+ self.entity_description.key, self._location, option, STATE_ON
 
 
130
  )
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py RENAMED
@@ -1,5 +1,4 @@
1
  """Plugwise Sensor component for Home Assistant."""
2
- from __future__ import annotations
3
 
4
  from dataclasses import dataclass
5
 
@@ -11,7 +10,6 @@
11
  SensorEntityDescription,
12
  SensorStateClass,
13
  )
14
- from homeassistant.config_entries import ConfigEntry
15
  from homeassistant.const import (
16
  LIGHT_LUX,
17
  PERCENTAGE,
@@ -24,184 +22,190 @@
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="uncorrected_temperature",
92
- translation_key="uncorrected_temperature",
93
  entity_category=EntityCategory.DIAGNOSTIC,
94
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
95
  ),
96
  PlugwiseSensorEntityDescription(
97
  key="outdoor_temperature",
98
  translation_key="outdoor_temperature",
99
- device_class=SensorDeviceClass.TEMPERATURE,
100
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
101
- suggested_display_precision=1,
 
 
102
  ),
103
  PlugwiseSensorEntityDescription(
104
  key="outdoor_air_temperature",
105
  translation_key="outdoor_air_temperature",
 
106
  device_class=SensorDeviceClass.TEMPERATURE,
107
  entity_category=EntityCategory.DIAGNOSTIC,
108
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
109
- suggested_display_precision=1,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
  key="water_temperature",
113
  translation_key="water_temperature",
 
114
  device_class=SensorDeviceClass.TEMPERATURE,
115
  entity_category=EntityCategory.DIAGNOSTIC,
116
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
117
  ),
118
  PlugwiseSensorEntityDescription(
119
  key="return_temperature",
120
  translation_key="return_temperature",
 
121
  device_class=SensorDeviceClass.TEMPERATURE,
122
  entity_category=EntityCategory.DIAGNOSTIC,
123
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
124
  ),
125
  PlugwiseSensorEntityDescription(
126
  key="electricity_consumed",
127
  translation_key="electricity_consumed",
128
- device_class=SensorDeviceClass.POWER,
129
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
130
  ),
131
  PlugwiseSensorEntityDescription(
132
  key="electricity_produced",
133
  translation_key="electricity_produced",
134
- device_class=SensorDeviceClass.POWER,
135
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
136
  entity_registry_enabled_default=False,
137
  ),
138
  PlugwiseSensorEntityDescription(
139
  key="electricity_consumed_interval",
140
  translation_key="electricity_consumed_interval",
141
- icon="mdi:lightning-bolt",
142
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
143
  ),
144
  PlugwiseSensorEntityDescription(
145
  key="electricity_consumed_peak_interval",
146
  translation_key="electricity_consumed_peak_interval",
147
- icon="mdi:lightning-bolt",
148
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
149
  ),
150
  PlugwiseSensorEntityDescription(
151
  key="electricity_consumed_off_peak_interval",
152
  translation_key="electricity_consumed_off_peak_interval",
153
- icon="mdi:lightning-bolt",
154
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
155
  ),
156
  PlugwiseSensorEntityDescription(
157
  key="electricity_produced_interval",
158
  translation_key="electricity_produced_interval",
159
- icon="mdi:lightning-bolt",
160
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
161
  entity_registry_enabled_default=False,
162
  ),
163
  PlugwiseSensorEntityDescription(
164
  key="electricity_produced_peak_interval",
165
  translation_key="electricity_produced_peak_interval",
166
- icon="mdi:lightning-bolt",
167
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
168
  ),
169
  PlugwiseSensorEntityDescription(
170
  key="electricity_produced_off_peak_interval",
171
  translation_key="electricity_produced_off_peak_interval",
172
- icon="mdi:lightning-bolt",
173
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
 
 
174
  ),
175
  PlugwiseSensorEntityDescription(
176
  key="electricity_consumed_point",
177
  translation_key="electricity_consumed_point",
178
  device_class=SensorDeviceClass.POWER,
179
  native_unit_of_measurement=UnitOfPower.WATT,
 
180
  ),
181
  PlugwiseSensorEntityDescription(
182
  key="electricity_consumed_off_peak_point",
183
  translation_key="electricity_consumed_off_peak_point",
184
- device_class=SensorDeviceClass.POWER,
185
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
186
  ),
187
  PlugwiseSensorEntityDescription(
188
  key="electricity_consumed_peak_point",
189
  translation_key="electricity_consumed_peak_point",
190
- device_class=SensorDeviceClass.POWER,
191
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
192
  ),
193
  PlugwiseSensorEntityDescription(
194
  key="electricity_consumed_off_peak_cumulative",
195
  translation_key="electricity_consumed_off_peak_cumulative",
196
- device_class=SensorDeviceClass.ENERGY,
197
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
198
  state_class=SensorStateClass.TOTAL_INCREASING,
199
  ),
200
  PlugwiseSensorEntityDescription(
201
  key="electricity_consumed_peak_cumulative",
202
  translation_key="electricity_consumed_peak_cumulative",
203
- device_class=SensorDeviceClass.ENERGY,
204
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
205
  state_class=SensorStateClass.TOTAL_INCREASING,
206
  ),
207
  PlugwiseSensorEntityDescription(
@@ -209,24 +213,27 @@
209
  translation_key="electricity_produced_point",
210
  device_class=SensorDeviceClass.POWER,
211
  native_unit_of_measurement=UnitOfPower.WATT,
 
212
  ),
213
  PlugwiseSensorEntityDescription(
214
  key="electricity_produced_off_peak_point",
215
  translation_key="electricity_produced_off_peak_point",
216
- device_class=SensorDeviceClass.POWER,
217
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
218
  ),
219
  PlugwiseSensorEntityDescription(
220
  key="electricity_produced_peak_point",
221
  translation_key="electricity_produced_peak_point",
222
- device_class=SensorDeviceClass.POWER,
223
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
224
  ),
225
  PlugwiseSensorEntityDescription(
226
  key="electricity_produced_off_peak_cumulative",
227
  translation_key="electricity_produced_off_peak_cumulative",
228
- device_class=SensorDeviceClass.ENERGY,
229
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
230
  state_class=SensorStateClass.TOTAL_INCREASING,
231
  ),
232
  PlugwiseSensorEntityDescription(
@@ -239,45 +246,51 @@
239
  PlugwiseSensorEntityDescription(
240
  key="electricity_phase_one_consumed",
241
  translation_key="electricity_phase_one_consumed",
242
- name="Electricity phase one consumed",
243
  device_class=SensorDeviceClass.POWER,
244
  native_unit_of_measurement=UnitOfPower.WATT,
 
245
  ),
246
  PlugwiseSensorEntityDescription(
247
  key="electricity_phase_two_consumed",
248
  translation_key="electricity_phase_two_consumed",
249
  device_class=SensorDeviceClass.POWER,
250
  native_unit_of_measurement=UnitOfPower.WATT,
 
251
  ),
252
  PlugwiseSensorEntityDescription(
253
  key="electricity_phase_three_consumed",
254
  translation_key="electricity_phase_three_consumed",
255
  device_class=SensorDeviceClass.POWER,
256
  native_unit_of_measurement=UnitOfPower.WATT,
 
257
  ),
258
  PlugwiseSensorEntityDescription(
259
  key="electricity_phase_one_produced",
260
  translation_key="electricity_phase_one_produced",
261
  device_class=SensorDeviceClass.POWER,
262
  native_unit_of_measurement=UnitOfPower.WATT,
 
263
  ),
264
  PlugwiseSensorEntityDescription(
265
  key="electricity_phase_two_produced",
266
  translation_key="electricity_phase_two_produced",
267
  device_class=SensorDeviceClass.POWER,
268
  native_unit_of_measurement=UnitOfPower.WATT,
 
269
  ),
270
  PlugwiseSensorEntityDescription(
271
  key="electricity_phase_three_produced",
272
  translation_key="electricity_phase_three_produced",
273
  device_class=SensorDeviceClass.POWER,
274
  native_unit_of_measurement=UnitOfPower.WATT,
 
275
  ),
276
  PlugwiseSensorEntityDescription(
277
  key="voltage_phase_one",
278
  translation_key="voltage_phase_one",
279
  device_class=SensorDeviceClass.VOLTAGE,
280
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
281
  entity_registry_enabled_default=False,
282
  ),
283
  PlugwiseSensorEntityDescription(
@@ -285,6 +298,7 @@
285
  translation_key="voltage_phase_two",
286
  device_class=SensorDeviceClass.VOLTAGE,
287
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
288
  entity_registry_enabled_default=False,
289
  ),
290
  PlugwiseSensorEntityDescription(
@@ -292,116 +306,121 @@
292
  translation_key="voltage_phase_three",
293
  device_class=SensorDeviceClass.VOLTAGE,
294
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 
295
  entity_registry_enabled_default=False,
296
  ),
297
  PlugwiseSensorEntityDescription(
298
  key="gas_consumed_interval",
299
  translation_key="gas_consumed_interval",
300
- icon="mdi:meter-gas",
301
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
 
302
  ),
303
  PlugwiseSensorEntityDescription(
304
  key="gas_consumed_cumulative",
305
  translation_key="gas_consumed_cumulative",
306
- device_class=SensorDeviceClass.GAS,
307
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
308
- state_class=SensorStateClass.TOTAL_INCREASING,
 
309
  ),
310
  PlugwiseSensorEntityDescription(
311
  key="net_electricity_point",
312
  translation_key="net_electricity_point",
313
- device_class=SensorDeviceClass.POWER,
314
  native_unit_of_measurement=UnitOfPower.WATT,
 
 
315
  ),
316
  PlugwiseSensorEntityDescription(
317
  key="net_electricity_cumulative",
318
  translation_key="net_electricity_cumulative",
319
- device_class=SensorDeviceClass.ENERGY,
320
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
 
321
  state_class=SensorStateClass.TOTAL,
322
  ),
323
  PlugwiseSensorEntityDescription(
324
  key="battery",
 
325
  device_class=SensorDeviceClass.BATTERY,
326
  entity_category=EntityCategory.DIAGNOSTIC,
327
- native_unit_of_measurement=PERCENTAGE,
328
  ),
329
  PlugwiseSensorEntityDescription(
330
  key="illuminance",
 
331
  device_class=SensorDeviceClass.ILLUMINANCE,
332
  entity_category=EntityCategory.DIAGNOSTIC,
333
- native_unit_of_measurement=LIGHT_LUX,
334
  ),
335
  PlugwiseSensorEntityDescription(
336
  key="modulation_level",
337
  translation_key="modulation_level",
338
- entity_category=EntityCategory.DIAGNOSTIC,
339
  native_unit_of_measurement=PERCENTAGE,
340
- icon="mdi:percent",
 
341
  ),
342
  PlugwiseSensorEntityDescription(
343
  key="valve_position",
344
  translation_key="valve_position",
345
- icon="mdi:valve",
346
- entity_category=EntityCategory.DIAGNOSTIC,
347
  native_unit_of_measurement=PERCENTAGE,
 
 
348
  ),
349
  PlugwiseSensorEntityDescription(
350
  key="water_pressure",
351
  translation_key="water_pressure",
 
352
  device_class=SensorDeviceClass.PRESSURE,
353
  entity_category=EntityCategory.DIAGNOSTIC,
354
- native_unit_of_measurement=UnitOfPressure.BAR,
355
  ),
356
  PlugwiseSensorEntityDescription(
357
  key="humidity",
358
- device_class=SensorDeviceClass.HUMIDITY,
359
  native_unit_of_measurement=PERCENTAGE,
 
 
360
  ),
361
  PlugwiseSensorEntityDescription(
362
  key="dhw_temperature",
363
  translation_key="dhw_temperature",
 
364
  device_class=SensorDeviceClass.TEMPERATURE,
365
  entity_category=EntityCategory.DIAGNOSTIC,
366
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
367
  ),
368
  PlugwiseSensorEntityDescription(
369
  key="domestic_hot_water_setpoint",
370
  translation_key="domestic_hot_water_setpoint",
 
371
  device_class=SensorDeviceClass.TEMPERATURE,
372
  entity_category=EntityCategory.DIAGNOSTIC,
373
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
374
  ),
375
  )
376
 
377
 
378
  async def async_setup_entry(
379
  hass: HomeAssistant,
380
- config_entry: ConfigEntry,
381
- async_add_entities: AddEntitiesCallback,
382
  ) -> None:
383
  """Set up the Smile sensors from a config entry."""
384
- coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
385
 
386
- entities: list[PlugwiseSensorEntity] = []
387
- for device_id, device in coordinator.data.devices.items():
388
- if not (sensors := device.get("sensors")):
389
- continue
390
- for description in SENSORS:
391
- if description.key not in sensors:
392
- continue
393
- entities.append(
394
- PlugwiseSensorEntity(
395
- coordinator,
396
- device_id,
397
- description,
398
- )
399
- )
400
- LOGGER.debug(
401
- "Add %s %s sensor", device["name"], description.translation_key
402
- )
403
 
404
- async_add_entities(entities)
 
405
 
406
 
407
  class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
@@ -417,8 +436,8 @@
417
  ) -> None:
418
  """Initialise the sensor."""
419
  super().__init__(coordinator, device_id)
420
- self.entity_description = description
421
  self._attr_unique_id = f"{device_id}-{description.key}"
 
422
 
423
  @property
424
  def native_value(self) -> int | float:
 
1
  """Plugwise Sensor component for Home Assistant."""
 
2
 
3
  from dataclasses import dataclass
4
 
 
10
  SensorEntityDescription,
11
  SensorStateClass,
12
  )
 
13
  from homeassistant.const import (
14
  LIGHT_LUX,
15
  PERCENTAGE,
 
22
  UnitOfVolume,
23
  UnitOfVolumeFlowRate,
24
  )
25
+ from homeassistant.core import HomeAssistant, callback
26
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
27
 
28
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
29
  from .entity import PlugwiseEntity
30
 
31
+ # Coordinator is used to centralize the data updates
32
  PARALLEL_UPDATES = 0
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
  ),
50
  PlugwiseSensorEntityDescription(
51
  key="setpoint_high",
52
  translation_key="cooling_setpoint",
 
 
53
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
54
+ device_class=SensorDeviceClass.TEMPERATURE,
55
+ state_class=SensorStateClass.MEASUREMENT,
56
  ),
57
  PlugwiseSensorEntityDescription(
58
  key="setpoint_low",
59
  translation_key="heating_setpoint",
 
 
60
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
61
+ device_class=SensorDeviceClass.TEMPERATURE,
62
+ state_class=SensorStateClass.MEASUREMENT,
63
  ),
64
  PlugwiseSensorEntityDescription(
65
  key="temperature",
 
 
66
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
67
+ device_class=SensorDeviceClass.TEMPERATURE,
68
+ state_class=SensorStateClass.MEASUREMENT,
69
  ),
70
  PlugwiseSensorEntityDescription(
71
  key="intended_boiler_temperature",
72
  translation_key="intended_boiler_temperature",
73
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
74
  device_class=SensorDeviceClass.TEMPERATURE,
75
  entity_category=EntityCategory.DIAGNOSTIC,
76
+ state_class=SensorStateClass.MEASUREMENT,
77
  ),
78
  PlugwiseSensorEntityDescription(
79
  key="temperature_difference",
80
  translation_key="temperature_difference",
 
81
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
82
+ device_class=SensorDeviceClass.TEMPERATURE,
 
 
 
83
  entity_category=EntityCategory.DIAGNOSTIC,
84
+ state_class=SensorStateClass.MEASUREMENT,
85
  ),
86
  PlugwiseSensorEntityDescription(
87
  key="outdoor_temperature",
88
  translation_key="outdoor_temperature",
 
89
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
90
+ device_class=SensorDeviceClass.TEMPERATURE,
91
+ entity_category=EntityCategory.DIAGNOSTIC,
92
+ state_class=SensorStateClass.MEASUREMENT,
93
  ),
94
  PlugwiseSensorEntityDescription(
95
  key="outdoor_air_temperature",
96
  translation_key="outdoor_air_temperature",
97
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
98
  device_class=SensorDeviceClass.TEMPERATURE,
99
  entity_category=EntityCategory.DIAGNOSTIC,
100
+ state_class=SensorStateClass.MEASUREMENT,
 
101
  ),
102
  PlugwiseSensorEntityDescription(
103
  key="water_temperature",
104
  translation_key="water_temperature",
105
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
106
  device_class=SensorDeviceClass.TEMPERATURE,
107
  entity_category=EntityCategory.DIAGNOSTIC,
108
+ state_class=SensorStateClass.MEASUREMENT,
109
  ),
110
  PlugwiseSensorEntityDescription(
111
  key="return_temperature",
112
  translation_key="return_temperature",
113
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
114
  device_class=SensorDeviceClass.TEMPERATURE,
115
  entity_category=EntityCategory.DIAGNOSTIC,
116
+ state_class=SensorStateClass.MEASUREMENT,
117
  ),
118
  PlugwiseSensorEntityDescription(
119
  key="electricity_consumed",
120
  translation_key="electricity_consumed",
 
121
  native_unit_of_measurement=UnitOfPower.WATT,
122
+ device_class=SensorDeviceClass.POWER,
123
+ state_class=SensorStateClass.MEASUREMENT,
124
  ),
125
  PlugwiseSensorEntityDescription(
126
  key="electricity_produced",
127
  translation_key="electricity_produced",
 
128
  native_unit_of_measurement=UnitOfPower.WATT,
129
+ device_class=SensorDeviceClass.POWER,
130
+ state_class=SensorStateClass.MEASUREMENT,
131
  entity_registry_enabled_default=False,
132
  ),
133
  PlugwiseSensorEntityDescription(
134
  key="electricity_consumed_interval",
135
  translation_key="electricity_consumed_interval",
 
136
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
137
+ device_class=SensorDeviceClass.ENERGY,
138
+ state_class=SensorStateClass.TOTAL,
139
  ),
140
  PlugwiseSensorEntityDescription(
141
  key="electricity_consumed_peak_interval",
142
  translation_key="electricity_consumed_peak_interval",
 
143
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
144
+ device_class=SensorDeviceClass.ENERGY,
145
+ state_class=SensorStateClass.TOTAL,
146
  ),
147
  PlugwiseSensorEntityDescription(
148
  key="electricity_consumed_off_peak_interval",
149
  translation_key="electricity_consumed_off_peak_interval",
 
150
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
151
+ device_class=SensorDeviceClass.ENERGY,
152
+ state_class=SensorStateClass.TOTAL,
153
  ),
154
  PlugwiseSensorEntityDescription(
155
  key="electricity_produced_interval",
156
  translation_key="electricity_produced_interval",
 
157
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
158
+ device_class=SensorDeviceClass.ENERGY,
159
+ state_class=SensorStateClass.TOTAL,
160
  entity_registry_enabled_default=False,
161
  ),
162
  PlugwiseSensorEntityDescription(
163
  key="electricity_produced_peak_interval",
164
  translation_key="electricity_produced_peak_interval",
 
165
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
166
+ device_class=SensorDeviceClass.ENERGY,
167
+ state_class=SensorStateClass.TOTAL,
168
  ),
169
  PlugwiseSensorEntityDescription(
170
  key="electricity_produced_off_peak_interval",
171
  translation_key="electricity_produced_off_peak_interval",
 
172
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
173
+ device_class=SensorDeviceClass.ENERGY,
174
+ state_class=SensorStateClass.TOTAL,
175
  ),
176
  PlugwiseSensorEntityDescription(
177
  key="electricity_consumed_point",
178
  translation_key="electricity_consumed_point",
179
  device_class=SensorDeviceClass.POWER,
180
  native_unit_of_measurement=UnitOfPower.WATT,
181
+ state_class=SensorStateClass.MEASUREMENT,
182
  ),
183
  PlugwiseSensorEntityDescription(
184
  key="electricity_consumed_off_peak_point",
185
  translation_key="electricity_consumed_off_peak_point",
 
186
  native_unit_of_measurement=UnitOfPower.WATT,
187
+ device_class=SensorDeviceClass.POWER,
188
+ state_class=SensorStateClass.MEASUREMENT,
189
  ),
190
  PlugwiseSensorEntityDescription(
191
  key="electricity_consumed_peak_point",
192
  translation_key="electricity_consumed_peak_point",
 
193
  native_unit_of_measurement=UnitOfPower.WATT,
194
+ device_class=SensorDeviceClass.POWER,
195
+ state_class=SensorStateClass.MEASUREMENT,
196
  ),
197
  PlugwiseSensorEntityDescription(
198
  key="electricity_consumed_off_peak_cumulative",
199
  translation_key="electricity_consumed_off_peak_cumulative",
 
200
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
201
+ device_class=SensorDeviceClass.ENERGY,
202
  state_class=SensorStateClass.TOTAL_INCREASING,
203
  ),
204
  PlugwiseSensorEntityDescription(
205
  key="electricity_consumed_peak_cumulative",
206
  translation_key="electricity_consumed_peak_cumulative",
 
207
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
208
+ device_class=SensorDeviceClass.ENERGY,
209
  state_class=SensorStateClass.TOTAL_INCREASING,
210
  ),
211
  PlugwiseSensorEntityDescription(
 
213
  translation_key="electricity_produced_point",
214
  device_class=SensorDeviceClass.POWER,
215
  native_unit_of_measurement=UnitOfPower.WATT,
216
+ state_class=SensorStateClass.MEASUREMENT,
217
  ),
218
  PlugwiseSensorEntityDescription(
219
  key="electricity_produced_off_peak_point",
220
  translation_key="electricity_produced_off_peak_point",
 
221
  native_unit_of_measurement=UnitOfPower.WATT,
222
+ device_class=SensorDeviceClass.POWER,
223
+ state_class=SensorStateClass.MEASUREMENT,
224
  ),
225
  PlugwiseSensorEntityDescription(
226
  key="electricity_produced_peak_point",
227
  translation_key="electricity_produced_peak_point",
 
228
  native_unit_of_measurement=UnitOfPower.WATT,
229
+ device_class=SensorDeviceClass.POWER,
230
+ state_class=SensorStateClass.MEASUREMENT,
231
  ),
232
  PlugwiseSensorEntityDescription(
233
  key="electricity_produced_off_peak_cumulative",
234
  translation_key="electricity_produced_off_peak_cumulative",
 
235
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
236
+ device_class=SensorDeviceClass.ENERGY,
237
  state_class=SensorStateClass.TOTAL_INCREASING,
238
  ),
239
  PlugwiseSensorEntityDescription(
 
246
  PlugwiseSensorEntityDescription(
247
  key="electricity_phase_one_consumed",
248
  translation_key="electricity_phase_one_consumed",
 
249
  device_class=SensorDeviceClass.POWER,
250
  native_unit_of_measurement=UnitOfPower.WATT,
251
+ state_class=SensorStateClass.MEASUREMENT,
252
  ),
253
  PlugwiseSensorEntityDescription(
254
  key="electricity_phase_two_consumed",
255
  translation_key="electricity_phase_two_consumed",
256
  device_class=SensorDeviceClass.POWER,
257
  native_unit_of_measurement=UnitOfPower.WATT,
258
+ state_class=SensorStateClass.MEASUREMENT,
259
  ),
260
  PlugwiseSensorEntityDescription(
261
  key="electricity_phase_three_consumed",
262
  translation_key="electricity_phase_three_consumed",
263
  device_class=SensorDeviceClass.POWER,
264
  native_unit_of_measurement=UnitOfPower.WATT,
265
+ state_class=SensorStateClass.MEASUREMENT,
266
  ),
267
  PlugwiseSensorEntityDescription(
268
  key="electricity_phase_one_produced",
269
  translation_key="electricity_phase_one_produced",
270
  device_class=SensorDeviceClass.POWER,
271
  native_unit_of_measurement=UnitOfPower.WATT,
272
+ state_class=SensorStateClass.MEASUREMENT,
273
  ),
274
  PlugwiseSensorEntityDescription(
275
  key="electricity_phase_two_produced",
276
  translation_key="electricity_phase_two_produced",
277
  device_class=SensorDeviceClass.POWER,
278
  native_unit_of_measurement=UnitOfPower.WATT,
279
+ state_class=SensorStateClass.MEASUREMENT,
280
  ),
281
  PlugwiseSensorEntityDescription(
282
  key="electricity_phase_three_produced",
283
  translation_key="electricity_phase_three_produced",
284
  device_class=SensorDeviceClass.POWER,
285
  native_unit_of_measurement=UnitOfPower.WATT,
286
+ state_class=SensorStateClass.MEASUREMENT,
287
  ),
288
  PlugwiseSensorEntityDescription(
289
  key="voltage_phase_one",
290
  translation_key="voltage_phase_one",
291
  device_class=SensorDeviceClass.VOLTAGE,
292
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
293
+ state_class=SensorStateClass.MEASUREMENT,
294
  entity_registry_enabled_default=False,
295
  ),
296
  PlugwiseSensorEntityDescription(
 
298
  translation_key="voltage_phase_two",
299
  device_class=SensorDeviceClass.VOLTAGE,
300
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
301
+ state_class=SensorStateClass.MEASUREMENT,
302
  entity_registry_enabled_default=False,
303
  ),
304
  PlugwiseSensorEntityDescription(
 
306
  translation_key="voltage_phase_three",
307
  device_class=SensorDeviceClass.VOLTAGE,
308
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
309
+ state_class=SensorStateClass.MEASUREMENT,
310
  entity_registry_enabled_default=False,
311
  ),
312
  PlugwiseSensorEntityDescription(
313
  key="gas_consumed_interval",
314
  translation_key="gas_consumed_interval",
 
315
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
316
+ state_class=SensorStateClass.MEASUREMENT,
317
  ),
318
  PlugwiseSensorEntityDescription(
319
  key="gas_consumed_cumulative",
320
  translation_key="gas_consumed_cumulative",
 
321
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
322
+ device_class=SensorDeviceClass.GAS,
323
+ state_class=SensorStateClass.TOTAL,
324
  ),
325
  PlugwiseSensorEntityDescription(
326
  key="net_electricity_point",
327
  translation_key="net_electricity_point",
 
328
  native_unit_of_measurement=UnitOfPower.WATT,
329
+ device_class=SensorDeviceClass.POWER,
330
+ state_class=SensorStateClass.MEASUREMENT,
331
  ),
332
  PlugwiseSensorEntityDescription(
333
  key="net_electricity_cumulative",
334
  translation_key="net_electricity_cumulative",
 
335
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
336
+ device_class=SensorDeviceClass.ENERGY,
337
  state_class=SensorStateClass.TOTAL,
338
  ),
339
  PlugwiseSensorEntityDescription(
340
  key="battery",
341
+ native_unit_of_measurement=PERCENTAGE,
342
  device_class=SensorDeviceClass.BATTERY,
343
  entity_category=EntityCategory.DIAGNOSTIC,
344
+ state_class=SensorStateClass.MEASUREMENT,
345
  ),
346
  PlugwiseSensorEntityDescription(
347
  key="illuminance",
348
+ native_unit_of_measurement=LIGHT_LUX,
349
  device_class=SensorDeviceClass.ILLUMINANCE,
350
  entity_category=EntityCategory.DIAGNOSTIC,
351
+ state_class=SensorStateClass.MEASUREMENT,
352
  ),
353
  PlugwiseSensorEntityDescription(
354
  key="modulation_level",
355
  translation_key="modulation_level",
 
356
  native_unit_of_measurement=PERCENTAGE,
357
+ entity_category=EntityCategory.DIAGNOSTIC,
358
+ state_class=SensorStateClass.MEASUREMENT,
359
  ),
360
  PlugwiseSensorEntityDescription(
361
  key="valve_position",
362
  translation_key="valve_position",
 
 
363
  native_unit_of_measurement=PERCENTAGE,
364
+ entity_category=EntityCategory.DIAGNOSTIC,
365
+ state_class=SensorStateClass.MEASUREMENT,
366
  ),
367
  PlugwiseSensorEntityDescription(
368
  key="water_pressure",
369
  translation_key="water_pressure",
370
+ native_unit_of_measurement=UnitOfPressure.BAR,
371
  device_class=SensorDeviceClass.PRESSURE,
372
  entity_category=EntityCategory.DIAGNOSTIC,
373
+ state_class=SensorStateClass.MEASUREMENT,
374
  ),
375
  PlugwiseSensorEntityDescription(
376
  key="humidity",
 
377
  native_unit_of_measurement=PERCENTAGE,
378
+ device_class=SensorDeviceClass.HUMIDITY,
379
+ state_class=SensorStateClass.MEASUREMENT,
380
  ),
381
  PlugwiseSensorEntityDescription(
382
  key="dhw_temperature",
383
  translation_key="dhw_temperature",
384
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
385
  device_class=SensorDeviceClass.TEMPERATURE,
386
  entity_category=EntityCategory.DIAGNOSTIC,
387
+ state_class=SensorStateClass.MEASUREMENT,
388
  ),
389
  PlugwiseSensorEntityDescription(
390
  key="domestic_hot_water_setpoint",
391
  translation_key="domestic_hot_water_setpoint",
392
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
393
  device_class=SensorDeviceClass.TEMPERATURE,
394
  entity_category=EntityCategory.DIAGNOSTIC,
395
+ state_class=SensorStateClass.MEASUREMENT,
396
  ),
397
  )
398
 
399
 
400
  async def async_setup_entry(
401
  hass: HomeAssistant,
402
+ entry: PlugwiseConfigEntry,
403
+ async_add_entities: AddConfigEntryEntitiesCallback,
404
  ) -> None:
405
  """Set up the Smile sensors from a config entry."""
406
+ coordinator = entry.runtime_data
407
 
408
+ @callback
409
+ def _add_entities() -> None:
410
+ """Add Entities."""
411
+ if not coordinator.new_devices:
412
+ return
413
+
414
+ async_add_entities(
415
+ PlugwiseSensorEntity(coordinator, device_id, description)
416
+ for device_id in coordinator.new_devices
417
+ if (sensors := coordinator.data[device_id].get("sensors"))
418
+ for description in SENSORS
419
+ if description.key in sensors
420
+ )
 
 
 
 
421
 
422
+ _add_entities()
423
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
424
 
425
 
426
  class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity):
 
436
  ) -> None:
437
  """Initialise the sensor."""
438
  super().__init__(coordinator, device_id)
 
439
  self._attr_unique_id = f"{device_id}-{description.key}"
440
+ self.entity_description = description
441
 
442
  @property
443
  def native_value(self) -> int | float:
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/strings.json RENAMED
@@ -1,48 +1,47 @@
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": {
@@ -53,6 +52,9 @@
53
  "cooling_enabled": {
54
  "name": "Cooling enabled"
55
  },
 
 
 
56
  "dhw_state": {
57
  "name": "DHW state"
58
  },
@@ -60,230 +62,274 @@
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
  }
89
  },
90
  "number": {
91
- "maximum_boiler_temperature": {
92
- "name": "Maximum boiler temperature setpoint"
93
- },
94
  "max_dhw_temperature": {
95
  "name": "Domestic hot water setpoint"
96
  },
 
 
 
97
  "temperature_offset": {
98
  "name": "Temperature offset"
99
  }
100
  },
101
  "select": {
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": {
112
  "name": "Regulation mode",
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
  }
124
  },
125
  "sensor": {
126
- "setpoint": {
127
- "name": "Setpoint"
128
- },
129
  "cooling_setpoint": {
130
  "name": "Cooling setpoint"
131
  },
132
- "heating_setpoint": {
133
- "name": " Heating setpoint"
134
- },
135
- "intended_boiler_temperature": {
136
- "name": "Intended boiler temperature"
137
- },
138
- "temperature_difference": {
139
- "name": "Temperature difference"
140
- },
141
- "uncorrected_temperature": {
142
- "name": "Uncorrected temperature"
143
- },
144
- "outdoor_temperature": {
145
- "name": "Outdoor temperature"
146
- },
147
- "outdoor_air_temperature": {
148
- "name": "Outdoor air temperature"
149
- },
150
- "water_temperature": {
151
- "name": "Water temperature"
152
  },
153
- "return_temperature": {
154
- "name": "Return temperature"
155
  },
156
  "electricity_consumed": {
157
  "name": "Electricity consumed"
158
  },
159
- "electricity_produced": {
160
- "name": "Electricity produced"
161
- },
162
- "electricity_consumed_point": {
163
- "name": "Electricity consumed point"
164
- },
165
- "electricity_produced_point": {
166
- "name": "Electricity produced point"
167
- },
168
  "electricity_consumed_interval": {
169
  "name": "Electricity consumed interval"
170
  },
171
- "electricity_consumed_peak_interval": {
172
- "name": "Electricity consumed peak interval"
173
  },
174
  "electricity_consumed_off_peak_interval": {
175
- "name": "Electricity consumed off peak interval"
176
- },
177
- "electricity_produced_interval": {
178
- "name": "Electricity produced interval"
179
- },
180
- "electricity_produced_peak_interval": {
181
- "name": "Electricity produced peak interval"
182
- },
183
- "electricity_produced_off_peak_interval": {
184
- "name": "Electricity produced off peak interval"
185
  },
186
  "electricity_consumed_off_peak_point": {
187
- "name": "Electricity consumed off peak point"
188
- },
189
- "electricity_consumed_peak_point": {
190
- "name": "Electricity consumed peak point"
191
- },
192
- "electricity_consumed_off_peak_cumulative": {
193
- "name": "Electricity consumed off peak cumulative"
194
  },
195
  "electricity_consumed_peak_cumulative": {
196
  "name": "Electricity consumed peak cumulative"
197
  },
198
- "electricity_produced_off_peak_point": {
199
- "name": "Electricity produced off peak point"
200
- },
201
- "electricity_produced_peak_point": {
202
- "name": "Electricity produced peak point"
203
  },
204
- "electricity_produced_off_peak_cumulative": {
205
- "name": "Electricity produced off peak cumulative"
206
  },
207
- "electricity_produced_peak_cumulative": {
208
- "name": "Electricity produced peak cumulative"
209
  },
210
  "electricity_phase_one_consumed": {
211
  "name": "Electricity phase one consumed"
212
  },
213
- "electricity_phase_two_consumed": {
214
- "name": "Electricity phase two consumed"
215
  },
216
  "electricity_phase_three_consumed": {
217
  "name": "Electricity phase three consumed"
218
  },
219
- "electricity_phase_one_produced": {
220
- "name": "Electricity phase one produced"
 
 
 
221
  },
222
  "electricity_phase_two_produced": {
223
  "name": "Electricity phase two produced"
224
  },
225
- "electricity_phase_three_produced": {
226
- "name": "Electricity phase three produced"
227
  },
228
- "voltage_phase_one": {
229
- "name": "Voltage phase one"
230
  },
231
- "voltage_phase_two": {
232
- "name": "Voltage phase two"
233
  },
234
- "voltage_phase_three": {
235
- "name": "Voltage phase three"
236
  },
237
- "gas_consumed_interval": {
238
- "name": "Gas consumed interval"
 
 
 
 
 
 
 
 
 
 
 
 
239
  },
240
  "gas_consumed_cumulative": {
241
  "name": "Gas consumed cumulative"
242
  },
243
- "net_electricity_point": {
244
- "name": "Net electricity point"
245
  },
246
- "net_electricity_cumulative": {
247
- "name": "Net electricity cumulative"
 
 
 
 
 
 
248
  },
249
  "modulation_level": {
250
  "name": "Modulation level"
251
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  "valve_position": {
253
  "name": "Valve position"
254
  },
255
- "water_pressure": {
256
- "name": "Water pressure"
257
  },
258
- "dhw_temperature": {
259
- "name": "DHW temperature"
260
  },
261
- "domestic_hot_water_setpoint": {
262
- "name": "DHW setpoint"
263
  },
264
- "maximum_boiler_temperature": {
265
- "name": "Maximum boiler temperature setpoint"
 
 
 
266
  }
267
  },
268
  "switch": {
269
  "cooling_ena_switch": {
270
- "name": "Cooling"
271
  },
272
  "dhw_cm_switch": {
273
  "name": "DHW comfort mode"
274
  },
275
  "lock": {
276
- "name": "Lock"
277
  },
278
  "relay": {
279
  "name": "Relay"
280
  }
281
  }
282
  },
283
- "services": {
284
- "delete_notification": {
285
- "name": "Delete Plugwise notification",
286
- "description": "Deletes a Plugwise Notification"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  }
288
  }
289
  }
 
1
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  "config": {
3
+ "abort": {
4
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
5
+ "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
6
+ "not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
7
+ "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
 
 
 
 
 
 
8
  },
9
  "error": {
10
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
11
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
12
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
 
 
13
  "response_error": "Invalid XML data, or error indication received",
14
+ "unknown": "[%key:common::config_flow::error::unknown%]",
 
15
  "unsupported": "Device with unsupported firmware"
16
  },
17
+ "step": {
18
+ "reconfigure": {
19
+ "data": {
20
+ "host": "[%key:common::config_flow::data::ip%]",
21
+ "port": "[%key:common::config_flow::data::port%]"
22
+ },
23
+ "data_description": {
24
+ "host": "[%key:component::plugwise::config::step::user::data_description::host%]",
25
+ "port": "[%key:component::plugwise::config::step::user::data_description::port%]"
26
+ },
27
+ "description": "Update configuration for {title}."
28
+ },
29
+ "user": {
30
+ "data": {
31
+ "host": "[%key:common::config_flow::data::ip%]",
32
+ "password": "Smile ID",
33
+ "port": "[%key:common::config_flow::data::port%]",
34
+ "username": "Smile username"
35
+ },
36
+ "data_description": {
37
+ "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.",
38
+ "password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
39
+ "port": "By default your Smile uses port 80, normally you should not have to change this.",
40
+ "username": "Default is `smile`, or `stretch` for the legacy Stretch."
41
+ },
42
+ "description": "Please enter",
43
+ "title": "Connect to the Smile"
44
+ }
45
  }
46
  },
47
  "entity": {
 
52
  "cooling_enabled": {
53
  "name": "Cooling enabled"
54
  },
55
+ "cooling_state": {
56
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
57
+ },
58
  "dhw_state": {
59
  "name": "DHW state"
60
  },
 
62
  "name": "Flame state"
63
  },
64
  "heating_state": {
65
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
 
 
 
 
 
 
66
  },
67
  "plugwise_notification": {
68
  "name": "Plugwise notification"
69
+ },
70
+ "secondary_boiler_state": {
71
+ "name": "Secondary boiler state"
72
+ }
73
+ },
74
+ "button": {
75
+ "reboot": {
76
+ "name": "Reboot"
77
  }
78
  },
79
  "climate": {
80
  "plugwise": {
81
  "state_attributes": {
82
+ "available_schemas": {
83
+ "name": "Available schemas"
84
+ },
85
  "preset_mode": {
86
  "state": {
87
  "asleep": "Night",
88
+ "away": "[%key:common::state::not_home%]",
89
+ "home": "[%key:common::state::home%]",
90
  "no_frost": "Anti-frost",
91
  "vacation": "Vacation"
92
  }
93
+ },
94
+ "selected_schema": {
95
+ "name": "Selected schema"
96
  }
97
  }
98
  }
99
  },
100
  "number": {
 
 
 
101
  "max_dhw_temperature": {
102
  "name": "Domestic hot water setpoint"
103
  },
104
+ "maximum_boiler_temperature": {
105
+ "name": "Maximum boiler temperature setpoint"
106
+ },
107
  "temperature_offset": {
108
  "name": "Temperature offset"
109
  }
110
  },
111
  "select": {
112
+ "select_dhw_mode": {
113
  "name": "DHW mode",
114
  "state": {
115
+ "auto": "[%key:common::state::auto%]",
116
+ "boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
117
+ "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
118
+ "off": "[%key:common::state::off%]"
119
+ }
120
+ },
121
+ "select_gateway_mode": {
122
+ "name": "Gateway mode",
123
+ "state": {
124
+ "away": "Pause",
125
+ "full": "[%key:common::state::normal%]",
126
+ "vacation": "Vacation"
127
  }
128
  },
129
+ "select_regulation_mode": {
130
  "name": "Regulation mode",
131
  "state": {
132
  "bleeding_cold": "Bleeding cold",
133
  "bleeding_hot": "Bleeding hot",
134
+ "cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
135
+ "heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
136
+ "off": "[%key:common::state::off%]"
137
  }
138
  },
139
+ "select_schedule": {
140
+ "name": "Thermostat schedule",
141
+ "state": {
142
+ "off": "[%key:common::state::off%]"
143
+ }
144
+ },
145
+ "select_zone_profile": {
146
+ "name": "Zone profile",
147
+ "state": {
148
+ "active": "[%key:common::state::active%]",
149
+ "off": "[%key:common::state::off%]",
150
+ "passive": "Passive"
151
+ }
152
  }
153
  },
154
  "sensor": {
 
 
 
155
  "cooling_setpoint": {
156
  "name": "Cooling setpoint"
157
  },
158
+ "dhw_temperature": {
159
+ "name": "DHW temperature"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  },
161
+ "domestic_hot_water_setpoint": {
162
+ "name": "DHW setpoint"
163
  },
164
  "electricity_consumed": {
165
  "name": "Electricity consumed"
166
  },
 
 
 
 
 
 
 
 
 
167
  "electricity_consumed_interval": {
168
  "name": "Electricity consumed interval"
169
  },
170
+ "electricity_consumed_off_peak_cumulative": {
171
+ "name": "Electricity consumed off-peak cumulative"
172
  },
173
  "electricity_consumed_off_peak_interval": {
174
+ "name": "Electricity consumed off-peak interval"
 
 
 
 
 
 
 
 
 
175
  },
176
  "electricity_consumed_off_peak_point": {
177
+ "name": "Electricity consumed off-peak point"
 
 
 
 
 
 
178
  },
179
  "electricity_consumed_peak_cumulative": {
180
  "name": "Electricity consumed peak cumulative"
181
  },
182
+ "electricity_consumed_peak_interval": {
183
+ "name": "Electricity consumed peak interval"
 
 
 
184
  },
185
+ "electricity_consumed_peak_point": {
186
+ "name": "Electricity consumed peak point"
187
  },
188
+ "electricity_consumed_point": {
189
+ "name": "Electricity consumed point"
190
  },
191
  "electricity_phase_one_consumed": {
192
  "name": "Electricity phase one consumed"
193
  },
194
+ "electricity_phase_one_produced": {
195
+ "name": "Electricity phase one produced"
196
  },
197
  "electricity_phase_three_consumed": {
198
  "name": "Electricity phase three consumed"
199
  },
200
+ "electricity_phase_three_produced": {
201
+ "name": "Electricity phase three produced"
202
+ },
203
+ "electricity_phase_two_consumed": {
204
+ "name": "Electricity phase two consumed"
205
  },
206
  "electricity_phase_two_produced": {
207
  "name": "Electricity phase two produced"
208
  },
209
+ "electricity_produced": {
210
+ "name": "Electricity produced"
211
  },
212
+ "electricity_produced_interval": {
213
+ "name": "Electricity produced interval"
214
  },
215
+ "electricity_produced_off_peak_cumulative": {
216
+ "name": "Electricity produced off-peak cumulative"
217
  },
218
+ "electricity_produced_off_peak_interval": {
219
+ "name": "Electricity produced off-peak interval"
220
  },
221
+ "electricity_produced_off_peak_point": {
222
+ "name": "Electricity produced off-peak point"
223
+ },
224
+ "electricity_produced_peak_cumulative": {
225
+ "name": "Electricity produced peak cumulative"
226
+ },
227
+ "electricity_produced_peak_interval": {
228
+ "name": "Electricity produced peak interval"
229
+ },
230
+ "electricity_produced_peak_point": {
231
+ "name": "Electricity produced peak point"
232
+ },
233
+ "electricity_produced_point": {
234
+ "name": "Electricity produced point"
235
  },
236
  "gas_consumed_cumulative": {
237
  "name": "Gas consumed cumulative"
238
  },
239
+ "gas_consumed_interval": {
240
+ "name": "Gas consumed interval"
241
  },
242
+ "heating_setpoint": {
243
+ "name": "Heating setpoint"
244
+ },
245
+ "intended_boiler_temperature": {
246
+ "name": "Intended boiler temperature"
247
+ },
248
+ "maximum_boiler_temperature": {
249
+ "name": "Maximum boiler temperature"
250
  },
251
  "modulation_level": {
252
  "name": "Modulation level"
253
  },
254
+ "net_electricity_cumulative": {
255
+ "name": "Net electricity cumulative"
256
+ },
257
+ "net_electricity_point": {
258
+ "name": "Net electricity point"
259
+ },
260
+ "outdoor_air_temperature": {
261
+ "name": "Outdoor air temperature"
262
+ },
263
+ "outdoor_temperature": {
264
+ "name": "Outdoor temperature"
265
+ },
266
+ "return_temperature": {
267
+ "name": "Return temperature"
268
+ },
269
+ "setpoint": {
270
+ "name": "Setpoint"
271
+ },
272
+ "temperature_difference": {
273
+ "name": "Temperature difference"
274
+ },
275
  "valve_position": {
276
  "name": "Valve position"
277
  },
278
+ "voltage_phase_one": {
279
+ "name": "Voltage phase one"
280
  },
281
+ "voltage_phase_three": {
282
+ "name": "Voltage phase three"
283
  },
284
+ "voltage_phase_two": {
285
+ "name": "Voltage phase two"
286
  },
287
+ "water_pressure": {
288
+ "name": "Water pressure"
289
+ },
290
+ "water_temperature": {
291
+ "name": "Water temperature"
292
  }
293
  },
294
  "switch": {
295
  "cooling_ena_switch": {
296
+ "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
297
  },
298
  "dhw_cm_switch": {
299
  "name": "DHW comfort mode"
300
  },
301
  "lock": {
302
+ "name": "[%key:component::lock::title%]"
303
  },
304
  "relay": {
305
  "name": "Relay"
306
  }
307
  }
308
  },
309
+ "exceptions": {
310
+ "authentication_failed": {
311
+ "message": "[%key:common::config_flow::error::invalid_auth%]"
312
+ },
313
+ "data_incomplete_or_missing": {
314
+ "message": "Data incomplete or missing."
315
+ },
316
+ "error_communicating_with_api": {
317
+ "message": "Error communicating with API: {error}."
318
+ },
319
+ "failed_to_connect": {
320
+ "message": "[%key:common::config_flow::error::cannot_connect%]"
321
+ },
322
+ "invalid_setup": {
323
+ "message": "Add your Adam instead of your Anna, see the documentation"
324
+ },
325
+ "response_error": {
326
+ "message": "[%key:component::plugwise::config::error::response_error%]"
327
+ },
328
+ "set_schedule_first": {
329
+ "message": "Failed setting HVACMode, set a schedule first."
330
+ },
331
+ "unsupported_firmware": {
332
+ "message": "[%key:component::plugwise::config::error::unsupported%]"
333
  }
334
  }
335
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py RENAMED
@@ -1,5 +1,4 @@
1
  """Plugwise Switch component for HomeAssistant."""
2
- from __future__ import annotations
3
 
4
  from dataclasses import dataclass
5
  from typing import Any
@@ -11,22 +10,18 @@
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 +32,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 +47,6 @@
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,24 +54,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
- entities: list[PlugwiseSwitchEntity] = []
74
- for device_id, device in coordinator.data.devices.items():
75
- if not (switches := device.get("switches")):
76
- continue
77
- for description in SWITCHES:
78
- if description.key not in switches:
79
- continue
80
- entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
81
- LOGGER.debug(
82
- "Add %s %s switch", device["name"], description.translation_key
83
- )
 
 
 
84
 
85
- async_add_entities(entities)
 
86
 
87
 
88
  class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
@@ -98,8 +91,8 @@
98
  ) -> None:
99
  """Set up the Plugwise API."""
100
  super().__init__(coordinator, device_id)
101
- self.entity_description = description
102
  self._attr_unique_id = f"{device_id}-{description.key}"
 
103
 
104
  @property
105
  def is_on(self) -> bool:
 
1
  """Plugwise Switch component for HomeAssistant."""
 
2
 
3
  from dataclasses import dataclass
4
  from typing import Any
 
10
  SwitchEntity,
11
  SwitchEntityDescription,
12
  )
 
13
  from homeassistant.const import EntityCategory
14
+ from homeassistant.core import HomeAssistant, callback
15
+ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16
 
17
+ from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
 
 
 
 
 
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
20
 
21
+ PARALLEL_UPDATES = 0
22
+
23
 
24
+ @dataclass(frozen=True)
25
  class PlugwiseSwitchEntityDescription(SwitchEntityDescription):
26
  """Describes Plugwise switch entity."""
27
 
 
32
  PlugwiseSwitchEntityDescription(
33
  key="dhw_cm_switch",
34
  translation_key="dhw_cm_switch",
 
 
35
  entity_category=EntityCategory.CONFIG,
36
  ),
37
  PlugwiseSwitchEntityDescription(
38
  key="lock",
39
  translation_key="lock",
 
 
40
  entity_category=EntityCategory.CONFIG,
41
  ),
42
  PlugwiseSwitchEntityDescription(
 
47
  PlugwiseSwitchEntityDescription(
48
  key="cooling_ena_switch",
49
  translation_key="cooling_ena_switch",
 
 
50
  entity_category=EntityCategory.CONFIG,
51
  ),
52
  )
 
54
 
55
  async def async_setup_entry(
56
  hass: HomeAssistant,
57
+ entry: PlugwiseConfigEntry,
58
+ async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
  """Set up the Smile switches from a config entry."""
61
+ coordinator = entry.runtime_data
62
+
63
+ @callback
64
+ def _add_entities() -> None:
65
+ """Add Entities."""
66
+ if not coordinator.new_devices:
67
+ return
68
+
69
+ async_add_entities(
70
+ PlugwiseSwitchEntity(coordinator, device_id, description)
71
+ for device_id in coordinator.new_devices
72
+ if (switches := coordinator.data[device_id].get("switches"))
73
+ for description in SWITCHES
74
+ if description.key in switches
75
+ )
76
 
77
+ _add_entities()
78
+ entry.async_on_unload(coordinator.async_add_listener(_add_entities))
79
 
80
 
81
  class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity):
 
91
  ) -> None:
92
  """Set up the Plugwise API."""
93
  super().__init__(coordinator, device_id)
 
94
  self._attr_unique_id = f"{device_id}-{description.key}"
95
+ self.entity_description = description
96
 
97
  @property
98
  def is_on(self) -> bool:
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py RENAMED
@@ -1,20 +1,18 @@
1
  """Utilities for Plugwise."""
 
2
  from collections.abc import Awaitable, Callable, Coroutine
3
- from typing import Any, Concatenate, ParamSpec, TypeVar
4
 
5
  from plugwise.exceptions import PlugwiseException
6
 
7
  from homeassistant.exceptions import HomeAssistantError
8
 
 
9
  from .entity import PlugwiseEntity
10
 
11
- _PlugwiseEntityT = TypeVar("_PlugwiseEntityT", bound=PlugwiseEntity)
12
- _R = TypeVar("_R")
13
- _P = ParamSpec("_P")
14
-
15
 
16
- def plugwise_command(
17
- func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]]
18
  ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
19
  """Decorate Plugwise calls that send commands/make changes to the device.
20
 
@@ -27,10 +25,14 @@
27
  ) -> _R:
28
  try:
29
  return await func(self, *args, **kwargs)
30
- except PlugwiseException as error:
31
  raise HomeAssistantError(
32
- f"Error communicating with API: {error}"
33
- ) from error
 
 
 
 
34
  finally:
35
  await self.coordinator.async_request_refresh()
36
 
 
1
  """Utilities for Plugwise."""
2
+
3
  from collections.abc import Awaitable, Callable, Coroutine
4
+ from typing import Any, Concatenate
5
 
6
  from plugwise.exceptions import PlugwiseException
7
 
8
  from homeassistant.exceptions import HomeAssistantError
9
 
10
+ from .const import DOMAIN
11
  from .entity import PlugwiseEntity
12
 
 
 
 
 
13
 
14
+ def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
15
+ func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
16
  ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
17
  """Decorate Plugwise calls that send commands/make changes to the device.
18
 
 
25
  ) -> _R:
26
  try:
27
  return await func(self, *args, **kwargs)
28
+ except PlugwiseException as err:
29
  raise HomeAssistantError(
30
+ translation_domain=DOMAIN,
31
+ translation_key="error_communicating_with_api",
32
+ translation_placeholders={
33
+ "error": str(err),
34
+ },
35
+ ) from err
36
  finally:
37
  await self.coordinator.async_request_refresh()
38