Diff to HTML by rtfpessoa

Files changed (16) hide show
  1. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py +25 -109
  2. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/binary_sensor.py +35 -94
  3. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/button.py +7 -17
  4. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py +71 -105
  5. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py +27 -130
  6. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py +58 -162
  7. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py +21 -46
  8. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py +16 -35
  9. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/icons.json +0 -3
  10. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json +5 -4
  11. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py +36 -48
  12. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py +14 -39
  13. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py +101 -172
  14. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py +60 -47
  15. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py +171 -9
  16. /home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/water_heater.py +11 -17
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/__init__.py RENAMED
@@ -2,51 +2,21 @@
2
 
3
  from typing import Any
4
 
5
- from homeassistant.const import CONF_TIMEOUT, Platform
6
  from homeassistant.core import HomeAssistant, callback
7
- from homeassistant.helpers import (
8
- config_validation as cv,
9
- device_registry as dr,
10
- entity_registry as er,
11
- )
12
- from homeassistant.helpers.typing import ConfigType
13
-
14
- from .const import (
15
- CONF_REFRESH_INTERVAL, # pw-beta options
16
- DOMAIN,
17
- LOGGER,
18
- PLATFORMS,
19
- )
20
- from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
21
- from .services import async_setup_services
22
-
23
- CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
24
-
25
- async def async_setup(hass: HomeAssistant, _config: ConfigType) -> bool:
26
- """Set up my integration."""
27
-
28
- async_setup_services(hass)
29
 
30
- return True
 
31
 
32
 
33
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> None:
34
- """Set up Plugwise from a config entry."""
35
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
36
 
37
- cooldown = 1.5 # pw-beta frontend refresh-interval
38
- if (
39
- custom_refresh := entry.options.get(CONF_REFRESH_INTERVAL)
40
- ) is not None: # pragma: no cover
41
- cooldown = custom_refresh
42
- LOGGER.debug("DUC cooldown interval: %s", cooldown)
43
-
44
- coordinator = PlugwiseDataUpdateCoordinator(
45
- hass, cooldown, entry
46
- ) # pw-beta - cooldown, update_interval as extra
47
  await coordinator.async_config_entry_first_refresh()
48
-
49
- await async_migrate_entities(hass, coordinator)
50
 
51
  entry.runtime_data = coordinator
52
 
@@ -63,23 +33,18 @@
63
 
64
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
65
 
66
- entry.async_on_unload(entry.add_update_listener(update_listener)) # pw-beta options_flow
67
-
68
- async def update_listener(
69
- hass: HomeAssistant, entry: PlugwiseConfigEntry
70
- ) -> None: # pragma: no cover # pw-beta
71
- """Handle options update."""
72
- await hass.config_entries.async_reload(entry.entry_id)
73
 
74
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> None:
75
- """Unload Plugwise."""
76
- await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
 
77
 
78
  @callback
79
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
80
  """Migrate Plugwise entity entries.
81
 
82
- - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
 
83
  """
84
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
85
  "-slave_boiler_state"
@@ -101,78 +66,29 @@
101
  # No migration needed
102
  return None
103
 
104
- async def async_migrate_entities(
 
105
  hass: HomeAssistant,
106
  coordinator: PlugwiseDataUpdateCoordinator,
107
  ) -> None:
108
- """Migrate entities if needed."""
109
  ent_reg = er.async_get(hass)
 
 
 
110
  for device_id, device in coordinator.data.items():
111
- if device["dev_class"] not in ("climate", "heater_central", "thermostat"):
112
  continue
113
 
114
- # Migrate opentherm_outdoor_temperature
115
- # to opentherm_outdoor_air_temperature sensor
116
  old_unique_id = f"{device_id}-outdoor_temperature"
117
  if entity_id := ent_reg.async_get_entity_id(
118
  Platform.SENSOR, DOMAIN, old_unique_id
119
  ):
120
  new_unique_id = f"{device_id}-outdoor_air_temperature"
 
 
 
 
 
 
121
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
122
- LOGGER.debug("Migrated %s to %s", old_unique_id, new_unique_id)
123
-
124
- # Pw-beta only - reverse migrate climate entities using their stable device ID; the device name
125
- # in the present unique ID may have changed. When dual entries exist, one with the previously
126
- # and correct unique_id, remove the newer one.
127
- new_unique_id = f"{device_id}-climate"
128
- er_entries = er.async_entries_for_config_entry(
129
- ent_reg, coordinator.config_entry.entry_id
130
- )
131
- entry_list = [
132
- entry
133
- for entry in er_entries
134
- if entry.domain == Platform.CLIMATE
135
- and entry.platform == DOMAIN
136
- and entry.unique_id.startswith(f"{device_id}-")
137
- ]
138
-
139
- if len(entry_list) == 1:
140
- entity_id = entry_list[0].entity_id
141
- previous_unique_id = entry_list[0].unique_id
142
- if previous_unique_id != new_unique_id:
143
- ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
144
- LOGGER.debug("Migrated %s to %s", previous_unique_id, new_unique_id)
145
- continue
146
-
147
- # Remove the double entry with the present unique_id, this will leave
148
- # the entry with the original unique_id which ends with `-climate`
149
- for item in entry_list:
150
- if (old_unique_id := item.unique_id) != new_unique_id and (
151
- entity_id := ent_reg.async_get_entity_id(
152
- Platform.CLIMATE,
153
- DOMAIN,
154
- old_unique_id,
155
- )
156
- ):
157
- ent_reg.async_remove(entity_id)
158
- return
159
-
160
- # pw-beta only - revert adding CONF_TIMEOUT to config_entry in v0.53.3
161
- async def async_migrate_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> None:
162
- """Migrate back to v1.1 config entry."""
163
- if entry.version > 1:
164
- # This means the user has downgraded from a future version
165
- return #pragma: no cover
166
-
167
- if entry.version == 1 and entry.minor_version == 2:
168
- new_data = {**entry.data}
169
- new_data.pop(CONF_TIMEOUT)
170
- hass.config_entries.async_update_entry(
171
- entry, data=new_data, minor_version=1, version=1
172
- )
173
-
174
- LOGGER.debug(
175
- "Migration to version %s.%s successful",
176
- entry.version,
177
- entry.minor_version,
178
- )
 
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) -> None:
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
 
 
33
 
34
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
35
 
 
 
 
 
 
 
 
36
 
37
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> None:
38
+ """Unload the Plugwise components."""
39
+ await hass.config_entries.async_unload_platforms_or_raise(entry, PLATFORMS)
40
+
41
 
42
  @callback
43
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
44
  """Migrate Plugwise entity entries.
45
 
46
+ Migrates old unique ID's from old binary_sensors and
47
+ switches to the new unique ID's.
48
  """
49
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
50
  "-slave_boiler_state"
 
66
  # No migration needed
67
  return None
68
 
69
+
70
+ def migrate_sensor_entities(
71
  hass: HomeAssistant,
72
  coordinator: PlugwiseDataUpdateCoordinator,
73
  ) -> None:
74
+ """Migrate Sensors if needed."""
75
  ent_reg = er.async_get(hass)
76
+
77
+ # Migrating opentherm_outdoor_temperature
78
+ # to opentherm_outdoor_air_temperature sensor
79
  for device_id, device in coordinator.data.items():
80
+ if device[DEV_CLASS] != "heater_central":
81
  continue
82
 
 
 
83
  old_unique_id = f"{device_id}-outdoor_temperature"
84
  if entity_id := ent_reg.async_get_entity_id(
85
  Platform.SENSOR, DOMAIN, old_unique_id
86
  ):
87
  new_unique_id = f"{device_id}-outdoor_air_temperature"
88
+ LOGGER.debug(
89
+ "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
90
+ entity_id,
91
+ old_unique_id,
92
+ new_unique_id,
93
+ )
94
  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
@@ -6,9 +6,6 @@
6
 
7
  from plugwise.constants import BinarySensorType
8
 
9
- from homeassistant.components import (
10
- persistent_notification, # pw-beta Plugwise notifications
11
- )
12
  from homeassistant.components.binary_sensor import (
13
  BinarySensorDeviceClass,
14
  BinarySensorEntity,
@@ -18,26 +15,11 @@
18
  from homeassistant.core import HomeAssistant, callback
19
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
20
 
21
- from .const import (
22
- BATTERY_STATE,
23
- BINARY_SENSORS,
24
- COMPRESSOR_STATE,
25
- COOLING_ENABLED,
26
- COOLING_STATE,
27
- DHW_STATE,
28
- DOMAIN,
29
- FLAME_STATE,
30
- HEATING_STATE,
31
- LOGGER, # pw-beta
32
- PLUGWISE_NOTIFICATION,
33
- SECONDARY_BOILER_STATE,
34
- SEVERITIES,
35
- )
36
-
37
- # Upstream
38
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
39
  from .entity import PlugwiseEntity
40
 
 
 
41
  # Coordinator is used to centralize the data updates
42
  PARALLEL_UPDATES = 0
43
 
@@ -49,51 +31,50 @@
49
  key: BinarySensorType
50
 
51
 
52
- # Upstream PLUGWISE_BINARY_SENSORS
53
- PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
54
  PlugwiseBinarySensorEntityDescription(
55
- key=BATTERY_STATE,
56
  device_class=BinarySensorDeviceClass.BATTERY,
57
  entity_category=EntityCategory.DIAGNOSTIC,
58
  ),
59
  PlugwiseBinarySensorEntityDescription(
60
- key=COMPRESSOR_STATE,
61
- translation_key=COMPRESSOR_STATE,
62
  entity_category=EntityCategory.DIAGNOSTIC,
63
  ),
64
  PlugwiseBinarySensorEntityDescription(
65
- key=COOLING_ENABLED,
66
- translation_key=COOLING_ENABLED,
67
  entity_category=EntityCategory.DIAGNOSTIC,
68
  ),
69
  PlugwiseBinarySensorEntityDescription(
70
- key=DHW_STATE,
71
- translation_key=DHW_STATE,
72
  entity_category=EntityCategory.DIAGNOSTIC,
73
  ),
74
  PlugwiseBinarySensorEntityDescription(
75
- key=FLAME_STATE,
76
- translation_key=FLAME_STATE,
77
  entity_category=EntityCategory.DIAGNOSTIC,
78
  ),
79
  PlugwiseBinarySensorEntityDescription(
80
- key=HEATING_STATE,
81
- translation_key=HEATING_STATE,
82
  entity_category=EntityCategory.DIAGNOSTIC,
83
  ),
84
  PlugwiseBinarySensorEntityDescription(
85
- key=COOLING_STATE,
86
- translation_key=COOLING_STATE,
87
  entity_category=EntityCategory.DIAGNOSTIC,
88
  ),
89
  PlugwiseBinarySensorEntityDescription(
90
- key=SECONDARY_BOILER_STATE,
91
- translation_key=SECONDARY_BOILER_STATE,
92
  entity_category=EntityCategory.DIAGNOSTIC,
93
  ),
94
  PlugwiseBinarySensorEntityDescription(
95
- key=PLUGWISE_NOTIFICATION,
96
- translation_key=PLUGWISE_NOTIFICATION,
97
  entity_category=EntityCategory.DIAGNOSTIC,
98
  ),
99
  )
@@ -104,7 +85,7 @@
104
  entry: PlugwiseConfigEntry,
105
  async_add_entities: AddConfigEntryEntitiesCallback,
106
  ) -> None:
107
- """Set up Plugwise binary_sensors from a config entry."""
108
  coordinator = entry.runtime_data
109
 
110
  @callback
@@ -113,40 +94,20 @@
113
  if not coordinator.new_devices:
114
  return
115
 
116
- # Upstream consts to HA
117
- # async_add_entities(
118
- # PlugwiseBinarySensorEntity(coordinator, device_id, description)
119
- # for device_id in coordinator.new_devices
120
- # if (
121
- # binary_sensors := coordinator.data.devices[device_id].get(
122
- # BINARY_SENSORS
123
- # )
124
- # )
125
- # for description in PLUGWISE_BINARY_SENSORS
126
- # if description.key in binary_sensors
127
- # )
128
-
129
- # pw-beta alternative for debugging
130
- entities: list[PlugwiseBinarySensorEntity] = []
131
- for device_id in coordinator.new_devices:
132
- device = coordinator.data[device_id]
133
- if not (binary_sensors := device.get(BINARY_SENSORS)):
134
- continue
135
- for description in PLUGWISE_BINARY_SENSORS:
136
- if description.key not in binary_sensors:
137
- continue
138
- entities.append(PlugwiseBinarySensorEntity(coordinator, device_id, description))
139
- LOGGER.debug(
140
- "Add %s %s binary sensor", device["name"], description.translation_key or description.key
141
- )
142
- async_add_entities(entities)
143
 
144
  _add_entities()
145
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
146
 
147
 
148
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
149
- """Set up Plugwise binary_sensors from a config entry."""
150
 
151
  entity_description: PlugwiseBinarySensorEntityDescription
152
 
@@ -160,48 +121,28 @@
160
  super().__init__(coordinator, device_id)
161
  self.entity_description = description
162
  self._attr_unique_id = f"{device_id}-{description.key}"
163
- self._notification: dict[str, str] = {} # pw-beta
164
 
165
  @property
166
  @override
167
- def is_on(self) -> bool | None:
168
  """Return true if the binary sensor is on."""
169
- # pw-beta: show Plugwise notifications as HA persistent notifications
170
- if self._notification:
171
- for notify_id, message in self._notification.items():
172
- persistent_notification.async_create(
173
- self.hass, message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
174
- )
175
-
176
- return self.device.get(BINARY_SENSORS, {}).get(self.entity_description.key)
177
 
178
  @property
179
  @override
180
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
181
  """Return entity specific state attributes."""
182
- if self.entity_description.key != PLUGWISE_NOTIFICATION: # Upstream const
183
  return None
184
 
185
- # pw-beta adjustment with attrs is to only represent severities *with* content
186
- # not all severities including those without content as empty lists
187
- attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
188
- self._notification = {} # pw-beta
189
  gateway_id = self.coordinator.api.gateway_id
190
  if notify := self.coordinator.data[gateway_id]["notifications"]:
191
- for notify_id, details in notify.items(): # pw-beta uses notify_id
192
  for msg_type, msg in details.items():
193
  msg_type = msg_type.lower()
194
  if msg_type not in SEVERITIES:
195
- msg_type = "other" # pragma: no cover
196
-
197
- if (
198
- f"{msg_type}_msg" not in attrs
199
- ): # pw-beta Re-evaluate against Core
200
- attrs[f"{msg_type}_msg"] = []
201
  attrs[f"{msg_type}_msg"].append(msg)
202
 
203
- self._notification[
204
- notify_id
205
- ] = f"{msg_type.title()}: {msg}" # pw-beta
206
-
207
  return attrs
 
6
 
7
  from plugwise.constants import BinarySensorType
8
 
 
 
 
9
  from homeassistant.components.binary_sensor import (
10
  BinarySensorDeviceClass,
11
  BinarySensorEntity,
 
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
 
 
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
  )
 
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
 
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):
110
+ """Represent Smile Binary Sensors."""
111
 
112
  entity_description: PlugwiseBinarySensorEntityDescription
113
 
 
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
  @override
127
+ def is_on(self) -> bool:
128
  """Return true if the binary sensor is on."""
129
+ return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
130
 
131
  @property
132
  @override
133
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
134
  """Return entity specific state attributes."""
135
+ if self.entity_description.key != "plugwise_notification":
136
  return None
137
 
138
+ attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
 
 
 
139
  gateway_id = self.coordinator.api.gateway_id
140
  if notify := self.coordinator.data[gateway_id]["notifications"]:
141
+ for details in notify.values():
142
  for msg_type, msg in details.items():
143
  msg_type = msg_type.lower()
144
  if msg_type not in SEVERITIES:
145
+ msg_type = "other"
 
 
 
 
 
146
  attrs[f"{msg_type}_msg"].append(msg)
147
 
 
 
 
 
148
  return attrs
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/button.py RENAMED
@@ -7,10 +7,7 @@
7
  from homeassistant.core import HomeAssistant
8
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
- from .const import (
11
- LOGGER, # pw-betea
12
- REBOOT,
13
- )
14
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
15
  from .entity import PlugwiseEntity
16
  from .util import plugwise_command
@@ -23,21 +20,14 @@
23
  entry: PlugwiseConfigEntry,
24
  async_add_entities: AddConfigEntryEntitiesCallback,
25
  ) -> None:
26
- """Set up Plugwise buttons from a config entry."""
27
  coordinator = entry.runtime_data
28
 
29
- # async_add_entities(
30
- # PlugwiseButtonEntity(coordinator, device_id)
31
- # for device_id in coordinator.data.devices
32
- # if device_id == gateway[GATEWAY_ID] and REBOOT in gateway
33
- # )
34
- # pw-beta alternative for debugging
35
- entities: list[PlugwiseButtonEntity] = []
36
- for device_id, device in coordinator.data.items():
37
- if device_id == coordinator.api.gateway_id and coordinator.api.reboot:
38
- entities.append(PlugwiseButtonEntity(coordinator, device_id))
39
- LOGGER.debug("Add %s reboot button", device["name"])
40
- async_add_entities(entities)
41
 
42
 
43
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
 
7
  from homeassistant.core import HomeAssistant
8
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
+ from .const import REBOOT
 
 
 
11
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
12
  from .entity import PlugwiseEntity
13
  from .util import plugwise_command
 
20
  entry: PlugwiseConfigEntry,
21
  async_add_entities: AddConfigEntryEntitiesCallback,
22
  ) -> None:
23
+ """Set up the Plugwise buttons from a ConfigEntry."""
24
  coordinator = entry.runtime_data
25
 
26
+ async_add_entities(
27
+ PlugwiseButtonEntity(coordinator, device_id)
28
+ for device_id in coordinator.data
29
+ if device_id == coordinator.api.gateway_id and coordinator.api.reboot
30
+ )
 
 
 
 
 
 
 
31
 
32
 
33
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/climate.py RENAMED
@@ -1,7 +1,5 @@
1
  """Plugwise Climate component for Home Assistant."""
2
 
3
- # pylint: disable=home-assistant-entity-unique-id-redundant-platform
4
-
5
  from dataclasses import asdict, dataclass
6
  from typing import Any, override
7
 
@@ -14,38 +12,13 @@
14
  HVACAction,
15
  HVACMode,
16
  )
17
- from homeassistant.const import (
18
- ATTR_NAME,
19
- ATTR_TEMPERATURE,
20
- STATE_OFF,
21
- STATE_ON,
22
- UnitOfTemperature,
23
- )
24
  from homeassistant.core import HomeAssistant, callback
25
  from homeassistant.exceptions import HomeAssistantError
26
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
27
  from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
28
 
