Diff to HTML by rtfpessoa

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