29
- from .const import (
30
- ACTIVE_PRESET,
31
- AVAILABLE_SCHEDULES,
32
- CLIMATE_MODE,
33
- CONTROL_STATE,
34
- DEV_CLASS,
35
- DOMAIN,
36
- LOCATION,
37
- LOGGER,
38
- LOWER_BOUND,
39
- REGULATION_MODES,
40
- RESOLUTION,
41
- SELECT_REGULATION_MODE,
42
- SENSORS,
43
- TARGET_TEMP,
44
- TARGET_TEMP_HIGH,
45
- TARGET_TEMP_LOW,
46
- THERMOSTAT,
47
- UPPER_BOUND,
48
- )
49
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
50
  from .entity import PlugwiseEntity
51
  from .util import plugwise_command
@@ -90,32 +63,27 @@
90
  entry: PlugwiseConfigEntry,
91
  async_add_entities: AddConfigEntryEntitiesCallback,
92
  ) -> None:
93
- """Set up Plugwise thermostats from a config entry."""
94
  coordinator = entry.runtime_data
95
 
96
  @callback
97
  def _add_entities() -> None:
98
- """Add Entities during init and runtime."""
99
  if not coordinator.new_devices:
100
  return
101
 
102
- entities: list[PlugwiseClimateEntity] = []
103
- gateway_name = coordinator.api.smile.name
104
- for device_id in coordinator.new_devices:
105
- device = coordinator.data[device_id]
106
- if gateway_name == "Adam":
107
- if device[DEV_CLASS] == "climate":
108
- entities.append(
109
- PlugwiseClimateEntity(coordinator, device_id)
110
- )
111
- LOGGER.debug("Add climate %s", device[ATTR_NAME])
112
- elif device[DEV_CLASS] == THERMOSTAT:
113
- entities.append(
114
- PlugwiseClimateEntity(coordinator, device_id)
115
- )
116
- LOGGER.debug("Add climate %s", device[ATTR_NAME])
117
-
118
- async_add_entities(entities)
119
 
120
  _add_entities()
121
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -136,22 +104,20 @@
136
  """Set up the Plugwise API."""
137
  super().__init__(coordinator, device_id)
138
  # Legacy format, kept as migrating existing unique IDs is not worth the risk
139
- self._attr_unique_id = f"{device_id}-climate"
 
140
  self._api = coordinator.api
141
  gateway_id: str = self._api.gateway_id
142
  self._gateway_data = coordinator.data[gateway_id]
143
  self._last_active_schedule = STATE_OFF
144
  self._location = device_id
145
- if (location := self.device.get(LOCATION)) is not None:
146
  self._location = location
147
  self._previous_action_mode = HVACAction.HEATING.value
148
 
149
  # Determine supported features
150
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
151
- if (
152
- self._api.cooling_present
153
- and self._api.smile.name != "Adam"
154
- ):
155
  self._attr_supported_features = (
156
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
157
  )
@@ -159,15 +125,15 @@
159
  self._attr_supported_features |= (
160
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
161
  )
162
- if presets := self.device.get("preset_modes", None): # can be NONE
163
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
164
- self._attr_preset_modes = presets
165
 
166
- self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
167
- self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
168
  # Ensure we don't drop below 0.1
169
  self._attr_target_temperature_step = max(
170
- self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5), 0.1
171
  )
172
 
173
  @override
@@ -175,12 +141,15 @@
175
  """Run when entity about to be added."""
176
  await super().async_added_to_hass()
177
 
178
- extra_data = await self.async_get_last_extra_data()
179
- if extra_data is not None:
180
- data = extra_data.as_dict()
181
- self._last_active_schedule = data.get("last_active_schedule") or STATE_OFF
 
 
 
182
  self._previous_action_mode = (
183
- data.get("previous_action_mode") or HVACAction.HEATING.value
184
  )
185
 
186
  @property
@@ -188,76 +157,70 @@
188
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
189
  """Return text specific state data to be restored."""
190
  return PlugwiseClimateExtraStoredData(
191
- self._last_active_schedule,
192
- self._previous_action_mode,
193
  )
194
 
195
  @property
196
  @override
197
  def native_current_temperature(self) -> float | None:
198
  """Return the current temperature."""
199
- return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)
200
 
201
  @property
202
  @override
203
- def native_target_temperature(self) -> float | None:
204
  """Return the temperature we try to reach.
205
 
206
  Connected to the HVACMode combination of AUTO-HEAT.
207
  """
208
 
209
- return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP)
210
 
211
  @property
212
  @override
213
- def native_target_temperature_high(self) -> float | None:
214
  """Return the temperature we try to reach in case of cooling.
215
 
216
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
217
  """
218
- return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_HIGH)
219
 
220
  @property
221
  @override
222
- def native_target_temperature_low(self) -> float | None:
223
  """Return the heating temperature we try to reach in case of heating.
224
 
225
  Connected to the HVACMode combination AUTO-HEAT_COOL.
226
  """
227
- return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_LOW)
228
 
229
  @property
230
  @override
231
  def hvac_mode(self) -> HVACMode:
232
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
233
- mode = self.device.get(CLIMATE_MODE)
234
- if mode is None:
235
- return HVACMode.HEAT # pragma: no cover
236
- try:
237
- hvac = HVACMode(mode)
238
- except ValueError: # pragma: no cover
239
- return HVACMode.HEAT # pragma: no cover
240
- if hvac not in self.hvac_modes:
241
- return HVACMode.HEAT # pragma: no cover
242
-
243
- return hvac
244
 
245
  @property
246
  @override
247
  def hvac_modes(self) -> list[HVACMode]:
248
  """Return a list of available HVACModes."""
249
  hvac_modes: list[HVACMode] = []
250
- if REGULATION_MODES in self._gateway_data:
251
  hvac_modes.append(HVACMode.OFF)
252
 
253
- if self.device[AVAILABLE_SCHEDULES] != [STATE_OFF]:
254
  hvac_modes.append(HVACMode.AUTO)
255
 
256
  if self._api.cooling_present:
257
- if REGULATION_MODES in self._gateway_data:
258
- if "heating" in self._gateway_data[REGULATION_MODES]:
259
  hvac_modes.append(HVACMode.HEAT)
260
- if "cooling" in self._gateway_data[REGULATION_MODES]:
261
  hvac_modes.append(HVACMode.COOL)
262
  else:
263
  hvac_modes.append(HVACMode.HEAT_COOL)
@@ -268,19 +231,19 @@
268
 
269
  @property
270
  @override
271
- def hvac_action(self) -> HVACAction: # pw-beta add to Core
272
  """Return the current running hvac operation if supported."""
273
  # Keep track of the previous hvac_action mode.
274
  # When no cooling available, _previous_action_mode is always heating
275
  if (
276
- REGULATION_MODES in self._gateway_data
277
- and HVACAction.COOLING.value in self._gateway_data[REGULATION_MODES]
278
  ):
279
- mode = self._gateway_data[SELECT_REGULATION_MODE]
280
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
281
  self._previous_action_mode = mode
282
 
283
- if (action := self.device.get(CONTROL_STATE)) is not None:
284
  return HVACAction(action)
285
 
286
  return HVACAction.IDLE
@@ -289,7 +252,7 @@
289
  @override
290
  def preset_mode(self) -> str | None:
291
  """Return the current preset mode."""
292
- return self.device.get(ACTIVE_PRESET)
293
 
294
  @plugwise_command
295
  @override
@@ -297,13 +260,11 @@
297
  """Set new target temperature."""
298
  data: dict[str, Any] = {}
299
  if ATTR_TEMPERATURE in kwargs:
300
- data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
301
  if ATTR_TARGET_TEMP_HIGH in kwargs:
302
- data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
303
  if ATTR_TARGET_TEMP_LOW in kwargs:
304
- data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
305
-
306
- # Upstream removed input-valid check
307
 
308
  if mode := kwargs.get(ATTR_HVAC_MODE):
309
  await self.async_set_hvac_mode(mode)
@@ -311,7 +272,7 @@
311
  await self._api.set_temperature(self._location, data)
312
 
313
  def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
314
- """Return the API regulation value for a manual HVAC mode.
315
 
316
  The function inputs are limited to the HVACModes HEAT and COOL.
317
  """
@@ -325,7 +286,6 @@
325
  @override
326
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
327
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
328
-
329
  # Early exit if no mode change
330
  if hvac_mode == self.hvac_mode:
331
  return
@@ -345,7 +305,9 @@
345
  if hvac_mode == HVACMode.AUTO:
346
  _check_for_schedule(schedule_is_active, self._last_active_schedule)
347
  await self._api.set_schedule_state(
348
- self._location, desired_schedule, STATE_ON
 
 
349
  )
350
  await self._api.set_regulation_mode(self._previous_action_mode)
351
  return
@@ -353,7 +315,9 @@
353
  # Transition to manual mode
354
  if schedule_is_active:
355
  await self._api.set_schedule_state(
356
- self._location, current_schedule, STATE_OFF,
 
 
357
  )
358
  self._last_active_schedule = current_schedule
359
  regulation = self._regulation_mode_for_hvac(hvac_mode)
@@ -363,7 +327,9 @@
363
  # Common - transition from auto = schedule off
364
  if self.hvac_mode == HVACMode.AUTO:
365
  await self._api.set_schedule_state(
366
- self._location, current_schedule, STATE_OFF,
 
 
367
  )
368
  self._last_active_schedule = current_schedule
369
  return
 
1
  """Plugwise Climate component for Home Assistant."""
2
 
 
 
3
  from dataclasses import asdict, dataclass
4
  from typing import Any, override
5
 
 
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
 
63
  entry: PlugwiseConfigEntry,
64
  async_add_entities: AddConfigEntryEntitiesCallback,
65
  ) -> None:
66
+ """Set up the Smile Thermostats from a config entry."""
67
  coordinator = entry.runtime_data
68
 
69
  @callback
70
  def _add_entities() -> None:
71
+ """Add Entities."""
72
  if not coordinator.new_devices:
73
  return
74
 
75
+ if coordinator.api.smile.name == "Adam":
76
+ async_add_entities(
77
+ PlugwiseClimateEntity(coordinator, device_id)
78
+ for device_id in coordinator.new_devices
79
+ if coordinator.data[device_id]["dev_class"] == "climate"
80
+ )
81
+ else:
82
+ async_add_entities(
83
+ PlugwiseClimateEntity(coordinator, device_id)
84
+ for device_id in coordinator.new_devices
85
+ if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
86
+ )
 
 
 
 
 
87
 
88
  _add_entities()
89
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
104
  """Set up the Plugwise API."""
105
  super().__init__(coordinator, device_id)
106
  # Legacy format, kept as migrating existing unique IDs is not worth the risk
107
+ self._attr_unique_id = f"{device_id}-climate" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
108
+
109
  self._api = coordinator.api
110
  gateway_id: str = self._api.gateway_id
111
  self._gateway_data = coordinator.data[gateway_id]
112
  self._last_active_schedule = STATE_OFF
113
  self._location = device_id
114
+ if (location := self.device.get("location")) is not None:
115
  self._location = location
116
  self._previous_action_mode = HVACAction.HEATING.value
117
 
118
  # Determine supported features
119
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
120
+ if self._api.cooling_present and self._api.smile.name != "Adam":
 
 
 
121
  self._attr_supported_features = (
122
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
123
  )
 
125
  self._attr_supported_features |= (
126
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
127
  )
128
+ if presets := self.device.get("preset_modes"):
129
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
130
+ self._attr_preset_modes = presets
131
 
132
+ self._attr_min_temp = self.device["thermostat"]["lower_bound"]
133
+ self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
134
  # Ensure we don't drop below 0.1
135
  self._attr_target_temperature_step = max(
136
+ self.device["thermostat"]["resolution"], 0.1
137
  )
138
 
139
  @override
 
141
  """Run when entity about to be added."""
142
  await super().async_added_to_hass()
143
 
144
+ if extra_data := await self.async_get_last_extra_data():
145
+ plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
146
+ extra_data.as_dict()
147
+ )
148
+ self._last_active_schedule = (
149
+ plugwise_extra_data.last_active_schedule or STATE_OFF
150
+ )
151
  self._previous_action_mode = (
152
+ plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
153
  )
154
 
155
  @property
 
157
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
158
  """Return text specific state data to be restored."""
159
  return PlugwiseClimateExtraStoredData(
160
+ last_active_schedule=self._last_active_schedule,
161
+ previous_action_mode=self._previous_action_mode,
162
  )
163
 
164
  @property
165
  @override
166
  def native_current_temperature(self) -> float | None:
167
  """Return the current temperature."""
168
+ return self.device["sensors"].get("temperature")
169
 
170
  @property
171
  @override
172
+ def native_target_temperature(self) -> float:
173
  """Return the temperature we try to reach.
174
 
175
  Connected to the HVACMode combination of AUTO-HEAT.
176
  """
177
 
178
+ return self.device["thermostat"]["setpoint"]
179
 
180
  @property
181
  @override
182
+ def native_target_temperature_high(self) -> float:
183
  """Return the temperature we try to reach in case of cooling.
184
 
185
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
186
  """
187
+ return self.device["thermostat"]["setpoint_high"]
188
 
189
  @property
190
  @override
191
+ def native_target_temperature_low(self) -> float:
192
  """Return the heating temperature we try to reach in case of heating.
193
 
194
  Connected to the HVACMode combination AUTO-HEAT_COOL.
195
  """
196
+ return self.device["thermostat"]["setpoint_low"]
197
 
198
  @property
199
  @override
200
  def hvac_mode(self) -> HVACMode:
201
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
202
+ if (
203
+ mode := self.device.get("climate_mode")
204
+ ) is None or mode not in self.hvac_modes:
205
+ return HVACMode.HEAT
206
+ return HVACMode(mode)
 
 
 
 
 
 
207
 
208
  @property
209
  @override
210
  def hvac_modes(self) -> list[HVACMode]:
211
  """Return a list of available HVACModes."""
212
  hvac_modes: list[HVACMode] = []
213
+ if "regulation_modes" in self._gateway_data:
214
  hvac_modes.append(HVACMode.OFF)
215
 
216
+ if self.device["available_schedules"] != [STATE_OFF]:
217
  hvac_modes.append(HVACMode.AUTO)
218
 
219
  if self._api.cooling_present:
220
+ if "regulation_modes" in self._gateway_data:
221
+ if "heating" in self._gateway_data["regulation_modes"]:
222
  hvac_modes.append(HVACMode.HEAT)
223
+ if "cooling" in self._gateway_data["regulation_modes"]:
224
  hvac_modes.append(HVACMode.COOL)
225
  else:
226
  hvac_modes.append(HVACMode.HEAT_COOL)
 
231
 
232
  @property
233
  @override
234
+ def hvac_action(self) -> HVACAction:
235
  """Return the current running hvac operation if supported."""
236
  # Keep track of the previous hvac_action mode.
237
  # When no cooling available, _previous_action_mode is always heating
238
  if (
239
+ "regulation_modes" in self._gateway_data
240
+ and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
241
  ):
242
+ mode = self._gateway_data["select_regulation_mode"]
243
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
244
  self._previous_action_mode = mode
245
 
246
+ if (action := self.device.get("control_state")) is not None:
247
  return HVACAction(action)
248
 
249
  return HVACAction.IDLE
 
252
  @override
253
  def preset_mode(self) -> str | None:
254
  """Return the current preset mode."""
255
+ return self.device.get("active_preset")
256
 
257
  @plugwise_command
258
  @override
 
260
  """Set new target temperature."""
261
  data: dict[str, Any] = {}
262
  if ATTR_TEMPERATURE in kwargs:
263
+ data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
264
  if ATTR_TARGET_TEMP_HIGH in kwargs:
265
+ data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
266
  if ATTR_TARGET_TEMP_LOW in kwargs:
267
+ data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
 
 
268
 
269
  if mode := kwargs.get(ATTR_HVAC_MODE):
270
  await self.async_set_hvac_mode(mode)
 
272
  await self._api.set_temperature(self._location, data)
273
 
274
  def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
275
+ """Return the API regulation value for a manual HVAC mode, or None.
276
 
277
  The function inputs are limited to the HVACModes HEAT and COOL.
278
  """
 
286
  @override
287
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
288
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
 
289
  # Early exit if no mode change
290
  if hvac_mode == self.hvac_mode:
291
  return
 
305
  if hvac_mode == HVACMode.AUTO:
306
  _check_for_schedule(schedule_is_active, self._last_active_schedule)
307
  await self._api.set_schedule_state(
308
+ self._location,
309
+ desired_schedule,
310
+ STATE_ON,
311
  )
312
  await self._api.set_regulation_mode(self._previous_action_mode)
313
  return
 
315
  # Transition to manual mode
316
  if schedule_is_active:
317
  await self._api.set_schedule_state(
318
+ self._location,
319
+ current_schedule,
320
+ STATE_OFF,
321
  )
322
  self._last_active_schedule = current_schedule
323
  regulation = self._regulation_mode_for_hvac(hvac_mode)
 
327
  # Common - transition from auto = schedule off
328
  if self.hvac_mode == HVACMode.AUTO:
329
  await self._api.set_schedule_state(
330
+ self._location,
331
+ current_schedule,
332
+ STATE_OFF,
333
  )
334
  self._last_active_schedule = current_schedule
335
  return
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/config_flow.py RENAMED
@@ -1,8 +1,5 @@
1
  """Config flow for Plugwise integration."""
2
 
3
- # pylint: disable=home-assistant-config-flow-polling-field
4
-
5
- from copy import deepcopy
6
  import logging
7
  from typing import Any, Self, override
8
 
@@ -15,62 +12,38 @@
15
  ResponseError,
16
  UnsupportedDeviceError,
17
  )
 
18
 
19
- from homeassistant.config_entries import (
20
- SOURCE_USER,
21
- ConfigEntry,
22
- ConfigFlow,
23
- ConfigFlowResult,
24
- OptionsFlow,
25
- )
26
-
27
- # Upstream
28
  from homeassistant.const import (
29
  CONF_BASE,
30
  CONF_HOST,
31
  CONF_PASSWORD,
32
  CONF_PORT,
33
- CONF_SCAN_INTERVAL,
34
  CONF_USERNAME,
35
  )
36
-
37
- # Upstream
38
- from homeassistant.core import HomeAssistant, callback
39
- from homeassistant.helpers import config_validation as cv
40
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
41
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
42
- import probatio
43
 
44
  from .const import (
45
  ANNA_WITH_ADAM,
46
- CONF_REFRESH_INTERVAL, # pw-beta option
47
  DEFAULT_PORT,
48
- DEFAULT_UPDATE_INTERVAL,
49
  DEFAULT_USERNAME,
50
  DOMAIN,
51
  FLOW_SMILE,
52
  FLOW_STRETCH,
53
- INIT,
54
- P1_UPDATE_INTERVAL,
55
  SMILE,
56
  SMILE_OPEN_THERM,
57
  SMILE_THERMO,
58
  STRETCH,
59
  STRETCH_USERNAME,
60
- THERMOSTAT,
61
- VERSION,
62
  ZEROCONF_MAP,
63
  )
64
 
65
- # Upstream
66
- from .coordinator import PlugwiseDataUpdateCoordinator
67
-
68
- type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
69
-
70
  _LOGGER = logging.getLogger(__name__)
71
 
72
- # Upstream basically the whole file (excluding the pw-beta options)
73
-
74
  SMILE_RECONF_SCHEMA = probatio.Schema(
75
  {
76
  probatio.Required(CONF_HOST): str,
@@ -78,31 +51,21 @@
78
  )
79
 
80
 
81
- def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> probatio.Schema:
82
  """Generate base schema for gateways."""
83
- if not cf_input: # no discovery- or user-input available
84
- return probatio.Schema(
 
 
85
  {
86
  probatio.Required(CONF_HOST): str,
87
- probatio.Required(CONF_PASSWORD): str,
88
  probatio.Required(CONF_USERNAME, default=SMILE): probatio.In(
89
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
90
  ),
91
  }
92
  )
93
 
94
- if isinstance(cf_input, ZeroconfServiceInfo):
95
- return probatio.Schema({probatio.Required(CONF_PASSWORD): str})
96
-
97
- return probatio.Schema(
98
- {
99
- probatio.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
100
- probatio.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
101
- probatio.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): probatio.In(
102
- {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
103
- ),
104
- }
105
- )
106
 
107
 
108
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
@@ -152,10 +115,9 @@
152
  """Handle a config flow for Plugwise Smile."""
153
 
154
  VERSION = 1
155
- MINOR_VERSION = 1
156
 
157
  discovery_info: ZeroconfServiceInfo | None = None
158
- product: str = "Unknown Smile"
159
  _username: str = DEFAULT_USERNAME
160
 
161
  @override
@@ -165,21 +127,17 @@
165
  """Prepare configuration for a discovered Plugwise Smile."""
166
  self.discovery_info = discovery_info
167
  _properties = discovery_info.properties
168
- _version = _properties.get(VERSION, "n/a")
169
- self.product = _product = _properties.get("product", "Unknown Smile")
170
- unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
171
- if DEFAULT_USERNAME not in unique_id:
172
- self._username = STRETCH_USERNAME
173
 
 
174
  if config_entry := await self.async_set_unique_id(unique_id):
175
  try:
176
  await validate_input(
177
  self.hass,
178
  {
179
  CONF_HOST: discovery_info.host,
180
- CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
181
  CONF_PORT: discovery_info.port,
182
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
 
183
  },
184
  )
185
  except Exception: # noqa: BLE001
@@ -192,6 +150,12 @@
192
  }
193
  )
194
 
 
 
 
 
 
 
195
  # This is an Anna, but we already have config entries.
196
  # Assuming that the user has already configured Adam, aborting discovery.
197
  if self._async_current_entries() and _product == SMILE_THERMO:
@@ -201,20 +165,18 @@
201
  # In that case, we need to cancel the Anna flow, as the Adam should
202
  # be added.
203
  if self.hass.config_entries.flow.async_has_matching_flow(self):
204
- return self.async_abort(reason="anna_with_adam")
205
 
206
- _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
207
  self.context.update(
208
  {
209
  "title_placeholders": {"name": _name},
210
  "configuration_url": (
211
  f"http://{discovery_info.host}:{discovery_info.port}"
212
- )
213
  }
214
  )
215
  return await self.async_step_user()
216
 
217
-
218
  @override
219
  def is_matching(self, other_flow: Self) -> bool:
220
  """Return True if other_flow is matching this flow."""
@@ -228,7 +190,6 @@
228
 
229
  return False
230
 
231
-
232
  @override
233
  async def async_step_user(
234
  self, user_input: dict[str, Any] | None = None
@@ -246,19 +207,18 @@
246
  api, errors = await verify_connection(self.hass, user_input)
247
  if api:
248
  await self.async_set_unique_id(
249
- api.smile.hostname or api.gateway_id, raise_on_progress=False
 
250
  )
251
  self._abort_if_unique_id_configured()
252
  return self.async_create_entry(title=api.smile.name, data=user_input)
253
 
254
- configure_input = self.discovery_info or user_input
255
  return self.async_show_form(
256
  step_id=SOURCE_USER,
257
- data_schema=smile_user_schema(configure_input),
258
  errors=errors,
259
  )
260
 
261
-
262
  async def async_step_reconfigure(
263
  self, user_input: dict[str, Any] | None = None
264
  ) -> ConfigFlowResult:
@@ -268,7 +228,7 @@
268
  reconfigure_entry = self._get_reconfigure_entry()
269
 
270
  if user_input:
271
- # Redefine ingest existing username and password
272
  full_input = {
273
  CONF_HOST: user_input.get(CONF_HOST),
274
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
@@ -279,7 +239,8 @@
279
  api, errors = await verify_connection(self.hass, full_input)
280
  if api:
281
  await self.async_set_unique_id(
282
- api.smile.hostname or api.gateway_id, raise_on_progress=False
 
283
  )
284
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
285
  return self.async_update_reload_and_abort(
@@ -296,67 +257,3 @@
296
  description_placeholders={"title": reconfigure_entry.title},
297
  errors=errors,
298
  )
299
-
300
-
301
- @callback
302
- @override
303
- @staticmethod
304
- def async_get_options_flow(
305
- config_entry: PlugwiseConfigEntry,
306
- ) -> PlugwiseOptionsFlowHandler: # pw-beta options
307
- """Get the options flow for this handler."""
308
- return PlugwiseOptionsFlowHandler(config_entry)
309
-
310
-
311
- # pw-beta - change the scan-interval via CONFIGURE
312
- # pw-beta - change the frontend refresh interval via CONFIGURE
313
- class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
314
- """Plugwise option flow."""
315
-
316
- def __init__(self, config_entry: ConfigEntry) -> None:
317
- """Initialize options flow."""
318
- self.options = deepcopy(dict(config_entry.options))
319
-
320
- def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> probatio.Schema:
321
- interval = DEFAULT_UPDATE_INTERVAL
322
- if coordinator.api.smile.type == "power":
323
- interval = P1_UPDATE_INTERVAL
324
- schema = {
325
- probatio.Optional(
326
- CONF_SCAN_INTERVAL,
327
- default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
328
- ): probatio.All(cv.positive_int, probatio.Clamp(min=10)),
329
- } # pw-beta
330
-
331
- if coordinator.api.smile.type == THERMOSTAT:
332
- schema.update({
333
- probatio.Optional(
334
- CONF_REFRESH_INTERVAL,
335
- default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
336
- ): probatio.All(probatio.Coerce(float), probatio.Range(min=1.5, max=10.0)),
337
- }) # pw-beta
338
-
339
- return probatio.Schema(schema)
340
-
341
- async def async_step_none(
342
- self, user_input: dict[str, Any] | None = None
343
- ) -> ConfigFlowResult: # pragma: no cover
344
- """No options available."""
345
- if user_input is not None:
346
- # Apparently not possible to abort an options flow at the moment
347
- return self.async_create_entry(title="", data=self.options)
348
- return self.async_show_form(step_id="none")
349
-
350
- async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
351
- """Manage the Plugwise options."""
352
- if not self.config_entry.data.get(CONF_HOST):
353
- return await self.async_step_none(user_input) # pragma: no cover
354
-
355
- if user_input is not None:
356
- return self.async_create_entry(title="", data=user_input)
357
-
358
- coordinator = self.config_entry.runtime_data
359
- return self.async_show_form(
360
- step_id=INIT,
361
- data_schema=self._create_options_schema(coordinator)
362
- )
 
1
  """Config flow for Plugwise integration."""
2
 
 
 
 
3
  import logging
4
  from typing import Any, Self, override
5
 
 
12
  ResponseError,
13
  UnsupportedDeviceError,
14
  )
15
+ import probatio
16
 
17
+ from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
 
 
 
 
 
 
18
  from homeassistant.const import (
19
  CONF_BASE,
20
  CONF_HOST,
21
  CONF_PASSWORD,
22
  CONF_PORT,
 
23
  CONF_USERNAME,
24
  )
25
+ from homeassistant.core import HomeAssistant
 
 
 
26
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
27
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
 
28
 
29
  from .const import (
30
  ANNA_WITH_ADAM,
 
31
  DEFAULT_PORT,
 
32
  DEFAULT_USERNAME,
33
  DOMAIN,
34
  FLOW_SMILE,
35
  FLOW_STRETCH,
 
 
36
  SMILE,
37
  SMILE_OPEN_THERM,
38
  SMILE_THERMO,
39
  STRETCH,
40
  STRETCH_USERNAME,
41
+ UNKNOWN_SMILE,
 
42
  ZEROCONF_MAP,
43
  )
44
 
 
 
 
 
 
45
  _LOGGER = logging.getLogger(__name__)
46
 
 
 
47
  SMILE_RECONF_SCHEMA = probatio.Schema(
48
  {
49
  probatio.Required(CONF_HOST): str,
 
51
  )
52
 
53
 
54
+ def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> probatio.Schema:
55
  """Generate base schema for gateways."""
56
+ schema = probatio.Schema({probatio.Required(probatio.Secret(CONF_PASSWORD)): str})
57
+
58
+ if not discovery_info:
59
+ schema = schema.extend(
60
  {
61
  probatio.Required(CONF_HOST): str,
 
62
  probatio.Required(CONF_USERNAME, default=SMILE): probatio.In(
63
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
64
  ),
65
  }
66
  )
67
 
68
+ return schema
 
 
 
 
 
 
 
 
 
 
 
69
 
70
 
71
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
 
115
  """Handle a config flow for Plugwise Smile."""
116
 
117
  VERSION = 1
 
118
 
119
  discovery_info: ZeroconfServiceInfo | None = None
120
+ product: str = UNKNOWN_SMILE
121
  _username: str = DEFAULT_USERNAME
122
 
123
  @override
 
127
  """Prepare configuration for a discovered Plugwise Smile."""
128
  self.discovery_info = discovery_info
129
  _properties = discovery_info.properties
 
 
 
 
 
130
 
131
+ unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
132
  if config_entry := await self.async_set_unique_id(unique_id):
133
  try:
134
  await validate_input(
135
  self.hass,
136
  {
137
  CONF_HOST: discovery_info.host,
 
138
  CONF_PORT: discovery_info.port,
139
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
140
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
141
  },
142
  )
143
  except Exception: # noqa: BLE001
 
150
  }
151
  )
152
 
153
+ if DEFAULT_USERNAME not in unique_id:
154
+ self._username = STRETCH_USERNAME
155
+ self.product = _product = _properties.get("product", UNKNOWN_SMILE)
156
+ _version = _properties.get("version", "n/a")
157
+ _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
158
+
159
  # This is an Anna, but we already have config entries.
160
  # Assuming that the user has already configured Adam, aborting discovery.
161
  if self._async_current_entries() and _product == SMILE_THERMO:
 
165
  # In that case, we need to cancel the Anna flow, as the Adam should
166
  # be added.
167
  if self.hass.config_entries.flow.async_has_matching_flow(self):
168
+ return self.async_abort(reason=ANNA_WITH_ADAM)
169
 
 
170
  self.context.update(
171
  {
172
  "title_placeholders": {"name": _name},
173
  "configuration_url": (
174
  f"http://{discovery_info.host}:{discovery_info.port}"
175
+ ),
176
  }
177
  )
178
  return await self.async_step_user()
179
 
 
180
  @override
181
  def is_matching(self, other_flow: Self) -> bool:
182
  """Return True if other_flow is matching this flow."""
 
190
 
191
  return False
192
 
 
193
  @override
194
  async def async_step_user(
195
  self, user_input: dict[str, Any] | None = None
 
207
  api, errors = await verify_connection(self.hass, user_input)
208
  if api:
209
  await self.async_set_unique_id(
210
+ api.smile.hostname or api.gateway_id,
211
+ raise_on_progress=False,
212
  )
213
  self._abort_if_unique_id_configured()
214
  return self.async_create_entry(title=api.smile.name, data=user_input)
215
 
 
216
  return self.async_show_form(
217
  step_id=SOURCE_USER,
218
+ data_schema=smile_user_schema(self.discovery_info),
219
  errors=errors,
220
  )
221
 
 
222
  async def async_step_reconfigure(
223
  self, user_input: dict[str, Any] | None = None
224
  ) -> ConfigFlowResult:
 
228
  reconfigure_entry = self._get_reconfigure_entry()
229
 
230
  if user_input:
231
+ # Keep current username and password
232
  full_input = {
233
  CONF_HOST: user_input.get(CONF_HOST),
234
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
 
239
  api, errors = await verify_connection(self.hass, full_input)
240
  if api:
241
  await self.async_set_unique_id(
242
+ api.smile.hostname or api.gateway_id,
243
+ raise_on_progress=False,
244
  )
245
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
246
  return self.async_update_reload_and_abort(
 
257
  description_placeholders={"title": reconfigure_entry.title},
258
  errors=errors,
259
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/const.py RENAMED
@@ -6,160 +6,30 @@
6
 
7
  from homeassistant.const import Platform
8
 
9
- # Upstream basically the whole file excluding pw-beta options
10
-
11
  DOMAIN: Final = "plugwise"
12
 
13
  LOGGER = logging.getLogger(__package__)
14
 
 
15
  API: Final = "api"
16
- COORDINATOR: Final = "coordinator"
17
- CONFIG_ENTRY: Final = "config_entry" # pw-beta service
18
- CONF_HOMEKIT_EMULATION: Final = "homekit_emulation" # pw-beta options
19
- CONF_REFRESH_INTERVAL: Final = "refresh_interval" # pw-beta options
20
- CONF_MANUAL_PATH: Final = "Enter Manually"
21
  GATEWAY: Final = "gateway"
22
  LOCATION: Final = "location"
23
- MAC_ADDRESS: Final = "mac_address"
 
24
  REBOOT: Final = "reboot"
25
  SMILE: Final = "smile"
26
- STRETCH: Final = "stretch"
27
- STRETCH_USERNAME: Final = "stretch"
28
- SWITCH_GROUPS: Final[tuple[str, str]] = ("report", "switching")
29
- UNIQUE_IDS: Final = "unique_ids"
30
- WIFI_MAC_ADDRESS: Final = "wifi_mac_address"
31
- ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
32
-
33
- # Binary Sensor constants
34
- BINARY_SENSORS: Final = "binary_sensors"
35
- BATTERY_STATE: Final = "low_battery"
36
- COMPRESSOR_STATE: Final = "compressor_state"
37
- COOLING_ENABLED: Final = "cooling_enabled"
38
- COOLING_STATE: Final = "cooling_state"
39
- DHW_STATE: Final = "dhw_state"
40
- FLAME_STATE: Final = "flame_state"
41
- HEATING_STATE: Final = "heating_state"
42
- PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
43
- SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
44
-
45
- # Climate constants
46
- ACTIVE_PRESET: Final = "active_preset"
47
- CLIMATE_MODE: Final = "climate_mode"
48
- CONTROL_STATE: Final = "control_state"
49
- COOLING_PRESENT: Final ="cooling_present"
50
- DEV_CLASS: Final = "dev_class"
51
- NONE : Final = "None"
52
- TARGET_TEMP: Final = "setpoint"
53
- TARGET_TEMP_HIGH: Final = "setpoint_high"
54
- TARGET_TEMP_LOW: Final = "setpoint_low"
55
- THERMOSTAT: Final = "thermostat"
56
-
57
- # Config_flow constants
58
- ANNA_WITH_ADAM: Final = "anna_with_adam"
59
- CONTEXT: Final = "context"
60
- FLOW_ID: Final = "flow_id"
61
- FLOW_NET: Final = "Network: Smile/Stretch"
62
- FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
63
- FLOW_STRETCH: Final = "Stretch (Stretch)"
64
- FLOW_TYPE: Final = "flow_type"
65
- INIT: Final = "init"
66
- PRODUCT: Final = "product"
67
  SMILE_OPEN_THERM: Final = "smile_open_therm"
68
  SMILE_THERMO: Final = "smile_thermo"
69
- TITLE_PLACEHOLDERS: Final = "title_placeholders"
70
- VERSION: Final = "version"
71
-
72
- # Entity constants
73
- AVAILABLE: Final = "available"
74
- FIRMWARE: Final = "firmware"
75
- HARDWARE: Final = "hardware"
76
- MODEL: Final = "model"
77
- MODEL_ID: Final = "model_id"
78
- VENDOR: Final = "vendor"
79
-
80
- # Number constants
81
- BOILER_TEMP: Final = "boiler_temperature"
82
- LOWER_BOUND: Final = "lower_bound"
83
- RESOLUTION: Final = "resolution"
84
- TEMPERATURE_OFFSET: Final = "temperature_offset"
85
  UPPER_BOUND: Final = "upper_bound"
86
 
87
- # Sensor constants
88
- DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
89
- EL_CONSUMED: Final = "electricity_consumed"
90
- EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
91
- EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
92
- EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
93
- EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
94
- EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
95
- EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
96
- EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
97
- EL_CONS_POINT: Final = "electricity_consumed_point"
98
- EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
99
- EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
100
- EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
101
- EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
102
- EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
103
- EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
104
- EL_PRODUCED: Final = "electricity_produced"
105
- EL_PROD_INTERVAL: Final = "electricity_produced_interval"
106
- EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
107
- EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
108
- EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
109
- EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
110
- EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
111
- EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
112
- EL_PROD_POINT: Final = "electricity_produced_point"
113
- GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
114
- GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
115
- INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
116
- MOD_LEVEL: Final = "modulation_level"
117
- NET_EL_POINT: Final = "net_electricity_point"
118
- NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
119
- OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
120
- OUTDOOR_TEMP: Final = "outdoor_temperature"
121
- RETURN_TEMP: Final = "return_temperature"
122
- SENSORS: Final = "sensors"
123
- TEMP_DIFF: Final = "temperature_difference"
124
- VALVE_POS: Final = "valve_position"
125
- VOLTAGE_PH1: Final = "voltage_phase_one"
126
- VOLTAGE_PH2: Final = "voltage_phase_two"
127
- VOLTAGE_PH3: Final = "voltage_phase_three"
128
- WATER_TEMP: Final = "water_temperature"
129
- WATER_PRESSURE: Final = "water_pressure"
130
-
131
- # Select constants
132
- AVAILABLE_SCHEDULES: Final = "available_schedules"
133
- DHW_MODE: Final = "dhw_mode"
134
- DHW_MODES: Final = "dhw_modes"
135
- GATEWAY_MODES: Final = "gateway_modes"
136
- REGULATION_MODES: Final = "regulation_modes"
137
- ZONE_PROFILES: Final = "zone_profiles"
138
- SELECT_DHW_MODE: Final = "select_dhw_mode"
139
- SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
140
- SELECT_REGULATION_MODE: Final = "select_regulation_mode"
141
- SELECT_SCHEDULE: Final = "select_schedule"
142
- SELECT_ZONE_PROFILE: Final = "select_zone_profile"
143
-
144
- # Switch constants
145
- DHW_CM_SWITCH: Final = "dhw_cm_switch"
146
- LOCK: Final = "lock"
147
- MEMBERS: Final ="members"
148
- RELAY: Final = "relay"
149
- COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
150
- SWITCHES: Final = "switches"
151
-
152
- # Water_heater constants
153
- DHW_TEMP: Final = "dhw_temperature"
154
-
155
- # Default directives
156
- DEFAULT_PORT: Final[int] = 80
157
- DEFAULT_TIMEOUT: Final[int] = 30
158
- DEFAULT_UPDATE_INTERVAL: Final = timedelta(seconds=60)
159
- DEFAULT_USERNAME: Final = "smile"
160
- P1_UPDATE_INTERVAL: Final = timedelta(seconds=10)
161
-
162
- # --- Const for Plugwise Smile and Stretch
163
  PLATFORMS: Final[list[str]] = [
164
  Platform.BINARY_SENSOR,
165
  Platform.BUTTON,
@@ -170,10 +40,6 @@
170
  Platform.SWITCH,
171
  Platform.WATER_HEATER,
172
  ]
173
- SERVICE_DELETE: Final = "delete_notification"
174
- SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
175
-
176
- # Config_flow const:
177
  ZEROCONF_MAP: Final[dict[str, str]] = {
178
  "smile": "Smile P1",
179
  "smile_thermo": "Smile Anna",
@@ -183,46 +49,47 @@
183
 
184
  type BinarySensorType = Literal["dhw_state"]
185
 
186
- type PresetTemperatureType = Literal[
187
- "home_preset_temperature",
188
  "asleep_preset_temperature",
189
  "away_preset_temperature",
 
 
 
 
 
190
  "vacation_preset_temperature",
 
 
 
 
 
 
191
  "no_frost_preset_temperature",
 
192
  ]
193
  PRESET_TEMPERATURE_KEYS: Final[tuple[PresetTemperatureType, ...]] = get_args(
194
  PresetTemperatureType.__value__
195
  )
 
196
  type PresetCoolingTemperatureType = Literal[
197
- "home_preset_cooling_temperature",
198
  "asleep_preset_cooling_temperature",
199
  "away_preset_cooling_temperature",
200
- "vacation_preset_cooling_temperature",
201
  "no_frost_preset_cooling_temperature",
 
202
  ]
203
  PRESET_COOLING_TEMPERATURE_KEYS: Final[tuple[PresetCoolingTemperatureType, ...]] = (
204
  get_args(PresetCoolingTemperatureType.__value__)
205
  )
206
- type SetpointType = Literal["setpoint", "setpoint_low", "setpoint_high"]
207
- # mypy resolves GwEntityData.get() only when the key type is one flat Literal.
208
- type NumberDataType = Literal[
209
- "boiler_temperature",
210
- "dhw_temperature",
211
- "temperature_offset",
212
- "home_preset_temperature",
213
- "asleep_preset_temperature",
214
- "away_preset_temperature",
215
- "vacation_preset_temperature",
216
- "no_frost_preset_temperature",
217
- ]
218
  type NumberType = NumberDataType | PresetCoolingTemperatureType
 
219
  type SelectType = Literal[
220
  "dhw_mode",
221
  "select_dhw_mode",
222
  "select_gateway_mode",
223
  "select_regulation_mode",
224
  "select_schedule",
225
- "select_zone_profile"
226
  ]
227
  type SelectOptionsType = Literal[
228
  "available_schedules",
@@ -231,4 +98,33 @@
231
  "regulation_modes",
232
  "zone_profiles",
233
  ]
 
 
 
234
  type WaterHeaterType = Literal["dhw_temperature"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  from homeassistant.const import Platform
8
 
 
 
9
  DOMAIN: Final = "plugwise"
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
+ LOWER_BOUND: Final = "lower_bound"
23
+ PW_TYPE: Final = "plugwise_type"
24
  REBOOT: Final = "reboot"
25
  SMILE: Final = "smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  SMILE_OPEN_THERM: Final = "smile_open_therm"
27
  SMILE_THERMO: Final = "smile_thermo"
28
+ STRETCH: Final = "stretch"
29
+ STRETCH_USERNAME: Final = "stretch"
30
+ UNKNOWN_SMILE: Final = "Unknown Smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  UPPER_BOUND: Final = "upper_bound"
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  PLATFORMS: Final[list[str]] = [
34
  Platform.BINARY_SENSOR,
35
  Platform.BUTTON,
 
40
  Platform.SWITCH,
41
  Platform.WATER_HEATER,
42
  ]
 
 
 
 
43
  ZEROCONF_MAP: Final[dict[str, str]] = {
44
  "smile": "Smile P1",
45
  "smile_thermo": "Smile Anna",
 
49
 
50
  type BinarySensorType = Literal["dhw_state"]
51
 
52
+ type NumberDataType = Literal[
 
53
  "asleep_preset_temperature",
54
  "away_preset_temperature",
55
+ "boiler_temperature",
56
+ "dhw_temperature",
57
+ "home_preset_temperature",
58
+ "no_frost_preset_temperature",
59
+ "temperature_offset",
60
  "vacation_preset_temperature",
61
+ ]
62
+
63
+ type PresetTemperatureType = Literal[
64
+ "asleep_preset_temperature",
65
+ "away_preset_temperature",
66
+ "home_preset_temperature",
67
  "no_frost_preset_temperature",
68
+ "vacation_preset_temperature",
69
  ]
70
  PRESET_TEMPERATURE_KEYS: Final[tuple[PresetTemperatureType, ...]] = get_args(
71
  PresetTemperatureType.__value__
72
  )
73
+
74
  type PresetCoolingTemperatureType = Literal[
 
75
  "asleep_preset_cooling_temperature",
76
  "away_preset_cooling_temperature",
77
+ "home_preset_cooling_temperature",
78
  "no_frost_preset_cooling_temperature",
79
+ "vacation_preset_cooling_temperature",
80
  ]
81
  PRESET_COOLING_TEMPERATURE_KEYS: Final[tuple[PresetCoolingTemperatureType, ...]] = (
82
  get_args(PresetCoolingTemperatureType.__value__)
83
  )
 
 
 
 
 
 
 
 
 
 
 
 
84
  type NumberType = NumberDataType | PresetCoolingTemperatureType
85
+
86
  type SelectType = Literal[
87
  "dhw_mode",
88
  "select_dhw_mode",
89
  "select_gateway_mode",
90
  "select_regulation_mode",
91
  "select_schedule",
92
+ "select_zone_profile",
93
  ]
94
  type SelectOptionsType = Literal[
95
  "available_schedules",
 
98
  "regulation_modes",
99
  "zone_profiles",
100
  ]
101
+
102
+ type SetpointType = Literal["setpoint", "setpoint_high", "setpoint_low"]
103
+
104
  type WaterHeaterType = Literal["dhw_temperature"]
105
+
106
+ # Default directives
107
+ DEFAULT_MAX_TEMP: Final = 30
108
+ DEFAULT_MIN_TEMP: Final = 4
109
+ DEFAULT_PORT: Final = 80
110
+ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
111
+ DEFAULT_USERNAME: Final = "smile"
112
+ P1_UPDATE_INTERVAL = timedelta(seconds=10)
113
+
114
+ MASTER_THERMOSTATS: Final[list[str]] = [
115
+ "thermostat",
116
+ "thermostatic_radiator_valve",
117
+ "zone_thermometer",
118
+ "zone_thermostat",
119
+ ]
120
+
121
+ # Select constants
122
+ DHW_MODE: Final = "dhw_mode"
123
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
124
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
125
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
126
+ SELECT_SCHEDULE: Final = "select_schedule"
127
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
128
+
129
+ # Water_heater constants
130
+ DHW_TEMP: Final = "dhw_temperature"
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/coordinator.py RENAMED
@@ -1,8 +1,8 @@
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
- from datetime import timedelta
4
  from typing import override
5
 
 
6
  from plugwise import GwEntityData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
@@ -15,29 +15,21 @@
15
  )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
- from homeassistant.const import (
19
- CONF_HOST,
20
- CONF_PASSWORD,
21
- CONF_PORT,
22
- CONF_SCAN_INTERVAL, # pw-beta options
23
- CONF_USERNAME,
24
- )
25
  from homeassistant.core import HomeAssistant
26
  from homeassistant.exceptions import ConfigEntryError
27
  from homeassistant.helpers import device_registry as dr
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
  from homeassistant.helpers.debounce import Debouncer
30
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
31
- from packaging.version import Version
32
 
33
  from .const import (
 
34
  DEFAULT_UPDATE_INTERVAL,
35
- DEV_CLASS,
36
  DOMAIN,
37
- FIRMWARE,
38
  LOGGER,
39
  P1_UPDATE_INTERVAL,
40
- SWITCH_GROUPS,
41
  )
42
 
43
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
@@ -48,36 +40,29 @@
48
 
49
  config_entry: PlugwiseConfigEntry
50
 
51
- def __init__(
52
- self,
53
- hass: HomeAssistant,
54
- cooldown: float,
55
- config_entry: PlugwiseConfigEntry,
56
- ) -> None: # pw-beta cooldown
57
  """Initialize the coordinator."""
58
  super().__init__(
59
  hass,
60
  LOGGER,
61
  config_entry=config_entry,
62
  name=DOMAIN,
63
- # Core directly updates from const's DEFAULT_SCAN_INTERVAL
64
- # Upstream check correct progress for adjusting
65
  update_interval=DEFAULT_UPDATE_INTERVAL,
66
  # Don't refresh immediately, give the device time to process
67
  # the change in state before we query it.
68
  request_refresh_debouncer=Debouncer(
69
  hass,
70
  LOGGER,
71
- cooldown=cooldown,
72
  immediate=False,
73
  ),
74
  )
75
 
76
  self.api = Smile(
77
  host=self.config_entry.data[CONF_HOST],
 
78
  password=self.config_entry.data[CONF_PASSWORD],
79
- port=self.config_entry.data[CONF_PORT],
80
- username=self.config_entry.data[CONF_USERNAME],
81
  websession=async_get_clientsession(hass, verify_ssl=False),
82
  )
83
  self._connected: bool = False
@@ -93,15 +78,8 @@
93
  """
94
  version = await self.api.connect()
95
  self._connected = isinstance(version, Version)
96
- if self._connected:
97
- if self.api.smile.type == "power":
98
- self.update_interval = P1_UPDATE_INTERVAL
99
- if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
100
- self.update_interval = timedelta(
101
- seconds=int(custom_time)
102
- ) # pragma: no cover # pw-beta options
103
-
104
- LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
105
 
106
  @override
107
  async def _async_setup(self) -> None:
@@ -155,27 +133,25 @@
155
  translation_key="unsupported_firmware",
156
  ) from err
157
 
158
- LOGGER.debug("%s data: %s", self.api.smile.name, data)
159
  self._add_remove_devices(data)
160
  self._update_device_firmware(data)
161
  return data
162
 
163
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
164
  """Add new Plugwise devices, remove non-existing devices."""
165
- # Block switch-groups, use HA group helper instead to create switch-groups
166
- for device_id, device in data.copy().items():
167
- if device.get(DEV_CLASS) in SWITCH_GROUPS:
168
- data.pop(device_id)
169
-
170
- # Collect new or removed devices,
171
- # 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
172
- # this is required for the initialization of the available platform entities.
173
  set_of_data = set(data)
 
 
 
 
 
174
  self.new_devices = set_of_data - self._current_devices
175
  for device_id in self.new_devices:
176
- self._firmware_list.setdefault(device_id, data[device_id].get(FIRMWARE))
177
 
178
- current_devices = self._stored_devices if not self._current_devices else self._current_devices
 
 
179
  self._current_devices = set_of_data
180
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
181
  self._remove_devices(removed_devices)
@@ -199,15 +175,14 @@
199
 
200
  self._firmware_list.pop(device_id, None)
201
 
202
-
203
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
204
  """Detect firmware changes and update the device registry."""
205
  for device_id, device in data.items():
206
  # Only update firmware when the key is present and not None, to avoid
207
  # wiping stored firmware on partial or transient updates.
208
- if FIRMWARE not in device:
209
  continue
210
- new_firmware = device.get(FIRMWARE)
211
  if new_firmware is None:
212
  continue
213
  if (
 
1
  """DataUpdateCoordinator for Plugwise."""
2
 
 
3
  from typing import override
4
 
5
+ from packaging.version import Version
6
  from plugwise import GwEntityData, Smile
7
  from plugwise.exceptions import (
8
  ConnectionFailedError,
 
15
  )
16
 
17
  from homeassistant.config_entries import ConfigEntry
18
+ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
19
  from homeassistant.core import HomeAssistant
20
  from homeassistant.exceptions import ConfigEntryError
21
  from homeassistant.helpers import device_registry as dr
22
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
23
  from homeassistant.helpers.debounce import Debouncer
24
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
25
 
26
  from .const import (
27
+ DEFAULT_PORT,
28
  DEFAULT_UPDATE_INTERVAL,
29
+ DEFAULT_USERNAME,
30
  DOMAIN,
 
31
  LOGGER,
32
  P1_UPDATE_INTERVAL,
 
33
  )
34
 
35
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
 
40
 
41
  config_entry: PlugwiseConfigEntry
42
 
43
+ def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
44
  """Initialize the coordinator."""
45
  super().__init__(
46
  hass,
47
  LOGGER,
48
  config_entry=config_entry,
49
  name=DOMAIN,
 
 
50
  update_interval=DEFAULT_UPDATE_INTERVAL,
51
  # Don't refresh immediately, give the device time to process
52
  # the change in state before we query it.
53
  request_refresh_debouncer=Debouncer(
54
  hass,
55
  LOGGER,
56
+ cooldown=1.5,
57
  immediate=False,
58
  ),
59
  )
60
 
61
  self.api = Smile(
62
  host=self.config_entry.data[CONF_HOST],
63
+ username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
64
  password=self.config_entry.data[CONF_PASSWORD],
65
+ port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
66
  websession=async_get_clientsession(hass, verify_ssl=False),
67
  )
68
  self._connected: bool = False
 
78
  """
79
  version = await self.api.connect()
80
  self._connected = isinstance(version, Version)
81
+ if self._connected and self.api.smile.type == "power":
82
+ self.update_interval = P1_UPDATE_INTERVAL
 
 
 
 
 
 
 
83
 
84
  @override
85
  async def _async_setup(self) -> None:
 
133
  translation_key="unsupported_firmware",
134
  ) from err
135
 
 
136
  self._add_remove_devices(data)
137
  self._update_device_firmware(data)
138
  return data
139
 
140
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
141
  """Add new Plugwise devices, remove non-existing devices."""
 
 
 
 
 
 
 
 
142
  set_of_data = set(data)
143
+ # Check for new or removed devices,
144
+ # 'new_devices' contains all devices present in 'data'
145
+ # at init ('self._current_devices' is empty) this is
146
+ # required for the proper initialization of all the
147
+ # present platform entities.
148
  self.new_devices = set_of_data - self._current_devices
149
  for device_id in self.new_devices:
150
+ self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
151
 
152
+ current_devices = (
153
+ self._stored_devices if not self._current_devices else self._current_devices
154
+ )
155
  self._current_devices = set_of_data
156
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
157
  self._remove_devices(removed_devices)
 
175
 
176
  self._firmware_list.pop(device_id, None)
177
 
 
178
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
179
  """Detect firmware changes and update the device registry."""
180
  for device_id, device in data.items():
181
  # Only update firmware when the key is present and not None, to avoid
182
  # wiping stored firmware on partial or transient updates.
183
+ if "firmware" not in device:
184
  continue
185
+ new_firmware = device.get("firmware")
186
  if new_firmware is None:
187
  continue
188
  if (
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/entity.py RENAMED
@@ -2,7 +2,7 @@
2
 
3
  from typing import override
4
 
5
- from plugwise.constants import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
@@ -13,20 +13,7 @@
13
  )
14
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
15
 
16
- from .const import (
17
- AVAILABLE,
18
- DOMAIN,
19
- FIRMWARE,
20
- HARDWARE,
21
- MAC_ADDRESS,
22
- MODEL,
23
- MODEL_ID,
24
- VENDOR,
25
- WIFI_MAC_ADDRESS,
26
- ZIGBEE_MAC_ADDRESS,
27
- )
28
-
29
- # Upstream consts
30
  from .coordinator import PlugwiseDataUpdateCoordinator
31
 
32
 
@@ -59,12 +46,12 @@
59
  connections = {
60
  (CONNECTION_NETWORK_MAC, mac_address)
61
  for mac_address in (
62
- self.device.get(MAC_ADDRESS),
63
- self.device.get(WIFI_MAC_ADDRESS),
64
  )
65
  if mac_address is not None
66
  }
67
- if zigbee_mac := self.device.get(ZIGBEE_MAC_ADDRESS):
68
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
69
 
70
  # Set base device info
@@ -72,25 +59,21 @@
72
  configuration_url=configuration_url,
73
  identifiers={(DOMAIN, device_id)},
74
  connections=connections,
75
- manufacturer=self.device.get(VENDOR),
76
- model=self.device.get(MODEL),
77
- model_id=self.device.get(MODEL_ID),
78
  name=api.smile.name,
79
- sw_version=self.device.get(FIRMWARE),
80
- hw_version=self.device.get(HARDWARE),
81
  )
82
 
83
  # Add extra info if not the gateway device
84
  if device_id != gateway_id:
85
- self._attr_device_info.update(
86
- {
87
- ATTR_NAME: self.device.get(ATTR_NAME),
88
- "via_device_id": async_get_device_id_by_identifier(
89
- coordinator.hass,
90
- (DOMAIN, gateway_id),
91
- config_entry_id=entry.entry_id,
92
- ),
93
- }
94
  )
95
 
96
  @property
@@ -98,10 +81,8 @@
98
  def available(self) -> bool:
99
  """Return if entity is available."""
100
  return (
101
- # Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
102
- # Upstream: do not provide their availability-status!
103
  self._dev_id in self.coordinator.data
104
- and (self.device.get(AVAILABLE, True) is True)
105
  and super().available
106
  )
107
 
 
2
 
3
  from typing import override
4
 
5
+ from plugwise import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
 
13
  )
14
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
15
 
16
+ from .const import AVAILABLE, DOMAIN
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  from .coordinator import PlugwiseDataUpdateCoordinator
18
 
19
 
 
46
  connections = {
47
  (CONNECTION_NETWORK_MAC, mac_address)
48
  for mac_address in (
49
+ self.device.get("mac_address"),
50
+ self.device.get("wifi_mac_address"),
51
  )
52
  if mac_address is not None
53
  }
54
+ if zigbee_mac := self.device.get("zigbee_mac_address"):
55
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
56
 
57
  # Set base device info
 
59
  configuration_url=configuration_url,
60
  identifiers={(DOMAIN, device_id)},
61
  connections=connections,
62
+ manufacturer=self.device.get("vendor"),
63
+ model=self.device.get("model"),
64
+ model_id=self.device.get("model_id"),
65
  name=api.smile.name,
66
+ sw_version=self.device.get("firmware"),
67
+ hw_version=self.device.get("hardware"),
68
  )
69
 
70
  # Add extra info if not the gateway device
71
  if device_id != gateway_id:
72
+ self._attr_device_info.name = self.device.get(ATTR_NAME)
73
+ self._attr_device_info.via_device_id = async_get_device_id_by_identifier(
74
+ coordinator.hass,
75
+ (DOMAIN, gateway_id),
76
+ config_entry_id=entry.entry_id,
 
 
 
 
77
  )
78
 
79
  @property
 
81
  def available(self) -> bool:
82
  """Return if entity is available."""
83
  return (
 
 
84
  self._dev_id in self.coordinator.data
85
+ and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
86
  and super().available
87
  )
88
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/icons.json RENAMED
@@ -152,8 +152,5 @@
152
  "default": "mdi:lock"
153
  }
154
  }
155
- },
156
- "services": {
157
- "delete_notification": "mdi:trash-can"
158
  }
159
  }
 
152
  "default": "mdi:lock"
153
  }
154
  }
 
 
 
155
  }
156
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/manifest.json RENAMED
@@ -1,14 +1,15 @@
1
  {
2
  "domain": "plugwise",
3
- "name": "Plugwise Beta",
 
4
  "codeowners": ["@CoMPaTech", "@bouwew"],
5
  "config_flow": true,
6
  "connectivity": ["network"],
7
- "documentation": "https://github.com/plugwise/plugwise-beta",
8
  "integration_type": "hub",
9
  "iot_class": "local_polling",
10
  "loggers": ["plugwise"],
11
- "requirements": ["plugwise>=1.14.7"],
12
- "version": "0.66.0",
13
  "zeroconf": ["_plugwise._tcp.local."]
14
  }
 
1
  {
2
  "domain": "plugwise",
3
+ "name": "Plugwise",
4
+ "categories": ["climate.thermostats_heating", "energy.meters_monitoring"],
5
  "codeowners": ["@CoMPaTech", "@bouwew"],
6
  "config_flow": true,
7
  "connectivity": ["network"],
8
+ "documentation": "https://www.home-assistant.io/integrations/plugwise",
9
  "integration_type": "hub",
10
  "iot_class": "local_polling",
11
  "loggers": ["plugwise"],
12
+ "quality_scale": "platinum",
13
+ "requirements": ["plugwise==1.16.0"],
14
  "zeroconf": ["_plugwise._tcp.local."]
15
  }
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/number.py RENAMED
@@ -11,29 +11,21 @@
11
  NumberEntityDescription,
12
  NumberMode,
13
  )
14
- from homeassistant.const import EntityCategory, UnitOfTemperature
15
  from homeassistant.core import HomeAssistant, callback
 
16
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
18
  from .const import (
19
- BOILER_TEMP,
20
- DHW_TEMP,
21
- LOGGER,
22
- LOWER_BOUND,
23
  PRESET_COOLING_TEMPERATURE_KEYS,
24
  PRESET_TEMPERATURE_KEYS,
25
- RESOLUTION,
26
- TEMPERATURE_OFFSET,
27
- UPPER_BOUND,
28
  NumberDataType,
29
  NumberType,
30
  SetpointType,
31
  )
32
-
33
- # Upstream consts
34
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
35
  from .entity import PlugwiseEntity
36
- from .util import plugwise_command
37
 
38
  PARALLEL_UPDATES = 0
39
 
@@ -47,28 +39,27 @@
47
  value_keys: tuple[SetpointType, ...] = ("setpoint",)
48
 
49
 
50
- # Upstream + is there a reason we didn't rename this one prefixed?
51
  NUMBER_TYPES = (
52
  PlugwiseNumberEntityDescription(
53
- key=BOILER_TEMP,
54
- data_key=BOILER_TEMP,
55
  translation_key="maximum_boiler_temperature",
56
  device_class=NumberDeviceClass.TEMPERATURE,
57
  entity_category=EntityCategory.CONFIG,
58
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
59
  ),
60
  PlugwiseNumberEntityDescription(
61
- key=DHW_TEMP,
62
- data_key=DHW_TEMP,
63
  translation_key="max_dhw_temperature",
64
  device_class=NumberDeviceClass.TEMPERATURE,
65
  entity_category=EntityCategory.CONFIG,
66
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
67
  ),
68
  PlugwiseNumberEntityDescription(
69
- key=TEMPERATURE_OFFSET,
70
- data_key=TEMPERATURE_OFFSET,
71
- translation_key=TEMPERATURE_OFFSET,
72
  device_class=NumberDeviceClass.TEMPERATURE,
73
  entity_category=EntityCategory.CONFIG,
74
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -115,9 +106,9 @@
115
  entry: PlugwiseConfigEntry,
116
  async_add_entities: AddConfigEntryEntitiesCallback,
117
  ) -> None:
118
- """Set up Plugwise number platform from a config entry."""
119
- # Upstream above to adhere to standard used
120
  coordinator = entry.runtime_data
 
121
 
122
  @callback
123
  def _add_entities() -> None:
@@ -125,26 +116,24 @@
125
  if not coordinator.new_devices:
126
  return
127
 
128
- # Upstream consts
129
- # async_add_entities(
130
- # PlugwiseNumberEntity(coordinator, device_id, description)
131
- # for device_id in coordinator.new_devices
132
- # for description in NUMBER_TYPES
133
- # if description.key in coordinator.data.devices[device_id]
134
- # )
135
-
136
- # pw-beta alternative for debugging
137
- entities: list[PlugwiseNumberEntity] = []
138
  for device_id in coordinator.new_devices:
139
- device = coordinator.data[device_id]
140
  for description in NUMBER_TYPES:
141
- if _value_key(description, device) is not None:
142
- entities.append(
143
- PlugwiseNumberEntity(coordinator, device_id, description)
144
- )
145
- LOGGER.debug(
146
- "Add %s %s number", device["name"], description.translation_key
147
- )
 
 
 
 
 
 
 
 
148
 
149
  async_add_entities(entities)
150
 
@@ -165,16 +154,18 @@
165
  ) -> None:
166
  """Initiate Plugwise Number."""
167
  super().__init__(coordinator, device_id)
168
- self.device_id = device_id
169
- self.entity_description = description
170
- self._attr_unique_id = f"{device_id}-{description.key}"
171
  self._attr_mode = NumberMode.BOX
172
  self._data_key = description.data_key
173
  ctrl = self.device.get(self._data_key, {})
174
- self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0) # Upstream const
175
- self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0) # Upstream const
 
 
 
176
 
177
- native_step = ctrl.get(RESOLUTION, 0.5) # Upstream const
 
 
178
  self._attr_native_step = native_step
179
 
180
  @property
@@ -201,6 +192,3 @@
201
  await self.coordinator.api.set_number(
202
  self.device_id, self.entity_description.key, value
203
  )
204
- LOGGER.debug(
205
- "Setting %s to %s was successful", self.entity_description.key, value
206
- )
 
11
  NumberEntityDescription,
12
  NumberMode,
13
  )
14
+ from homeassistant.const import EntityCategory, Platform, UnitOfTemperature
15
  from homeassistant.core import HomeAssistant, callback
16
+ from homeassistant.helpers import entity_registry as er
17
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
18
 
19
  from .const import (
 
 
 
 
20
  PRESET_COOLING_TEMPERATURE_KEYS,
21
  PRESET_TEMPERATURE_KEYS,
 
 
 
22
  NumberDataType,
23
  NumberType,
24
  SetpointType,
25
  )
 
 
26
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
27
  from .entity import PlugwiseEntity
28
+ from .util import deprecate_entity, plugwise_command
29
 
30
  PARALLEL_UPDATES = 0
31
 
 
39
  value_keys: tuple[SetpointType, ...] = ("setpoint",)
40
 
41
 
 
42
  NUMBER_TYPES = (
43
  PlugwiseNumberEntityDescription(
44
+ key="boiler_temperature",
45
+ data_key="boiler_temperature",
46
  translation_key="maximum_boiler_temperature",
47
  device_class=NumberDeviceClass.TEMPERATURE,
48
  entity_category=EntityCategory.CONFIG,
49
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
50
  ),
51
  PlugwiseNumberEntityDescription(
52
+ key="dhw_temperature",
53
+ data_key="dhw_temperature",
54
  translation_key="max_dhw_temperature",
55
  device_class=NumberDeviceClass.TEMPERATURE,
56
  entity_category=EntityCategory.CONFIG,
57
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
58
  ),
59
  PlugwiseNumberEntityDescription(
60
+ key="temperature_offset",
61
+ data_key="temperature_offset",
62
+ translation_key="temperature_offset",
63
  device_class=NumberDeviceClass.TEMPERATURE,
64
  entity_category=EntityCategory.CONFIG,
65
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 
106
  entry: PlugwiseConfigEntry,
107
  async_add_entities: AddConfigEntryEntitiesCallback,
108
  ) -> None:
109
+ """Set up Plugwise number platform."""
 
110
  coordinator = entry.runtime_data
111
+ entity_registry = er.async_get(hass)
112
 
113
  @callback
114
  def _add_entities() -> None:
 
116
  if not coordinator.new_devices:
117
  return
118
 
119
+ entities = []
 
 
 
 
 
 
 
 
 
120
  for device_id in coordinator.new_devices:
 
121
  for description in NUMBER_TYPES:
122
+ if _value_key(description, coordinator.data[device_id]) is None:
123
+ continue
124
+ if description.key == "dhw_temperature" and not deprecate_entity(
125
+ hass,
126
+ entity_registry,
127
+ async_on_unload=entry.async_on_unload,
128
+ platform_domain=Platform.NUMBER,
129
+ entity_unique_id=f"{device_id}-{description.translation_key}",
130
+ issue_id=f"deprecated_dhw_temperature_{device_id}",
131
+ translation_key="deprecated_dhw_temperature",
132
+ ):
133
+ continue
134
+ entities.append(
135
+ PlugwiseNumberEntity(coordinator, device_id, description)
136
+ )
137
 
138
  async_add_entities(entities)
139
 
 
154
  ) -> None:
155
  """Initiate Plugwise Number."""
156
  super().__init__(coordinator, device_id)
 
 
 
157
  self._attr_mode = NumberMode.BOX
158
  self._data_key = description.data_key
159
  ctrl = self.device.get(self._data_key, {})
160
+ self._attr_native_max_value = ctrl.get("upper_bound", 100.0)
161
+ self._attr_native_min_value = ctrl.get("lower_bound", 0.0)
162
+ self._attr_unique_id = f"{device_id}-{description.translation_key}"
163
+ self.device_id = device_id
164
+ self.entity_description = description
165
 
166
+ native_step = ctrl["resolution"]
167
+ if description.key != "temperature_offset":
168
+ native_step = max(native_step, 0.5)
169
  self._attr_native_step = native_step
170
 
171
  @property
 
192
  await self.coordinator.api.set_number(
193
  self.device_id, self.entity_description.key, value
194
  )
 
 
 
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/select.py RENAMED
@@ -12,24 +12,16 @@
12
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
13
 
14
  from .const import (
15
- AVAILABLE_SCHEDULES,
16
  DHW_MODE,
17
- DHW_MODES,
18
- GATEWAY_MODES,
19
  LOCATION,
20
- LOGGER,
21
- REGULATION_MODES,
22
  SELECT_DHW_MODE,
23
  SELECT_GATEWAY_MODE,
24
  SELECT_REGULATION_MODE,
25
  SELECT_SCHEDULE,
26
  SELECT_ZONE_PROFILE,
27
- ZONE_PROFILES,
28
  SelectOptionsType,
29
  SelectType,
30
  )
31
-
32
- # Upstream consts
33
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
34
  from .entity import PlugwiseEntity
35
  from .util import plugwise_command
@@ -51,7 +43,7 @@
51
  key=DHW_MODE,
52
  translation_key=SELECT_DHW_MODE,
53
  entity_category=EntityCategory.CONFIG,
54
- options_key=DHW_MODES,
55
  set_value_fn=lambda api, key, appl_id, option, length: api.set_dhw_mode(
56
  key, appl_id, option, length
57
  ),
@@ -60,7 +52,7 @@
60
  key=SELECT_DHW_MODE,
61
  translation_key=SELECT_DHW_MODE,
62
  entity_category=EntityCategory.CONFIG,
63
- options_key=DHW_MODES,
64
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
65
  key, appl_or_loc_id, option, state
66
  ),
@@ -69,7 +61,7 @@
69
  key=SELECT_GATEWAY_MODE,
70
  translation_key=SELECT_GATEWAY_MODE,
71
  entity_category=EntityCategory.CONFIG,
72
- options_key=GATEWAY_MODES,
73
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
74
  key, appl_or_loc_id, option, state
75
  ),
@@ -78,7 +70,7 @@
78
  key=SELECT_REGULATION_MODE,
79
  translation_key=SELECT_REGULATION_MODE,
80
  entity_category=EntityCategory.CONFIG,
81
- options_key=REGULATION_MODES,
82
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
83
  key, appl_or_loc_id, option, state
84
  ),
@@ -86,7 +78,7 @@
86
  PlugwiseSelectEntityDescription(
87
  key=SELECT_SCHEDULE,
88
  translation_key=SELECT_SCHEDULE,
89
- options_key=AVAILABLE_SCHEDULES,
90
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
91
  key, appl_or_loc_id, option, state
92
  ),
@@ -95,7 +87,7 @@
95
  key=SELECT_ZONE_PROFILE,
96
  translation_key=SELECT_ZONE_PROFILE,
97
  entity_category=EntityCategory.CONFIG,
98
- options_key=ZONE_PROFILES,
99
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
100
  key, appl_or_loc_id, option, state
101
  ),
@@ -108,38 +100,21 @@
108
  entry: PlugwiseConfigEntry,
109
  async_add_entities: AddConfigEntryEntitiesCallback,
110
  ) -> None:
111
- """Set up Plugwise selector from a config entry."""
112
  coordinator = entry.runtime_data
113
 
114
  @callback
115
  def _add_entities() -> None:
116
- """Add Entities."""
117
  if not coordinator.new_devices:
118
  return
119
 
120
- # Upstream consts
121
- # async_add_entities(
122
- # PlugwiseSelectEntity(coordinator, device_id, description)
123
- # for device_id in coordinator.new_devices
124
- # for description in SELECT_TYPES
125
- # if description.options_key in coordinator.data.devices[device_id]
126
- # )
127
- # pw-beta alternative for debugging
128
- entities: list[PlugwiseSelectEntity] = []
129
- for device_id in coordinator.new_devices:
130
- device = coordinator.data[device_id]
131
- for description in SELECT_TYPES:
132
- if device.get(description.key):
133
- entities.append(
134
- PlugwiseSelectEntity(coordinator, device_id, description)
135
- )
136
- LOGGER.debug(
137
- "Add %s %s selector",
138
- device["name"],
139
- description.translation_key,
140
- )
141
-
142
- async_add_entities(entities)
143
 
144
  _add_entities()
145
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
12
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
13
 
14
  from .const import (
 
15
  DHW_MODE,
 
 
16
  LOCATION,
 
 
17
  SELECT_DHW_MODE,
18
  SELECT_GATEWAY_MODE,
19
  SELECT_REGULATION_MODE,
20
  SELECT_SCHEDULE,
21
  SELECT_ZONE_PROFILE,
 
22
  SelectOptionsType,
23
  SelectType,
24
  )
 
 
25
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
26
  from .entity import PlugwiseEntity
27
  from .util import plugwise_command
 
43
  key=DHW_MODE,
44
  translation_key=SELECT_DHW_MODE,
45
  entity_category=EntityCategory.CONFIG,
46
+ options_key="dhw_modes",
47
  set_value_fn=lambda api, key, appl_id, option, length: api.set_dhw_mode(
48
  key, appl_id, option, length
49
  ),
 
52
  key=SELECT_DHW_MODE,
53
  translation_key=SELECT_DHW_MODE,
54
  entity_category=EntityCategory.CONFIG,
55
+ options_key="dhw_modes",
56
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
57
  key, appl_or_loc_id, option, state
58
  ),
 
61
  key=SELECT_GATEWAY_MODE,
62
  translation_key=SELECT_GATEWAY_MODE,
63
  entity_category=EntityCategory.CONFIG,
64
+ options_key="gateway_modes",
65
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
66
  key, appl_or_loc_id, option, state
67
  ),
 
70
  key=SELECT_REGULATION_MODE,
71
  translation_key=SELECT_REGULATION_MODE,
72
  entity_category=EntityCategory.CONFIG,
73
+ options_key="regulation_modes",
74
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
75
  key, appl_or_loc_id, option, state
76
  ),
 
78
  PlugwiseSelectEntityDescription(
79
  key=SELECT_SCHEDULE,
80
  translation_key=SELECT_SCHEDULE,
81
+ options_key="available_schedules",
82
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
83
  key, appl_or_loc_id, option, state
84
  ),
 
87
  key=SELECT_ZONE_PROFILE,
88
  translation_key=SELECT_ZONE_PROFILE,
89
  entity_category=EntityCategory.CONFIG,
90
+ options_key="zone_profiles",
91
  set_value_fn=lambda api, key, appl_or_loc_id, option, state: api.set_select(
92
  key, appl_or_loc_id, option, state
93
  ),
 
100
  entry: PlugwiseConfigEntry,
101
  async_add_entities: AddConfigEntryEntitiesCallback,
102
  ) -> None:
103
+ """Set up the Smile selector from a config entry."""
104
  coordinator = entry.runtime_data
105
 
106
  @callback
107
  def _add_entities() -> None:
108
+ """Add entities."""
109
  if not coordinator.new_devices:
110
  return
111
 
112
+ async_add_entities(
113
+ PlugwiseSelectEntity(coordinator, device_id, description)
114
+ for device_id in coordinator.new_devices
115
+ for description in SELECT_TYPES
116
+ if coordinator.data[device_id].get(description.key)
117
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  _add_entities()
120
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/sensor.py RENAMED
@@ -12,7 +12,6 @@
12
  SensorStateClass,
13
  )
14
  from homeassistant.const import (
15
- ATTR_TEMPERATURE, # Upstream
16
  LIGHT_LUX,
17
  PERCENTAGE,
18
  EntityCategory,
@@ -27,57 +26,6 @@
27
  from homeassistant.core import HomeAssistant, callback
28
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
29
 
30
- from .const import (
31
- DHW_SETPOINT,
32
- DHW_TEMP,
33
- EL_CONS_INTERVAL,
34
- EL_CONS_OP_CUMULATIVE,
35
- EL_CONS_OP_INTERVAL,
36
- EL_CONS_OP_POINT,
37
- EL_CONS_P_CUMULATIVE,
38
- EL_CONS_P_INTERVAL,
39
- EL_CONS_P_POINT,
40
- EL_CONS_POINT,
41
- EL_CONSUMED,
42
- EL_PH1_CONSUMED,
43
- EL_PH1_PRODUCED,
44
- EL_PH2_CONSUMED,
45
- EL_PH2_PRODUCED,
46
- EL_PH3_CONSUMED,
47
- EL_PH3_PRODUCED,
48
- EL_PROD_INTERVAL,
49
- EL_PROD_OP_CUMULATIVE,
50
- EL_PROD_OP_INTERVAL,
51
- EL_PROD_OP_POINT,
52
- EL_PROD_P_CUMULATIVE,
53
- EL_PROD_P_INTERVAL,
54
- EL_PROD_P_POINT,
55
- EL_PROD_POINT,
56
- EL_PRODUCED,
57
- GAS_CONS_CUMULATIVE,
58
- GAS_CONS_INTERVAL,
59
- INTENDED_BOILER_TEMP,
60
- LOGGER, # pw-beta
61
- MOD_LEVEL,
62
- NET_EL_CUMULATIVE,
63
- NET_EL_POINT,
64
- OUTDOOR_AIR_TEMP,
65
- OUTDOOR_TEMP,
66
- RETURN_TEMP,
67
- SENSORS,
68
- TARGET_TEMP,
69
- TARGET_TEMP_HIGH,
70
- TARGET_TEMP_LOW,
71
- TEMP_DIFF,
72
- VALVE_POS,
73
- VOLTAGE_PH1,
74
- VOLTAGE_PH2,
75
- VOLTAGE_PH3,
76
- WATER_PRESSURE,
77
- WATER_TEMP,
78
- )
79
-
80
- # Upstream consts
81
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
82
  from .entity import PlugwiseEntity
83
 
@@ -92,301 +40,299 @@
92
  key: SensorType
93
 
94
 
95
- # Upstream consts
96
- PLUGWISE_SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
97
  PlugwiseSensorEntityDescription(
98
- key=TARGET_TEMP,
99
- translation_key=TARGET_TEMP,
100
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
101
  device_class=SensorDeviceClass.TEMPERATURE,
102
  state_class=SensorStateClass.MEASUREMENT,
103
  ),
104
  PlugwiseSensorEntityDescription(
105
- key=TARGET_TEMP_HIGH,
106
  translation_key="cooling_setpoint",
107
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
108
  device_class=SensorDeviceClass.TEMPERATURE,
109
  state_class=SensorStateClass.MEASUREMENT,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
- key=TARGET_TEMP_LOW,
113
  translation_key="heating_setpoint",
114
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  state_class=SensorStateClass.MEASUREMENT,
117
  ),
118
  PlugwiseSensorEntityDescription(
119
- key=ATTR_TEMPERATURE,
120
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
121
  device_class=SensorDeviceClass.TEMPERATURE,
122
  state_class=SensorStateClass.MEASUREMENT,
123
  ),
124
  PlugwiseSensorEntityDescription(
125
- key=INTENDED_BOILER_TEMP,
126
- translation_key=INTENDED_BOILER_TEMP,
127
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
128
  device_class=SensorDeviceClass.TEMPERATURE,
129
  entity_category=EntityCategory.DIAGNOSTIC,
130
  state_class=SensorStateClass.MEASUREMENT,
131
  ),
132
  PlugwiseSensorEntityDescription(
133
- key=TEMP_DIFF,
134
- translation_key=TEMP_DIFF,
135
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
136
  device_class=SensorDeviceClass.TEMPERATURE,
137
  entity_category=EntityCategory.DIAGNOSTIC,
138
  state_class=SensorStateClass.MEASUREMENT,
139
  ),
140
  PlugwiseSensorEntityDescription(
141
- key=OUTDOOR_TEMP,
142
- translation_key=OUTDOOR_TEMP,
143
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
144
  device_class=SensorDeviceClass.TEMPERATURE,
145
  entity_category=EntityCategory.DIAGNOSTIC,
146
  state_class=SensorStateClass.MEASUREMENT,
147
- suggested_display_precision=1,
148
  ),
149
  PlugwiseSensorEntityDescription(
150
- key=OUTDOOR_AIR_TEMP,
151
- translation_key=OUTDOOR_AIR_TEMP,
152
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
153
  device_class=SensorDeviceClass.TEMPERATURE,
154
  entity_category=EntityCategory.DIAGNOSTIC,
155
  state_class=SensorStateClass.MEASUREMENT,
156
  ),
157
  PlugwiseSensorEntityDescription(
158
- key=WATER_TEMP,
159
- translation_key=WATER_TEMP,
160
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
161
  device_class=SensorDeviceClass.TEMPERATURE,
162
  entity_category=EntityCategory.DIAGNOSTIC,
163
  state_class=SensorStateClass.MEASUREMENT,
164
  ),
165
  PlugwiseSensorEntityDescription(
166
- key=RETURN_TEMP,
167
- translation_key=RETURN_TEMP,
168
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
169
  device_class=SensorDeviceClass.TEMPERATURE,
170
  entity_category=EntityCategory.DIAGNOSTIC,
171
  state_class=SensorStateClass.MEASUREMENT,
172
  ),
173
  PlugwiseSensorEntityDescription(
174
- key=EL_CONSUMED,
175
- translation_key=EL_CONSUMED,
176
  native_unit_of_measurement=UnitOfPower.WATT,
177
  device_class=SensorDeviceClass.POWER,
178
  state_class=SensorStateClass.MEASUREMENT,
179
  ),
180
  PlugwiseSensorEntityDescription(
181
- key=EL_PRODUCED,
182
- translation_key=EL_PRODUCED,
183
  native_unit_of_measurement=UnitOfPower.WATT,
184
  device_class=SensorDeviceClass.POWER,
185
  state_class=SensorStateClass.MEASUREMENT,
186
  entity_registry_enabled_default=False,
187
  ),
188
  PlugwiseSensorEntityDescription(
189
- key=EL_CONS_INTERVAL,
190
- translation_key=EL_CONS_INTERVAL,
191
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
192
  device_class=SensorDeviceClass.ENERGY,
193
  state_class=SensorStateClass.TOTAL,
194
  ),
195
  PlugwiseSensorEntityDescription(
196
- key=EL_CONS_P_INTERVAL,
197
- translation_key=EL_CONS_P_INTERVAL,
198
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
199
  device_class=SensorDeviceClass.ENERGY,
200
  state_class=SensorStateClass.TOTAL,
201
  ),
202
  PlugwiseSensorEntityDescription(
203
- key=EL_CONS_OP_INTERVAL,
204
- translation_key=EL_CONS_OP_INTERVAL,
205
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
206
  device_class=SensorDeviceClass.ENERGY,
207
  state_class=SensorStateClass.TOTAL,
208
  ),
209
  PlugwiseSensorEntityDescription(
210
- key=EL_PROD_INTERVAL,
211
- translation_key=EL_PROD_INTERVAL,
212
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
213
  device_class=SensorDeviceClass.ENERGY,
214
  state_class=SensorStateClass.TOTAL,
215
  entity_registry_enabled_default=False,
216
  ),
217
  PlugwiseSensorEntityDescription(
218
- key=EL_PROD_P_INTERVAL,
219
- translation_key=EL_PROD_P_INTERVAL,
220
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
221
  device_class=SensorDeviceClass.ENERGY,
222
  state_class=SensorStateClass.TOTAL,
223
  ),
224
  PlugwiseSensorEntityDescription(
225
- key=EL_PROD_OP_INTERVAL,
226
- translation_key=EL_PROD_OP_INTERVAL,
227
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
228
  device_class=SensorDeviceClass.ENERGY,
229
  state_class=SensorStateClass.TOTAL,
230
  ),
231
  PlugwiseSensorEntityDescription(
232
- key=EL_CONS_POINT,
233
- translation_key=EL_CONS_POINT,
234
  device_class=SensorDeviceClass.POWER,
235
  native_unit_of_measurement=UnitOfPower.WATT,
236
  state_class=SensorStateClass.MEASUREMENT,
237
  ),
238
  PlugwiseSensorEntityDescription(
239
- key=EL_CONS_OP_POINT,
240
- translation_key=EL_CONS_OP_POINT,
241
  native_unit_of_measurement=UnitOfPower.WATT,
242
  device_class=SensorDeviceClass.POWER,
243
  state_class=SensorStateClass.MEASUREMENT,
244
  ),
245
  PlugwiseSensorEntityDescription(
246
- key=EL_CONS_P_POINT,
247
- translation_key=EL_CONS_P_POINT,
248
  native_unit_of_measurement=UnitOfPower.WATT,
249
  device_class=SensorDeviceClass.POWER,
250
  state_class=SensorStateClass.MEASUREMENT,
251
  ),
252
  PlugwiseSensorEntityDescription(
253
- key=EL_CONS_OP_CUMULATIVE,
254
- translation_key=EL_CONS_OP_CUMULATIVE,
255
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
256
  device_class=SensorDeviceClass.ENERGY,
257
  state_class=SensorStateClass.TOTAL_INCREASING,
258
  ),
259
  PlugwiseSensorEntityDescription(
260
- key=EL_CONS_P_CUMULATIVE,
261
- translation_key=EL_CONS_P_CUMULATIVE,
262
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
263
  device_class=SensorDeviceClass.ENERGY,
264
  state_class=SensorStateClass.TOTAL_INCREASING,
265
  ),
266
  PlugwiseSensorEntityDescription(
267
- key=EL_PROD_POINT,
268
- translation_key=EL_PROD_POINT,
269
  device_class=SensorDeviceClass.POWER,
270
  native_unit_of_measurement=UnitOfPower.WATT,
271
  state_class=SensorStateClass.MEASUREMENT,
272
  ),
273
  PlugwiseSensorEntityDescription(
274
- key=EL_PROD_OP_POINT,
275
- translation_key=EL_PROD_OP_POINT,
276
  native_unit_of_measurement=UnitOfPower.WATT,
277
  device_class=SensorDeviceClass.POWER,
278
  state_class=SensorStateClass.MEASUREMENT,
279
  ),
280
  PlugwiseSensorEntityDescription(
281
- key=EL_PROD_P_POINT,
282
- translation_key=EL_PROD_P_POINT,
283
  native_unit_of_measurement=UnitOfPower.WATT,
284
  device_class=SensorDeviceClass.POWER,
285
  state_class=SensorStateClass.MEASUREMENT,
286
  ),
287
  PlugwiseSensorEntityDescription(
288
- key=EL_PROD_OP_CUMULATIVE,
289
- translation_key=EL_PROD_OP_CUMULATIVE,
290
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
291
  device_class=SensorDeviceClass.ENERGY,
292
  state_class=SensorStateClass.TOTAL_INCREASING,
293
  ),
294
  PlugwiseSensorEntityDescription(
295
- key=EL_PROD_P_CUMULATIVE,
296
- translation_key=EL_PROD_P_CUMULATIVE,
297
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
298
  device_class=SensorDeviceClass.ENERGY,
299
  state_class=SensorStateClass.TOTAL_INCREASING,
300
  ),
301
  PlugwiseSensorEntityDescription(
302
- key=EL_PH1_CONSUMED,
303
- translation_key=EL_PH1_CONSUMED,
304
  device_class=SensorDeviceClass.POWER,
305
  native_unit_of_measurement=UnitOfPower.WATT,
306
  state_class=SensorStateClass.MEASUREMENT,
307
  ),
308
  PlugwiseSensorEntityDescription(
309
- key=EL_PH2_CONSUMED,
310
- translation_key=EL_PH2_CONSUMED,
311
  device_class=SensorDeviceClass.POWER,
312
  native_unit_of_measurement=UnitOfPower.WATT,
313
  state_class=SensorStateClass.MEASUREMENT,
314
  ),
315
  PlugwiseSensorEntityDescription(
316
- key=EL_PH3_CONSUMED,
317
- translation_key=EL_PH3_CONSUMED,
318
  device_class=SensorDeviceClass.POWER,
319
  native_unit_of_measurement=UnitOfPower.WATT,
320
  state_class=SensorStateClass.MEASUREMENT,
321
  ),
322
  PlugwiseSensorEntityDescription(
323
- key=EL_PH1_PRODUCED,
324
- translation_key=EL_PH1_PRODUCED,
325
  device_class=SensorDeviceClass.POWER,
326
  native_unit_of_measurement=UnitOfPower.WATT,
327
  state_class=SensorStateClass.MEASUREMENT,
328
  ),
329
  PlugwiseSensorEntityDescription(
330
- key=EL_PH2_PRODUCED,
331
- translation_key=EL_PH2_PRODUCED,
332
  device_class=SensorDeviceClass.POWER,
333
  native_unit_of_measurement=UnitOfPower.WATT,
334
  state_class=SensorStateClass.MEASUREMENT,
335
  ),
336
  PlugwiseSensorEntityDescription(
337
- key=EL_PH3_PRODUCED,
338
- translation_key=EL_PH3_PRODUCED,
339
  device_class=SensorDeviceClass.POWER,
340
  native_unit_of_measurement=UnitOfPower.WATT,
341
  state_class=SensorStateClass.MEASUREMENT,
342
  ),
343
  PlugwiseSensorEntityDescription(
344
- key=VOLTAGE_PH1,
345
- translation_key=VOLTAGE_PH1,
346
  device_class=SensorDeviceClass.VOLTAGE,
347
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
348
  state_class=SensorStateClass.MEASUREMENT,
349
  entity_registry_enabled_default=False,
350
  ),
351
  PlugwiseSensorEntityDescription(
352
- key=VOLTAGE_PH2,
353
- translation_key=VOLTAGE_PH2,
354
  device_class=SensorDeviceClass.VOLTAGE,
355
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
356
  state_class=SensorStateClass.MEASUREMENT,
357
  entity_registry_enabled_default=False,
358
  ),
359
  PlugwiseSensorEntityDescription(
360
- key=VOLTAGE_PH3,
361
- translation_key=VOLTAGE_PH3,
362
  device_class=SensorDeviceClass.VOLTAGE,
363
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
364
  state_class=SensorStateClass.MEASUREMENT,
365
  entity_registry_enabled_default=False,
366
  ),
367
  PlugwiseSensorEntityDescription(
368
- key=GAS_CONS_INTERVAL,
369
- translation_key=GAS_CONS_INTERVAL,
370
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
371
  state_class=SensorStateClass.MEASUREMENT,
372
  ),
373
  PlugwiseSensorEntityDescription(
374
- key=GAS_CONS_CUMULATIVE,
375
- translation_key=GAS_CONS_CUMULATIVE,
376
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
377
  device_class=SensorDeviceClass.GAS,
378
  state_class=SensorStateClass.TOTAL,
379
  ),
380
  PlugwiseSensorEntityDescription(
381
- key=NET_EL_POINT,
382
- translation_key=NET_EL_POINT,
383
  native_unit_of_measurement=UnitOfPower.WATT,
384
  device_class=SensorDeviceClass.POWER,
385
  state_class=SensorStateClass.MEASUREMENT,
386
  ),
387
  PlugwiseSensorEntityDescription(
388
- key=NET_EL_CUMULATIVE,
389
- translation_key=NET_EL_CUMULATIVE,
390
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
391
  device_class=SensorDeviceClass.ENERGY,
392
  state_class=SensorStateClass.TOTAL,
@@ -406,22 +352,22 @@
406
  state_class=SensorStateClass.MEASUREMENT,
407
  ),
408
  PlugwiseSensorEntityDescription(
409
- key=MOD_LEVEL,
410
- translation_key=MOD_LEVEL,
411
  native_unit_of_measurement=PERCENTAGE,
412
  entity_category=EntityCategory.DIAGNOSTIC,
413
  state_class=SensorStateClass.MEASUREMENT,
414
  ),
415
  PlugwiseSensorEntityDescription(
416
- key=VALVE_POS,
417
- translation_key=VALVE_POS,
418
  native_unit_of_measurement=PERCENTAGE,
419
  entity_category=EntityCategory.DIAGNOSTIC,
420
  state_class=SensorStateClass.MEASUREMENT,
421
  ),
422
  PlugwiseSensorEntityDescription(
423
- key=WATER_PRESSURE,
424
- translation_key=WATER_PRESSURE,
425
  native_unit_of_measurement=UnitOfPressure.BAR,
426
  device_class=SensorDeviceClass.PRESSURE,
427
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -434,16 +380,16 @@
434
  state_class=SensorStateClass.MEASUREMENT,
435
  ),
436
  PlugwiseSensorEntityDescription(
437
- key=DHW_TEMP,
438
- translation_key=DHW_TEMP,
439
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
440
  device_class=SensorDeviceClass.TEMPERATURE,
441
  entity_category=EntityCategory.DIAGNOSTIC,
442
  state_class=SensorStateClass.MEASUREMENT,
443
  ),
444
  PlugwiseSensorEntityDescription(
445
- key=DHW_SETPOINT,
446
- translation_key=DHW_SETPOINT,
447
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
448
  device_class=SensorDeviceClass.TEMPERATURE,
449
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -457,8 +403,7 @@
457
  entry: PlugwiseConfigEntry,
458
  async_add_entities: AddConfigEntryEntitiesCallback,
459
  ) -> None:
460
- """Set up Plugwise sensors from a config entry."""
461
- # Upstream as Plugwise not Smile
462
  coordinator = entry.runtime_data
463
 
464
  @callback
@@ -467,29 +412,13 @@
467
  if not coordinator.new_devices:
468
  return
469
 
470
- # Upstream consts
471
- # async_add_entities(
472
- # PlugwiseSensorEntity(coordinator, device_id, description)
473
- # for device_id in coordinator.new_devices
474
- # if (sensors := coordinator.data.devices[device_id].get(SENSORS))
475
- # for description in PLUGWISE_SENSORS
476
- # if description.key in sensors
477
- # )
478
- # pw-beta alternative for debugging
479
- entities: list[PlugwiseSensorEntity] = []
480
- for device_id in coordinator.new_devices:
481
- device = coordinator.data[device_id]
482
- if not (sensors := device.get(SENSORS)):
483
- continue
484
- for description in PLUGWISE_SENSORS:
485
- if description.key not in sensors:
486
- continue
487
- entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
488
- LOGGER.debug(
489
- "Add %s %s sensor", device["name"], description.translation_key or description.key
490
- )
491
-
492
- async_add_entities(entities)
493
 
494
  _add_entities()
495
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -508,11 +437,11 @@
508
  ) -> None:
509
  """Initialise the sensor."""
510
  super().__init__(coordinator, device_id)
511
- self.entity_description = description
512
  self._attr_unique_id = f"{device_id}-{description.key}"
 
513
 
514
  @property
515
  @override
516
- def native_value(self) -> int | float | None:
517
  """Return the value reported by the sensor."""
518
- return self.device.get(SENSORS, {}).get(self.entity_description.key) # Upstream consts
 
12
  SensorStateClass,
13
  )
14
  from homeassistant.const import (
 
15
  LIGHT_LUX,
16
  PERCENTAGE,
17
  EntityCategory,
 
26
  from homeassistant.core import HomeAssistant, callback
27
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
30
  from .entity import PlugwiseEntity
31
 
 
40
  key: SensorType
41
 
42
 
43
+ SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
 
44
  PlugwiseSensorEntityDescription(
45
+ key="setpoint",
46
+ translation_key="setpoint",
47
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
48
  device_class=SensorDeviceClass.TEMPERATURE,
49
  state_class=SensorStateClass.MEASUREMENT,
50
  ),
51
  PlugwiseSensorEntityDescription(
52
+ key="setpoint_high",
53
  translation_key="cooling_setpoint",
54
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
55
  device_class=SensorDeviceClass.TEMPERATURE,
56
  state_class=SensorStateClass.MEASUREMENT,
57
  ),
58
  PlugwiseSensorEntityDescription(
59
+ key="setpoint_low",
60
  translation_key="heating_setpoint",
61
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
62
  device_class=SensorDeviceClass.TEMPERATURE,
63
  state_class=SensorStateClass.MEASUREMENT,
64
  ),
65
  PlugwiseSensorEntityDescription(
66
+ key="temperature",
67
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
68
  device_class=SensorDeviceClass.TEMPERATURE,
69
  state_class=SensorStateClass.MEASUREMENT,
70
  ),
71
  PlugwiseSensorEntityDescription(
72
+ key="intended_boiler_temperature",
73
+ translation_key="intended_boiler_temperature",
74
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
75
  device_class=SensorDeviceClass.TEMPERATURE,
76
  entity_category=EntityCategory.DIAGNOSTIC,
77
  state_class=SensorStateClass.MEASUREMENT,
78
  ),
79
  PlugwiseSensorEntityDescription(
80
+ key="temperature_difference",
81
+ translation_key="temperature_difference",
82
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
83
  device_class=SensorDeviceClass.TEMPERATURE,
84
  entity_category=EntityCategory.DIAGNOSTIC,
85
  state_class=SensorStateClass.MEASUREMENT,
86
  ),
87
  PlugwiseSensorEntityDescription(
88
+ key="outdoor_temperature",
89
+ translation_key="outdoor_temperature",
90
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
91
  device_class=SensorDeviceClass.TEMPERATURE,
92
  entity_category=EntityCategory.DIAGNOSTIC,
93
  state_class=SensorStateClass.MEASUREMENT,
 
94
  ),
95
  PlugwiseSensorEntityDescription(
96
+ key="outdoor_air_temperature",
97
+ translation_key="outdoor_air_temperature",
98
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
99
  device_class=SensorDeviceClass.TEMPERATURE,
100
  entity_category=EntityCategory.DIAGNOSTIC,
101
  state_class=SensorStateClass.MEASUREMENT,
102
  ),
103
  PlugwiseSensorEntityDescription(
104
+ key="water_temperature",
105
+ translation_key="water_temperature",
106
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
107
  device_class=SensorDeviceClass.TEMPERATURE,
108
  entity_category=EntityCategory.DIAGNOSTIC,
109
  state_class=SensorStateClass.MEASUREMENT,
110
  ),
111
  PlugwiseSensorEntityDescription(
112
+ key="return_temperature",
113
+ translation_key="return_temperature",
114
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115
  device_class=SensorDeviceClass.TEMPERATURE,
116
  entity_category=EntityCategory.DIAGNOSTIC,
117
  state_class=SensorStateClass.MEASUREMENT,
118
  ),
119
  PlugwiseSensorEntityDescription(
120
+ key="electricity_consumed",
121
+ translation_key="electricity_consumed",
122
  native_unit_of_measurement=UnitOfPower.WATT,
123
  device_class=SensorDeviceClass.POWER,
124
  state_class=SensorStateClass.MEASUREMENT,
125
  ),
126
  PlugwiseSensorEntityDescription(
127
+ key="electricity_produced",
128
+ translation_key="electricity_produced",
129
  native_unit_of_measurement=UnitOfPower.WATT,
130
  device_class=SensorDeviceClass.POWER,
131
  state_class=SensorStateClass.MEASUREMENT,
132
  entity_registry_enabled_default=False,
133
  ),
134
  PlugwiseSensorEntityDescription(
135
+ key="electricity_consumed_interval",
136
+ translation_key="electricity_consumed_interval",
137
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
138
  device_class=SensorDeviceClass.ENERGY,
139
  state_class=SensorStateClass.TOTAL,
140
  ),
141
  PlugwiseSensorEntityDescription(
142
+ key="electricity_consumed_peak_interval",
143
+ translation_key="electricity_consumed_peak_interval",
144
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
145
  device_class=SensorDeviceClass.ENERGY,
146
  state_class=SensorStateClass.TOTAL,
147
  ),
148
  PlugwiseSensorEntityDescription(
149
+ key="electricity_consumed_off_peak_interval",
150
+ translation_key="electricity_consumed_off_peak_interval",
151
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
152
  device_class=SensorDeviceClass.ENERGY,
153
  state_class=SensorStateClass.TOTAL,
154
  ),
155
  PlugwiseSensorEntityDescription(
156
+ key="electricity_produced_interval",
157
+ translation_key="electricity_produced_interval",
158
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
159
  device_class=SensorDeviceClass.ENERGY,
160
  state_class=SensorStateClass.TOTAL,
161
  entity_registry_enabled_default=False,
162
  ),
163
  PlugwiseSensorEntityDescription(
164
+ key="electricity_produced_peak_interval",
165
+ translation_key="electricity_produced_peak_interval",
166
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
167
  device_class=SensorDeviceClass.ENERGY,
168
  state_class=SensorStateClass.TOTAL,
169
  ),
170
  PlugwiseSensorEntityDescription(
171
+ key="electricity_produced_off_peak_interval",
172
+ translation_key="electricity_produced_off_peak_interval",
173
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
174
  device_class=SensorDeviceClass.ENERGY,
175
  state_class=SensorStateClass.TOTAL,
176
  ),
177
  PlugwiseSensorEntityDescription(
178
+ key="electricity_consumed_point",
179
+ translation_key="electricity_consumed_point",
180
  device_class=SensorDeviceClass.POWER,
181
  native_unit_of_measurement=UnitOfPower.WATT,
182
  state_class=SensorStateClass.MEASUREMENT,
183
  ),
184
  PlugwiseSensorEntityDescription(
185
+ key="electricity_consumed_off_peak_point",
186
+ translation_key="electricity_consumed_off_peak_point",
187
  native_unit_of_measurement=UnitOfPower.WATT,
188
  device_class=SensorDeviceClass.POWER,
189
  state_class=SensorStateClass.MEASUREMENT,
190
  ),
191
  PlugwiseSensorEntityDescription(
192
+ key="electricity_consumed_peak_point",
193
+ translation_key="electricity_consumed_peak_point",
194
  native_unit_of_measurement=UnitOfPower.WATT,
195
  device_class=SensorDeviceClass.POWER,
196
  state_class=SensorStateClass.MEASUREMENT,
197
  ),
198
  PlugwiseSensorEntityDescription(
199
+ key="electricity_consumed_off_peak_cumulative",
200
+ translation_key="electricity_consumed_off_peak_cumulative",
201
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
202
  device_class=SensorDeviceClass.ENERGY,
203
  state_class=SensorStateClass.TOTAL_INCREASING,
204
  ),
205
  PlugwiseSensorEntityDescription(
206
+ key="electricity_consumed_peak_cumulative",
207
+ translation_key="electricity_consumed_peak_cumulative",
208
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
209
  device_class=SensorDeviceClass.ENERGY,
210
  state_class=SensorStateClass.TOTAL_INCREASING,
211
  ),
212
  PlugwiseSensorEntityDescription(
213
+ key="electricity_produced_point",
214
+ translation_key="electricity_produced_point",
215
  device_class=SensorDeviceClass.POWER,
216
  native_unit_of_measurement=UnitOfPower.WATT,
217
  state_class=SensorStateClass.MEASUREMENT,
218
  ),
219
  PlugwiseSensorEntityDescription(
220
+ key="electricity_produced_off_peak_point",
221
+ translation_key="electricity_produced_off_peak_point",
222
  native_unit_of_measurement=UnitOfPower.WATT,
223
  device_class=SensorDeviceClass.POWER,
224
  state_class=SensorStateClass.MEASUREMENT,
225
  ),
226
  PlugwiseSensorEntityDescription(
227
+ key="electricity_produced_peak_point",
228
+ translation_key="electricity_produced_peak_point",
229
  native_unit_of_measurement=UnitOfPower.WATT,
230
  device_class=SensorDeviceClass.POWER,
231
  state_class=SensorStateClass.MEASUREMENT,
232
  ),
233
  PlugwiseSensorEntityDescription(
234
+ key="electricity_produced_off_peak_cumulative",
235
+ translation_key="electricity_produced_off_peak_cumulative",
236
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
237
  device_class=SensorDeviceClass.ENERGY,
238
  state_class=SensorStateClass.TOTAL_INCREASING,
239
  ),
240
  PlugwiseSensorEntityDescription(
241
+ key="electricity_produced_peak_cumulative",
242
+ translation_key="electricity_produced_peak_cumulative",
243
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
244
  device_class=SensorDeviceClass.ENERGY,
245
  state_class=SensorStateClass.TOTAL_INCREASING,
246
  ),
247
  PlugwiseSensorEntityDescription(
248
+ key="electricity_phase_one_consumed",
249
+ translation_key="electricity_phase_one_consumed",
250
  device_class=SensorDeviceClass.POWER,
251
  native_unit_of_measurement=UnitOfPower.WATT,
252
  state_class=SensorStateClass.MEASUREMENT,
253
  ),
254
  PlugwiseSensorEntityDescription(
255
+ key="electricity_phase_two_consumed",
256
+ translation_key="electricity_phase_two_consumed",
257
  device_class=SensorDeviceClass.POWER,
258
  native_unit_of_measurement=UnitOfPower.WATT,
259
  state_class=SensorStateClass.MEASUREMENT,
260
  ),
261
  PlugwiseSensorEntityDescription(
262
+ key="electricity_phase_three_consumed",
263
+ translation_key="electricity_phase_three_consumed",
264
  device_class=SensorDeviceClass.POWER,
265
  native_unit_of_measurement=UnitOfPower.WATT,
266
  state_class=SensorStateClass.MEASUREMENT,
267
  ),
268
  PlugwiseSensorEntityDescription(
269
+ key="electricity_phase_one_produced",
270
+ translation_key="electricity_phase_one_produced",
271
  device_class=SensorDeviceClass.POWER,
272
  native_unit_of_measurement=UnitOfPower.WATT,
273
  state_class=SensorStateClass.MEASUREMENT,
274
  ),
275
  PlugwiseSensorEntityDescription(
276
+ key="electricity_phase_two_produced",
277
+ translation_key="electricity_phase_two_produced",
278
  device_class=SensorDeviceClass.POWER,
279
  native_unit_of_measurement=UnitOfPower.WATT,
280
  state_class=SensorStateClass.MEASUREMENT,
281
  ),
282
  PlugwiseSensorEntityDescription(
283
+ key="electricity_phase_three_produced",
284
+ translation_key="electricity_phase_three_produced",
285
  device_class=SensorDeviceClass.POWER,
286
  native_unit_of_measurement=UnitOfPower.WATT,
287
  state_class=SensorStateClass.MEASUREMENT,
288
  ),
289
  PlugwiseSensorEntityDescription(
290
+ key="voltage_phase_one",
291
+ translation_key="voltage_phase_one",
292
  device_class=SensorDeviceClass.VOLTAGE,
293
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
294
  state_class=SensorStateClass.MEASUREMENT,
295
  entity_registry_enabled_default=False,
296
  ),
297
  PlugwiseSensorEntityDescription(
298
+ key="voltage_phase_two",
299
+ translation_key="voltage_phase_two",
300
  device_class=SensorDeviceClass.VOLTAGE,
301
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
302
  state_class=SensorStateClass.MEASUREMENT,
303
  entity_registry_enabled_default=False,
304
  ),
305
  PlugwiseSensorEntityDescription(
306
+ key="voltage_phase_three",
307
+ translation_key="voltage_phase_three",
308
  device_class=SensorDeviceClass.VOLTAGE,
309
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
310
  state_class=SensorStateClass.MEASUREMENT,
311
  entity_registry_enabled_default=False,
312
  ),
313
  PlugwiseSensorEntityDescription(
314
+ key="gas_consumed_interval",
315
+ translation_key="gas_consumed_interval",
316
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
317
  state_class=SensorStateClass.MEASUREMENT,
318
  ),
319
  PlugwiseSensorEntityDescription(
320
+ key="gas_consumed_cumulative",
321
+ translation_key="gas_consumed_cumulative",
322
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
323
  device_class=SensorDeviceClass.GAS,
324
  state_class=SensorStateClass.TOTAL,
325
  ),
326
  PlugwiseSensorEntityDescription(
327
+ key="net_electricity_point",
328
+ translation_key="net_electricity_point",
329
  native_unit_of_measurement=UnitOfPower.WATT,
330
  device_class=SensorDeviceClass.POWER,
331
  state_class=SensorStateClass.MEASUREMENT,
332
  ),
333
  PlugwiseSensorEntityDescription(
334
+ key="net_electricity_cumulative",
335
+ translation_key="net_electricity_cumulative",
336
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
337
  device_class=SensorDeviceClass.ENERGY,
338
  state_class=SensorStateClass.TOTAL,
 
352
  state_class=SensorStateClass.MEASUREMENT,
353
  ),
354
  PlugwiseSensorEntityDescription(
355
+ key="modulation_level",
356
+ translation_key="modulation_level",
357
  native_unit_of_measurement=PERCENTAGE,
358
  entity_category=EntityCategory.DIAGNOSTIC,
359
  state_class=SensorStateClass.MEASUREMENT,
360
  ),
361
  PlugwiseSensorEntityDescription(
362
+ key="valve_position",
363
+ translation_key="valve_position",
364
  native_unit_of_measurement=PERCENTAGE,
365
  entity_category=EntityCategory.DIAGNOSTIC,
366
  state_class=SensorStateClass.MEASUREMENT,
367
  ),
368
  PlugwiseSensorEntityDescription(
369
+ key="water_pressure",
370
+ translation_key="water_pressure",
371
  native_unit_of_measurement=UnitOfPressure.BAR,
372
  device_class=SensorDeviceClass.PRESSURE,
373
  entity_category=EntityCategory.DIAGNOSTIC,
 
380
  state_class=SensorStateClass.MEASUREMENT,
381
  ),
382
  PlugwiseSensorEntityDescription(
383
+ key="dhw_temperature",
384
+ translation_key="dhw_temperature",
385
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
386
  device_class=SensorDeviceClass.TEMPERATURE,
387
  entity_category=EntityCategory.DIAGNOSTIC,
388
  state_class=SensorStateClass.MEASUREMENT,
389
  ),
390
  PlugwiseSensorEntityDescription(
391
+ key="domestic_hot_water_setpoint",
392
+ translation_key="domestic_hot_water_setpoint",
393
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
394
  device_class=SensorDeviceClass.TEMPERATURE,
395
  entity_category=EntityCategory.DIAGNOSTIC,
 
403
  entry: PlugwiseConfigEntry,
404
  async_add_entities: AddConfigEntryEntitiesCallback,
405
  ) -> None:
406
+ """Set up the Smile sensors from a config entry."""
 
407
  coordinator = entry.runtime_data
408
 
409
  @callback
 
412
  if not coordinator.new_devices:
413
  return
414
 
415
+ async_add_entities(
416
+ PlugwiseSensorEntity(coordinator, device_id, description)
417
+ for device_id in coordinator.new_devices
418
+ if (sensors := coordinator.data[device_id].get("sensors"))
419
+ for description in SENSORS
420
+ if description.key in sensors
421
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  _add_entities()
424
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
437
  ) -> None:
438
  """Initialise the sensor."""
439
  super().__init__(coordinator, device_id)
 
440
  self._attr_unique_id = f"{device_id}-{description.key}"
441
+ self.entity_description = description
442
 
443
  @property
444
  @override
445
+ def native_value(self) -> int | float:
446
  """Return the value reported by the sensor."""
447
+ return self.device["sensors"][self.entity_description.key]
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/switch.py RENAMED
@@ -10,23 +10,14 @@
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 .const import (
18
- COOLING_ENA_SWITCH,
19
- LOCK,
20
- LOGGER, # pw-beta
21
- MEMBERS,
22
- RELAY,
23
- SWITCHES,
24
- )
25
-
26
- # Upstream consts
27
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
28
  from .entity import PlugwiseEntity
29
- from .util import plugwise_command
30
 
31
  PARALLEL_UPDATES = 0
32
 
@@ -38,23 +29,25 @@
38
  key: SwitchType
39
 
40
 
41
- # Upstream consts
42
- PLUGWISE_SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
43
  PlugwiseSwitchEntityDescription(
44
- key=LOCK,
45
- translation_key=LOCK,
46
- device_class=SwitchDeviceClass.SWITCH,
47
  entity_category=EntityCategory.CONFIG,
48
  ),
49
  PlugwiseSwitchEntityDescription(
50
- key=RELAY,
51
- translation_key=RELAY,
52
- device_class=SwitchDeviceClass.SWITCH,
53
  ),
54
  PlugwiseSwitchEntityDescription(
55
- key=COOLING_ENA_SWITCH,
56
- translation_key=COOLING_ENA_SWITCH,
57
  device_class=SwitchDeviceClass.SWITCH,
 
 
 
 
58
  entity_category=EntityCategory.CONFIG,
59
  ),
60
  )
@@ -65,8 +58,9 @@
65
  entry: PlugwiseConfigEntry,
66
  async_add_entities: AddConfigEntryEntitiesCallback,
67
  ) -> None:
68
- """Set up Plugwise switches from a config entry."""
69
  coordinator = entry.runtime_data
 
70
 
71
  @callback
72
  def _add_entities() -> None:
@@ -74,27 +68,32 @@
74
  if not coordinator.new_devices:
75
  return
76
 
77
- # Upstream consts
78
- # async_add_entities(
79
- # PlugwiseSwitchEntity(coordinator, device_id, description)
80
- # for device_id in coordinator.new_devices
81
- # if (switches := coordinator.data.devices[device_id].get(SWITCHES))
82
- # for description in PLUGWISE_SWITCHES
83
- # if description.key in switches
84
- # )
85
- # pw-beta alternative for debugging
86
- entities: list[PlugwiseSwitchEntity] = []
87
  for device_id in coordinator.new_devices:
88
- device = coordinator.data[device_id]
89
- if not (switches := device.get(SWITCHES)):
90
  continue
91
- for description in PLUGWISE_SWITCHES:
92
  if description.key not in switches:
93
  continue
94
- entities.append(PlugwiseSwitchEntity(coordinator, device_id, description))
95
- LOGGER.debug(
96
- "Add %s %s switch", device["name"], description.translation_key
97
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  async_add_entities(entities)
99
 
100
  _add_entities()
@@ -114,14 +113,14 @@
114
  ) -> None:
115
  """Set up the Plugwise API."""
116
  super().__init__(coordinator, device_id)
117
- self.entity_description = description
118
  self._attr_unique_id = f"{device_id}-{description.key}"
 
119
 
120
  @property
121
  @override
122
- def is_on(self) -> bool | None:
123
  """Return True if entity is on."""
124
- return self.device.get(SWITCHES, {}).get(self.entity_description.key) # Upstream const
125
 
126
  @plugwise_command
127
  @override
@@ -129,10 +128,10 @@
129
  """Turn the device on."""
130
  await self.coordinator.api.set_switch_state(
131
  self._dev_id,
132
- self.device.get(MEMBERS),
133
  self.entity_description.key,
134
  "on",
135
- ) # Upstream const
136
 
137
  @plugwise_command
138
  @override
@@ -140,7 +139,21 @@
140
  """Turn the device off."""
141
  await self.coordinator.api.set_switch_state(
142
  self._dev_id,
143
- self.device.get(MEMBERS),
144
  self.entity_description.key,
145
  "off",
146
- ) # Upstream const
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  SwitchEntity,
11
  SwitchEntityDescription,
12
  )
13
+ from homeassistant.const import EntityCategory, Platform
14
  from homeassistant.core import HomeAssistant, callback
15
+ from homeassistant.helpers import entity_registry as er
16
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
 
 
 
 
 
 
 
 
 
 
18
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
19
  from .entity import PlugwiseEntity
20
+ from .util import deprecate_entity, plugwise_command
21
 
22
  PARALLEL_UPDATES = 0
23
 
 
29
  key: SwitchType
30
 
31
 
32
+ SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
 
33
  PlugwiseSwitchEntityDescription(
34
+ key="dhw_cm_switch",
35
+ translation_key="dhw_cm_switch",
 
36
  entity_category=EntityCategory.CONFIG,
37
  ),
38
  PlugwiseSwitchEntityDescription(
39
+ key="lock",
40
+ translation_key="lock",
41
+ entity_category=EntityCategory.CONFIG,
42
  ),
43
  PlugwiseSwitchEntityDescription(
44
+ key="relay",
45
+ translation_key="relay",
46
  device_class=SwitchDeviceClass.SWITCH,
47
+ ),
48
+ PlugwiseSwitchEntityDescription(
49
+ key="cooling_ena_switch",
50
+ translation_key="cooling_ena_switch",
51
  entity_category=EntityCategory.CONFIG,
52
  ),
53
  )
 
58
  entry: PlugwiseConfigEntry,
59
  async_add_entities: AddConfigEntryEntitiesCallback,
60
  ) -> None:
61
+ """Set up the Smile switches from a config entry."""
62
  coordinator = entry.runtime_data
63
+ entity_registry = er.async_get(hass)
64
 
65
  @callback
66
  def _add_entities() -> None:
 
68
  if not coordinator.new_devices:
69
  return
70
 
71
+ entities: list[SwitchEntity] = []
 
 
 
 
 
 
 
 
 
72
  for device_id in coordinator.new_devices:
73
+ if not (switches := coordinator.data[device_id].get("switches")):
 
74
  continue
75
+ for description in SWITCHES:
76
  if description.key not in switches:
77
  continue
78
+ if description.key == "dhw_cm_switch":
79
+ if not deprecate_entity(
80
+ hass,
81
+ entity_registry,
82
+ async_on_unload=entry.async_on_unload,
83
+ platform_domain=Platform.SWITCH,
84
+ entity_unique_id=f"{device_id}-dhw_cm_switch",
85
+ issue_id=f"deprecated_dhw_cm_switch_{device_id}",
86
+ translation_key="deprecated_dhw_cm_switch",
87
+ ):
88
+ continue
89
+ entities.append(
90
+ PlugwiseDhwCmSwitchEntity(coordinator, device_id, description)
91
+ )
92
+ else:
93
+ entities.append(
94
+ PlugwiseSwitchEntity(coordinator, device_id, description)
95
+ )
96
+
97
  async_add_entities(entities)
98
 
99
  _add_entities()
 
113
  ) -> None:
114
  """Set up the Plugwise API."""
115
  super().__init__(coordinator, device_id)
 
116
  self._attr_unique_id = f"{device_id}-{description.key}"
117
+ self.entity_description = description
118
 
119
  @property
120
  @override
121
+ def is_on(self) -> bool:
122
  """Return True if entity is on."""
123
+ return self.device["switches"][self.entity_description.key]
124
 
125
  @plugwise_command
126
  @override
 
128
  """Turn the device on."""
129
  await self.coordinator.api.set_switch_state(
130
  self._dev_id,
131
+ self.device.get("members"),
132
  self.entity_description.key,
133
  "on",
134
+ )
135
 
136
  @plugwise_command
137
  @override
 
139
  """Turn the device off."""
140
  await self.coordinator.api.set_switch_state(
141
  self._dev_id,
142
+ self.device.get("members"),
143
  self.entity_description.key,
144
  "off",
145
+ )
146
+
147
+
148
+ class PlugwiseDhwCmSwitchEntity(PlugwiseSwitchEntity):
149
+ """Represent the deprecated DHW comfort switch."""
150
+
151
+ @override
152
+ async def async_turn_on(self, **kwargs: Any) -> None:
153
+ """Turn the deprecated DHW comfort switch on."""
154
+ await super().async_turn_on(**kwargs)
155
+
156
+ @override
157
+ async def async_turn_off(self, **kwargs: Any) -> None:
158
+ """Turn the deprecated DHW comfort switch off."""
159
+ await super().async_turn_off(**kwargs)
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/util.py RENAMED
@@ -5,20 +5,182 @@
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
- # For reference:
14
- # PlugwiseEntityT = TypeVar("PlugwiseEntityT", bound=PlugwiseEntity)
15
- # R = TypeVar("_R")
16
- # P = ParamSpec("_P")
17
 
18
 
19
- def plugwise_command[PlugwiseEntityT: PlugwiseEntity, **P, R](
20
- func: Callable[Concatenate[PlugwiseEntityT, P], Awaitable[R]],
21
- ) -> Callable[Concatenate[PlugwiseEntityT, P], Coroutine[Any, Any, R]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  """Decorate Plugwise calls that send commands/make changes to the device.
23
 
24
  A decorator that wraps the passed in function, catches Plugwise errors,
@@ -26,8 +188,8 @@
26
  """
27
 
28
  async def handler(
29
- self: PlugwiseEntityT, *args: P.args, **kwargs: P.kwargs
30
- ) -> R:
31
  try:
32
  return await func(self, *args, **kwargs)
33
  except PlugwiseException as err:
 
5
 
6
  from plugwise.exceptions import PlugwiseException
7
 
8
+ from homeassistant.components.automation import automations_with_entity
9
+ from homeassistant.components.script import scripts_with_entity
10
+ from homeassistant.core import CoreState, HomeAssistant, callback
11
  from homeassistant.exceptions import HomeAssistantError
12
+ from homeassistant.helpers import entity_registry as er
13
+ from homeassistant.helpers.issue_registry import (
14
+ IssueSeverity,
15
+ async_create_issue,
16
+ async_delete_issue,
17
+ )
18
+ from homeassistant.helpers.start import async_at_started
19
 
20
  from .const import DOMAIN
21
  from .entity import PlugwiseEntity
22
 
23
+ # Version in which deprecated entities will be removed.
24
+ DEPRECATED_REMOVAL_VERSION = "2027.5.0"
 
 
25
 
26
 
27
+ def deprecate_entity(
28
+ hass: HomeAssistant,
29
+ entity_registry: er.EntityRegistry,
30
+ *,
31
+ async_on_unload: Callable[[Callable[[], None]], None],
32
+ platform_domain: str,
33
+ entity_unique_id: str,
34
+ issue_id: str,
35
+ translation_key: str,
36
+ ) -> bool:
37
+ """Handle deprecation of an entity that has been replaced.
38
+
39
+ Return True if the deprecated entity should still be set up, which is the
40
+ case while it exists in the entity registry. A repair issue informs the user
41
+ about the replacement and the removal date; when the entity is still used by
42
+ automations or scripts they are listed in the issue. The entity is removed
43
+ once the user disables it and nothing references it anymore. New
44
+ installations never create the entity.
45
+ """
46
+ entity_id = entity_registry.async_get_entity_id(
47
+ platform_domain, DOMAIN, entity_unique_id
48
+ )
49
+ if entity_id is None:
50
+ async_delete_issue(hass, DOMAIN, issue_id)
51
+ return False
52
+
53
+ entity_entry = entity_registry.async_get(entity_id)
54
+ if entity_entry is None:
55
+ async_delete_issue(hass, DOMAIN, issue_id)
56
+ return False
57
+
58
+ if hass.state is not CoreState.running:
59
+ _async_defer_deprecation_check(
60
+ hass,
61
+ entity_registry,
62
+ async_on_unload=async_on_unload,
63
+ platform_domain=platform_domain,
64
+ entity_unique_id=entity_unique_id,
65
+ issue_id=issue_id,
66
+ translation_key=translation_key,
67
+ )
68
+ return True
69
+
70
+ return _async_check_deprecated_entity(
71
+ hass,
72
+ entity_registry,
73
+ entity_id=entity_id,
74
+ issue_id=issue_id,
75
+ translation_key=translation_key,
76
+ )
77
+
78
+
79
+ @callback
80
+ def _async_check_deprecated_entity(
81
+ hass: HomeAssistant,
82
+ entity_registry: er.EntityRegistry,
83
+ *,
84
+ entity_id: str,
85
+ issue_id: str,
86
+ translation_key: str,
87
+ ) -> bool:
88
+ """Check whether a deprecated entity should remain in the registry."""
89
+ entity_entry = entity_registry.async_get(entity_id)
90
+ if entity_entry is None:
91
+ async_delete_issue(hass, DOMAIN, issue_id)
92
+ return False
93
+
94
+ items = _automations_and_scripts_using_entity(hass, entity_registry, entity_id)
95
+
96
+ if entity_entry.disabled and not items:
97
+ entity_registry.async_remove(entity_id)
98
+ async_delete_issue(hass, DOMAIN, issue_id)
99
+ return False
100
+
101
+ placeholders = {
102
+ "entity_id": entity_id,
103
+ "entity_name": entity_entry.name or entity_entry.original_name or entity_id,
104
+ }
105
+ if items:
106
+ translation_key = f"{translation_key}_scripts"
107
+ placeholders["items"] = "\n".join(items)
108
+
109
+ async_create_issue(
110
+ hass,
111
+ DOMAIN,
112
+ issue_id,
113
+ breaks_in_ha_version=DEPRECATED_REMOVAL_VERSION,
114
+ config_entry_id=entity_entry.config_entry_id,
115
+ is_fixable=False,
116
+ severity=IssueSeverity.WARNING,
117
+ translation_key=translation_key,
118
+ translation_placeholders=placeholders,
119
+ )
120
+ return True
121
+
122
+
123
+ def _automations_and_scripts_using_entity(
124
+ hass: HomeAssistant,
125
+ entity_registry: er.EntityRegistry,
126
+ entity_id: str,
127
+ ) -> list[str]:
128
+ """Return markdown list items for automations and scripts."""
129
+ automations = automations_with_entity(hass, entity_id)
130
+ scripts = scripts_with_entity(hass, entity_id)
131
+ if not automations and not scripts:
132
+ return []
133
+
134
+ items: list[str] = []
135
+ for integration, used_entities in (
136
+ ("automation", automations),
137
+ ("script", scripts),
138
+ ):
139
+ for used_entity_id in used_entities:
140
+ if entry := entity_registry.async_get(used_entity_id):
141
+ items.append(
142
+ f"- [{entry.original_name}](/config/{integration}/edit/{entry.unique_id})"
143
+ )
144
+ else:
145
+ items.append(f"- `{used_entity_id}`")
146
+
147
+ return items
148
+
149
+
150
+ @callback
151
+ def _async_defer_deprecation_check(
152
+ hass: HomeAssistant,
153
+ entity_registry: er.EntityRegistry,
154
+ *,
155
+ async_on_unload: Callable[[Callable[[], None]], None],
156
+ platform_domain: str,
157
+ entity_unique_id: str,
158
+ issue_id: str,
159
+ translation_key: str,
160
+ ) -> None:
161
+ """Recheck entity references once Home Assistant has started."""
162
+
163
+ @callback
164
+ def _async_recheck(_: HomeAssistant) -> None:
165
+ if entity_id := entity_registry.async_get_entity_id(
166
+ platform_domain, DOMAIN, entity_unique_id
167
+ ):
168
+ _async_check_deprecated_entity(
169
+ hass,
170
+ entity_registry,
171
+ entity_id=entity_id,
172
+ issue_id=issue_id,
173
+ translation_key=translation_key,
174
+ )
175
+ else:
176
+ async_delete_issue(hass, DOMAIN, issue_id)
177
+
178
+ async_on_unload(async_at_started(hass, _async_recheck))
179
+
180
+
181
+ def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
182
+ func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
183
+ ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
184
  """Decorate Plugwise calls that send commands/make changes to the device.
185
 
186
  A decorator that wraps the passed in function, catches Plugwise errors,
 
188
  """
189
 
190
  async def handler(
191
+ self: _PlugwiseEntityT, *args: _P.args, **kwargs: _P.kwargs
192
+ ) -> _R:
193
  try:
194
  return await func(self, *args, **kwargs)
195
  except PlugwiseException as err:
/home/runner/work/progress/progress/clones/beta/{beta/custom_components → ha-core/homeassistant/components}/plugwise/water_heater.py RENAMED
@@ -1,4 +1,4 @@
1
- """Plugwise water heater component for HomeAssistant."""
2
 
3
  from dataclasses import dataclass
4
  from typing import Any, Final, override
@@ -19,7 +19,6 @@
19
  from .const import (
20
  DHW_TEMP,
21
  DOMAIN,
22
- LOGGER,
23
  LOWER_BOUND,
24
  UPPER_BOUND,
25
  BinarySensorType,
@@ -34,6 +33,7 @@
34
  FAIL_SET_TEMP: Final = "temperature_out_of_range"
35
  OPERATION_LIST: Final[list[str]] = [STATE_GAS, STATE_HEAT_PUMP, STATE_OFF]
36
 
 
37
  @dataclass(frozen=True, kw_only=True)
38
  class PlugwiseWaterHeaterEntityDescription(WaterHeaterEntityDescription):
39
  """Class describing Plugwise WaterHeater entities."""
@@ -50,8 +50,9 @@
50
  ),
51
  )
52
 
 
53
  async def async_setup_entry(
54
- _hass: HomeAssistant,
55
  entry: PlugwiseConfigEntry,
56
  async_add_entities: AddConfigEntryEntitiesCallback,
57
  ) -> None:
@@ -64,19 +65,12 @@
64
  if not coordinator.new_devices:
65
  return
66
 
67
- entities: list[PlugwiseWaterHeaterEntity] = []
68
- for device_id in coordinator.new_devices:
69
- device = coordinator.data[device_id]
70
- for description in WATERHEATER_TYPES:
71
- if description.key in device:
72
- entities.append(
73
- PlugwiseWaterHeaterEntity(coordinator, device_id, description)
74
- )
75
- LOGGER.debug(
76
- "Add %s %s water_heater", device["name"], description.translation_key
77
- )
78
-
79
- async_add_entities(entities)
80
 
81
  _add_entities()
82
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -108,7 +102,7 @@
108
 
109
  @property
110
  @override
111
- def current_operation(self) -> str | None:
112
  """Return current readable operation mode."""
113
  if (
114
  binary_sensors := self.device.get("binary_sensors", {})
 
1
+ """Plugwise water heater component for Home Assistant."""
2
 
3
  from dataclasses import dataclass
4
  from typing import Any, Final, override
 
19
  from .const import (
20
  DHW_TEMP,
21
  DOMAIN,
 
22
  LOWER_BOUND,
23
  UPPER_BOUND,
24
  BinarySensorType,
 
33
  FAIL_SET_TEMP: Final = "temperature_out_of_range"
34
  OPERATION_LIST: Final[list[str]] = [STATE_GAS, STATE_HEAT_PUMP, STATE_OFF]
35
 
36
+
37
  @dataclass(frozen=True, kw_only=True)
38
  class PlugwiseWaterHeaterEntityDescription(WaterHeaterEntityDescription):
39
  """Class describing Plugwise WaterHeater entities."""
 
50
  ),
51
  )
52
 
53
+
54
  async def async_setup_entry(
55
+ hass: HomeAssistant,
56
  entry: PlugwiseConfigEntry,
57
  async_add_entities: AddConfigEntryEntitiesCallback,
58
  ) -> None:
 
65
  if not coordinator.new_devices:
66
  return
67
 
68
+ async_add_entities(
69
+ PlugwiseWaterHeaterEntity(coordinator, device_id, description)
70
+ for device_id in coordinator.new_devices
71
+ for description in WATERHEATER_TYPES
72
+ if description.key in coordinator.data[device_id]
73
+ )
 
 
 
 
 
 
 
74
 
75
  _add_entities()
76
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
102
 
103
  @property
104
  @override
105
+ def current_operation(self) -> str:
106
  """Return current readable operation mode."""
107
  if (
108
  binary_sensors := self.device.get("binary_sensors", {})