Diff to HTML by rtfpessoa

Files changed (17) hide show
  1. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py +88 -20
  2. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/binary_sensor.py +96 -35
  3. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/button.py +19 -7
  4. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/climate.py +173 -148
  5. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py +132 -28
  6. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/const.py +151 -33
  7. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py +49 -27
  8. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/diagnostics.py +2 -0
  9. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py +26 -10
  10. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/icons.json +3 -0
  11. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/manifest.json +3 -3
  12. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py +57 -24
  13. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/select.py +48 -16
  14. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py +174 -101
  15. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/strings.json +64 -47
  16. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py +56 -24
  17. /home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/util.py +12 -5
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/__init__.py RENAMED
@@ -1,22 +1,47 @@
1
  """Plugwise platform for Home Assistant Core."""
2
 
 
 
3
  from typing import Any
4
 
5
- from homeassistant.const import Platform
6
- from homeassistant.core import HomeAssistant, callback
 
 
 
 
 
 
 
7
  from homeassistant.helpers import device_registry as dr, entity_registry as er
8
 
9
- from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
 
 
 
 
 
 
10
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
11
 
12
 
13
  async def async_setup_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
14
- """Set up Plugwise components from a config entry."""
15
  await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry)
16
 
17
- coordinator = PlugwiseDataUpdateCoordinator(hass, entry)
 
 
 
 
 
 
 
 
 
18
  await coordinator.async_config_entry_first_refresh()
19
- migrate_sensor_entities(hass, coordinator)
 
20
 
21
  entry.runtime_data = coordinator
22
 
@@ -31,22 +56,49 @@
31
  sw_version=str(coordinator.api.smile.version),
32
  ) # required for adding the entity-less P1 Gateway
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
35
 
 
 
 
 
 
 
 
36
  return True
37
 
 
 
 
 
 
38
 
39
  async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) -> bool:
40
- """Unload the Plugwise components."""
41
  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
42
 
43
-
44
  @callback
45
  def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
46
  """Migrate Plugwise entity entries.
47
 
48
- Migrates old unique ID's from old binary_sensors and
49
- switches to the new unique ID's.
50
  """
51
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
52
  "-slave_boiler_state"
@@ -68,18 +120,17 @@
68
  # No migration needed
69
  return None
70
 
71
-
72
- def migrate_sensor_entities(
73
  hass: HomeAssistant,
74
  coordinator: PlugwiseDataUpdateCoordinator,
75
  ) -> None:
76
  """Migrate Sensors if needed."""
77
  ent_reg = er.async_get(hass)
78
 
79
- # Migrating opentherm_outdoor_temperature
80
  # to opentherm_outdoor_air_temperature sensor
81
  for device_id, device in coordinator.data.items():
82
- if device[DEV_CLASS] != "heater_central":
83
  continue
84
 
85
  old_unique_id = f"{device_id}-outdoor_temperature"
@@ -87,10 +138,27 @@
87
  Platform.SENSOR, DOMAIN, old_unique_id
88
  ):
89
  new_unique_id = f"{device_id}-outdoor_air_temperature"
90
- LOGGER.debug(
91
- "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
92
- entity_id,
93
- old_unique_id,
94
- new_unique_id,
95
- )
96
  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Plugwise platform for Home Assistant Core."""
2
 
3
+ from __future__ import annotations
4
+
5
  from typing import Any
6
 
7
+ from 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.
100
 
101
+ - Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
 
102
  """
103
  if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
104
  "-slave_boiler_state"
 
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":
134
  continue
135
 
136
  old_unique_id = f"{device_id}-outdoor_temperature"
 
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 #pragma: no cover
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
@@ -1,11 +1,16 @@
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
 
 
 
3
  from collections.abc import Mapping
4
  from dataclasses import dataclass
5
  from typing import Any
6
 
7
  from plugwise.constants import BinarySensorType
8
 
 
 
 
9
  from homeassistant.components.binary_sensor import (
10
  BinarySensorDeviceClass,
11
  BinarySensorEntity,
@@ -15,11 +20,26 @@
15
  from homeassistant.core import HomeAssistant, callback
16
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
19
  from .entity import PlugwiseEntity
20
 
21
- SEVERITIES = ["other", "info", "warning", "error"]
22
-
23
  # Coordinator is used to centralize the data updates
24
  PARALLEL_UPDATES = 0
25
 
@@ -31,50 +51,51 @@
31
  key: BinarySensorType
32
 
33
 
34
- BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
 
35
  PlugwiseBinarySensorEntityDescription(
36
- key="low_battery",
37
  device_class=BinarySensorDeviceClass.BATTERY,
38
  entity_category=EntityCategory.DIAGNOSTIC,
39
  ),
40
  PlugwiseBinarySensorEntityDescription(
41
- key="compressor_state",
42
- translation_key="compressor_state",
43
  entity_category=EntityCategory.DIAGNOSTIC,
44
  ),
45
  PlugwiseBinarySensorEntityDescription(
46
- key="cooling_enabled",
47
- translation_key="cooling_enabled",
48
  entity_category=EntityCategory.DIAGNOSTIC,
49
  ),
50
  PlugwiseBinarySensorEntityDescription(
51
- key="dhw_state",
52
- translation_key="dhw_state",
53
  entity_category=EntityCategory.DIAGNOSTIC,
54
  ),
55
  PlugwiseBinarySensorEntityDescription(
56
- key="flame_state",
57
- translation_key="flame_state",
58
  entity_category=EntityCategory.DIAGNOSTIC,
59
  ),
60
  PlugwiseBinarySensorEntityDescription(
61
- key="heating_state",
62
- translation_key="heating_state",
63
  entity_category=EntityCategory.DIAGNOSTIC,
64
  ),
65
  PlugwiseBinarySensorEntityDescription(
66
- key="cooling_state",
67
- translation_key="cooling_state",
68
  entity_category=EntityCategory.DIAGNOSTIC,
69
  ),
70
  PlugwiseBinarySensorEntityDescription(
71
- key="secondary_boiler_state",
72
- translation_key="secondary_boiler_state",
73
  entity_category=EntityCategory.DIAGNOSTIC,
74
  ),
75
  PlugwiseBinarySensorEntityDescription(
76
- key="plugwise_notification",
77
- translation_key="plugwise_notification",
78
  entity_category=EntityCategory.DIAGNOSTIC,
79
  ),
80
  )
@@ -85,7 +106,7 @@
85
  entry: PlugwiseConfigEntry,
86
  async_add_entities: AddConfigEntryEntitiesCallback,
87
  ) -> None:
88
- """Set up the Smile binary_sensors from a config entry."""
89
  coordinator = entry.runtime_data
90
 
91
  @callback
@@ -94,20 +115,40 @@
94
  if not coordinator.new_devices:
95
  return
96
 
97
- async_add_entities(
98
- PlugwiseBinarySensorEntity(coordinator, device_id, description)
99
- for device_id in coordinator.new_devices
100
- if (binary_sensors := coordinator.data[device_id].get("binary_sensors"))
101
- for description in BINARY_SENSORS
102
- if description.key in binary_sensors
103
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  _add_entities()
106
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
107
 
108
 
109
  class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity):
110
- """Represent Smile Binary Sensors."""
111
 
112
  entity_description: PlugwiseBinarySensorEntityDescription
113
 
@@ -121,26 +162,46 @@
121
  super().__init__(coordinator, device_id)
122
  self.entity_description = description
123
  self._attr_unique_id = f"{device_id}-{description.key}"
 
124
 
125
  @property
126
- def is_on(self) -> bool:
127
  """Return true if the binary sensor is on."""
128
- return self.device["binary_sensors"][self.entity_description.key]
 
 
 
 
 
 
 
129
 
130
  @property
131
  def extra_state_attributes(self) -> Mapping[str, Any] | None:
132
  """Return entity specific state attributes."""
133
- if self.entity_description.key != "plugwise_notification":
134
  return None
135
 
136
- attrs: dict[str, list[str]] = {f"{severity}_msg": [] for severity in SEVERITIES}
 
 
 
137
  gateway_id = self.coordinator.api.gateway_id
138
  if notify := self.coordinator.data[gateway_id]["notifications"]:
139
- for details in notify.values():
140
  for msg_type, msg in details.items():
141
  msg_type = msg_type.lower()
142
  if msg_type not in SEVERITIES:
143
- msg_type = "other"
 
 
 
 
 
144
  attrs[f"{msg_type}_msg"].append(msg)
145
 
 
 
 
 
146
  return attrs
 
1
  """Plugwise Binary Sensor component for Home Assistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from collections.abc import Mapping
6
  from dataclasses import dataclass
7
  from typing import Any
8
 
9
  from plugwise.constants import BinarySensorType
10
 
11
+ from homeassistant.components 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 or description.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 | None:
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.get(BINARY_SENSORS, {}).get(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
@@ -1,11 +1,16 @@
1
  """Plugwise Button component for Home Assistant."""
2
 
 
 
3
  from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
4
  from homeassistant.const import EntityCategory
5
  from homeassistant.core import HomeAssistant
6
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
7
 
8
- from .const import REBOOT
 
 
 
9
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
10
  from .entity import PlugwiseEntity
11
  from .util import plugwise_command
@@ -18,14 +23,21 @@
18
  entry: PlugwiseConfigEntry,
19
  async_add_entities: AddConfigEntryEntitiesCallback,
20
  ) -> None:
21
- """Set up the Plugwise buttons from a ConfigEntry."""
22
  coordinator = entry.runtime_data
23
 
24
- async_add_entities(
25
- PlugwiseButtonEntity(coordinator, device_id)
26
- for device_id in coordinator.data
27
- if device_id == coordinator.api.gateway_id and coordinator.api.reboot
28
- )
 
 
 
 
 
 
 
29
 
30
 
31
  class PlugwiseButtonEntity(PlugwiseEntity, ButtonEntity):
 
1
  """Plugwise Button component for Home Assistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
6
  from homeassistant.const import EntityCategory
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
@@ -1,5 +1,7 @@
1
  """Plugwise Climate component for Home Assistant."""
2
 
 
 
3
  from dataclasses import asdict, dataclass
4
  from typing import Any
5
 
@@ -12,13 +14,39 @@
12
  HVACAction,
13
  HVACMode,
14
  )
15
- from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, UnitOfTemperature
 
 
 
 
 
 
16
  from homeassistant.core import HomeAssistant, callback
17
  from homeassistant.exceptions import HomeAssistantError
18
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19
  from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
20
 
21
- from .const import DOMAIN, MASTER_THERMOSTATS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
23
  from .entity import PlugwiseEntity
24
  from .util import plugwise_command
@@ -27,79 +55,62 @@
27
  PARALLEL_UPDATES = 0
28
 
29
 
30
- @dataclass
31
- class PlugwiseClimateExtraStoredData(ExtraStoredData):
32
- """Object to hold extra stored data."""
33
-
34
- last_active_schedule: str | None
35
- previous_action_mode: str | None
36
-
37
- def as_dict(self) -> dict[str, Any]:
38
- """Return a dict representation of the text data."""
39
- return asdict(self)
40
-
41
- @classmethod
42
- def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData:
43
- """Initialize a stored data object from a dict."""
44
- return cls(
45
- last_active_schedule=restored.get("last_active_schedule"),
46
- previous_action_mode=restored.get("previous_action_mode"),
47
- )
48
-
49
-
50
  async def async_setup_entry(
51
  hass: HomeAssistant,
52
  entry: PlugwiseConfigEntry,
53
  async_add_entities: AddConfigEntryEntitiesCallback,
54
  ) -> None:
55
- """Set up the Smile Thermostats from a config entry."""
56
  coordinator = entry.runtime_data
57
 
58
  @callback
59
  def _add_entities() -> None:
60
- """Add Entities."""
61
  if not coordinator.new_devices:
62
  return
63
 
64
- if coordinator.api.smile.name == "Adam":
65
- async_add_entities(
66
- PlugwiseClimateEntity(coordinator, device_id)
67
- for device_id in coordinator.new_devices
68
- if coordinator.data[device_id]["dev_class"] == "climate"
69
- )
70
- else:
71
- async_add_entities(
72
- PlugwiseClimateEntity(coordinator, device_id)
73
- for device_id in coordinator.new_devices
74
- if coordinator.data[device_id]["dev_class"] in MASTER_THERMOSTATS
75
- )
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
79
 
80
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
82
  """Representation of a Plugwise thermostat."""
83
 
 
84
  _attr_name = None
85
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
86
  _attr_translation_key = DOMAIN
87
-
88
- _last_active_schedule: str | None = None
89
- _previous_action_mode: str | None = HVACAction.HEATING.value
90
-
91
- async def async_added_to_hass(self) -> None:
92
- """Run when entity about to be added."""
93
- await super().async_added_to_hass()
94
-
95
- if extra_data := await self.async_get_last_extra_data():
96
- plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict(
97
- extra_data.as_dict()
98
- )
99
- self._last_active_schedule = plugwise_extra_data.last_active_schedule
100
- self._previous_action_mode = (
101
- plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value
102
- )
103
 
104
  def __init__(
105
  self,
@@ -108,19 +119,29 @@
108
  ) -> None:
109
  """Set up the Plugwise API."""
110
  super().__init__(coordinator, device_id)
111
- self._attr_unique_id = f"{device_id}-climate"
112
 
113
- gateway_id: str = coordinator.api.gateway_id
 
114
  self._gateway_data = coordinator.data[gateway_id]
 
115
  self._location = device_id
116
- if (location := self.device.get("location")) is not None:
117
  self._location = location
 
 
 
 
 
 
 
 
 
118
 
119
  # Determine supported features
120
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
121
  if (
122
- self.coordinator.api.cooling_present
123
- and coordinator.api.smile.name != "Adam"
124
  ):
125
  self._attr_supported_features = (
126
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
@@ -129,79 +150,91 @@
129
  self._attr_supported_features |= (
130
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
131
  )
132
- if presets := self.device.get("preset_modes"):
133
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
134
- self._attr_preset_modes = presets
135
 
136
- self._attr_min_temp = self.device["thermostat"]["lower_bound"]
137
- self._attr_max_temp = min(self.device["thermostat"]["upper_bound"], 35.0)
138
- # Ensure we don't drop below 0.1
139
- self._attr_target_temperature_step = max(
140
- self.device["thermostat"]["resolution"], 0.1
141
- )
142
 
143
- @property
144
- def current_temperature(self) -> float:
145
- """Return the current temperature."""
146
- return self.device["sensors"]["temperature"]
 
 
 
 
 
147
 
148
  @property
149
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
150
  """Return text specific state data to be restored."""
151
  return PlugwiseClimateExtraStoredData(
152
- last_active_schedule=self._last_active_schedule,
153
- previous_action_mode=self._previous_action_mode,
154
  )
155
 
156
  @property
157
- def target_temperature(self) -> float:
 
 
 
 
 
158
  """Return the temperature we try to reach.
159
 
160
  Connected to the HVACMode combination of AUTO-HEAT.
161
  """
162
 
163
- return self.device["thermostat"]["setpoint"]
164
 
165
  @property
166
- def target_temperature_high(self) -> float:
167
  """Return the temperature we try to reach in case of cooling.
168
 
169
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
170
  """
171
- return self.device["thermostat"]["setpoint_high"]
172
 
173
  @property
174
- def target_temperature_low(self) -> float:
175
  """Return the heating temperature we try to reach in case of heating.
176
 
177
  Connected to the HVACMode combination AUTO-HEAT_COOL.
178
  """
179
- return self.device["thermostat"]["setpoint_low"]
180
 
181
  @property
182
  def hvac_mode(self) -> HVACMode:
183
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
184
- if (
185
- mode := self.device.get("climate_mode")
186
- ) is None or mode not in self.hvac_modes:
187
- return HVACMode.HEAT
188
- return HVACMode(mode)
 
 
 
 
 
 
189
 
190
  @property
191
  def hvac_modes(self) -> list[HVACMode]:
192
  """Return a list of available HVACModes."""
193
  hvac_modes: list[HVACMode] = []
194
- if "regulation_modes" in self._gateway_data:
195
  hvac_modes.append(HVACMode.OFF)
196
 
197
- if self.device.get("available_schedules"):
198
  hvac_modes.append(HVACMode.AUTO)
199
 
200
- if self.coordinator.api.cooling_present:
201
- if "regulation_modes" in self._gateway_data:
202
- if "heating" in self._gateway_data["regulation_modes"]:
203
  hvac_modes.append(HVACMode.HEAT)
204
- if "cooling" in self._gateway_data["regulation_modes"]:
205
  hvac_modes.append(HVACMode.COOL)
206
  else:
207
  hvac_modes.append(HVACMode.HEAT_COOL)
@@ -211,19 +244,19 @@
211
  return hvac_modes
212
 
213
  @property
214
- def hvac_action(self) -> HVACAction:
215
  """Return the current running hvac operation if supported."""
216
  # Keep track of the previous hvac_action mode.
217
  # When no cooling available, _previous_action_mode is always heating
218
  if (
219
- "regulation_modes" in self._gateway_data
220
- and HVACAction.COOLING.value in self._gateway_data["regulation_modes"]
221
  ):
222
- mode = self._gateway_data["select_regulation_mode"]
223
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
224
  self._previous_action_mode = mode
225
 
226
- if (action := self.device.get("control_state")) is not None:
227
  return HVACAction(action)
228
 
229
  return HVACAction.IDLE
@@ -231,95 +264,87 @@
231
  @property
232
  def preset_mode(self) -> str | None:
233
  """Return the current preset mode."""
234
- return self.device.get("active_preset")
235
 
236
  @plugwise_command
237
  async def async_set_temperature(self, **kwargs: Any) -> None:
238
  """Set new target temperature."""
239
  data: dict[str, Any] = {}
240
  if ATTR_TEMPERATURE in kwargs:
241
- data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
242
  if ATTR_TARGET_TEMP_HIGH in kwargs:
243
- data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
244
  if ATTR_TARGET_TEMP_LOW in kwargs:
245
- data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
 
 
246
 
247
  if mode := kwargs.get(ATTR_HVAC_MODE):
248
  await self.async_set_hvac_mode(mode)
249
 
250
- await self.coordinator.api.set_temperature(self._location, data)
 
 
 
251
 
252
- def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str | None:
253
- """Return the API regulation value for a manual HVAC mode, or None."""
254
  if hvac_mode == HVACMode.HEAT:
255
- return HVACAction.HEATING.value
256
  if hvac_mode == HVACMode.COOL:
257
- return HVACAction.COOLING.value
258
- return None
259
 
260
  @plugwise_command
261
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
262
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
 
 
263
  if hvac_mode == self.hvac_mode:
264
  return
265
 
266
- api = self.coordinator.api
267
- current_schedule = self.device.get("select_schedule")
268
-
269
- # OFF: single API call
270
  if hvac_mode == HVACMode.OFF:
271
- await api.set_regulation_mode(hvac_mode.value)
272
  return
273
 
274
- # Manual mode (heat/cool/heat_cool) without a schedule: set regulation only
275
- if (
276
- current_schedule is None
277
- and hvac_mode != HVACMode.AUTO
278
- and (
279
- regulation := self._regulation_mode_for_hvac(hvac_mode)
280
- or self._previous_action_mode
281
- )
282
- ):
283
- await api.set_regulation_mode(regulation)
 
 
 
 
 
 
 
 
 
 
284
  return
285
 
286
- # Manual mode: ensure regulation and turn off schedule when needed
287
- if hvac_mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL):
288
- regulation = self._regulation_mode_for_hvac(hvac_mode) or (
289
- self._previous_action_mode
290
- if self.hvac_mode in (HVACMode.HEAT_COOL, HVACMode.OFF)
291
- else None
292
- )
293
- if regulation:
294
- await api.set_regulation_mode(regulation)
295
-
296
- if (
297
- self.hvac_mode == HVACMode.OFF and current_schedule not in (None, "off")
298
- ) or (self.hvac_mode == HVACMode.AUTO and current_schedule is not None):
299
- await api.set_schedule_state(
300
- self._location, STATE_OFF, current_schedule
301
- )
302
  return
303
 
304
- # AUTO: restore schedule and regulation
305
- desired_schedule = current_schedule
306
- if desired_schedule and desired_schedule != "off":
307
- self._last_active_schedule = desired_schedule
308
- elif desired_schedule == "off":
309
- desired_schedule = self._last_active_schedule
310
-
311
- if not desired_schedule:
312
  raise HomeAssistantError(
313
  translation_domain=DOMAIN,
314
  translation_key=ERROR_NO_SCHEDULE,
315
  )
316
-
317
- if self._previous_action_mode:
318
- if self.hvac_mode == HVACMode.OFF:
319
- await api.set_regulation_mode(self._previous_action_mode)
320
- await api.set_schedule_state(self._location, STATE_ON, desired_schedule)
321
 
322
  @plugwise_command
323
  async def async_set_preset_mode(self, preset_mode: str) -> None:
324
  """Set the preset mode."""
325
- await self.coordinator.api.set_preset(self._location, preset_mode)
 
1
  """Plugwise Climate component for Home Assistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from dataclasses import asdict, dataclass
6
  from typing import Any
7
 
 
14
  HVACAction,
15
  HVACMode,
16
  )
17
+ from homeassistant.const import (
18
+ ATTR_NAME,
19
+ ATTR_TEMPERATURE,
20
+ STATE_OFF,
21
+ STATE_ON,
22
+ UnitOfTemperature,
23
+ )
24
  from homeassistant.core import HomeAssistant, callback
25
  from homeassistant.exceptions import HomeAssistantError
26
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
27
  from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
28
 
29
+ from .const import (
30
+ ACTIVE_PRESET,
31
+ AVAILABLE_SCHEDULES,
32
+ CLIMATE_MODE,
33
+ CONTROL_STATE,
34
+ DEV_CLASS,
35
+ DOMAIN,
36
+ LOCATION,
37
+ LOGGER,
38
+ LOWER_BOUND,
39
+ MASTER_THERMOSTATS,
40
+ REGULATION_MODES,
41
+ RESOLUTION,
42
+ SELECT_REGULATION_MODE,
43
+ SENSORS,
44
+ TARGET_TEMP,
45
+ TARGET_TEMP_HIGH,
46
+ TARGET_TEMP_LOW,
47
+ THERMOSTAT,
48
+ UPPER_BOUND,
49
+ )
50
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
51
  from .entity import PlugwiseEntity
52
  from .util import plugwise_command
 
55
  PARALLEL_UPDATES = 0
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  async def async_setup_entry(
59
  hass: HomeAssistant,
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
 
66
  @callback
67
  def _add_entities() -> None:
68
+ """Add Entities during init and runtime."""
69
  if not coordinator.new_devices:
70
  return
71
 
72
+ entities: list[PlugwiseClimateEntity] = []
73
+ gateway_name = coordinator.api.smile.name
74
+ for device_id in coordinator.new_devices:
75
+ device = coordinator.data[device_id]
76
+ if gateway_name == "Adam":
77
+ if device[DEV_CLASS] == "climate":
78
+ entities.append(
79
+ PlugwiseClimateEntity(coordinator, device_id)
80
+ )
81
+ LOGGER.debug("Add climate %s", device[ATTR_NAME])
82
+ elif device[DEV_CLASS] in MASTER_THERMOSTATS:
83
+ entities.append(
84
+ PlugwiseClimateEntity(coordinator, device_id)
85
+ )
86
+ LOGGER.debug("Add climate %s", device[ATTR_NAME])
87
+
88
+ async_add_entities(entities)
89
 
90
  _add_entities()
91
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
92
 
93
 
94
+ @dataclass
95
+ class PlugwiseClimateExtraStoredData(ExtraStoredData):
96
+ """Object to hold extra stored data."""
97
+
98
+ last_active_schedule: str | None
99
+ previous_action_mode: str | None
100
+
101
+ def as_dict(self) -> dict[str, Any]:
102
+ """Return a dict representation of the text data."""
103
+ return asdict(self)
104
+
105
+
106
  class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity):
107
  """Representation of a Plugwise thermostat."""
108
 
109
+ _attr_has_entity_name = True
110
  _attr_name = None
111
  _attr_temperature_unit = UnitOfTemperature.CELSIUS
112
  _attr_translation_key = DOMAIN
113
+ _enable_turn_on_off_backwards_compatibility = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  def __init__(
116
  self,
 
119
  ) -> None:
120
  """Set up the Plugwise API."""
121
  super().__init__(coordinator, device_id)
 
122
 
123
+ self._api = coordinator.api
124
+ gateway_id: str = self._api.gateway_id
125
  self._gateway_data = coordinator.data[gateway_id]
126
+ self._last_active_schedule: str | None = None
127
  self._location = device_id
128
+ if (location := self.device.get(LOCATION)) is not None:
129
  self._location = location
130
+ self._previous_action_mode = HVACAction.HEATING.value
131
+
132
+ self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
133
+ self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
134
+ # Ensure we don't drop below 0.1
135
+ self._attr_target_temperature_step = max(
136
+ self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5), 0.1
137
+ )
138
+ self._attr_unique_id = f"{device_id}-climate"
139
 
140
  # Determine supported features
141
  self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
142
  if (
143
+ self._api.cooling_present
144
+ and self._api.smile.name != "Adam"
145
  ):
146
  self._attr_supported_features = (
147
  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
 
150
  self._attr_supported_features |= (
151
  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
152
  )
153
+ if presets := self.device.get("preset_modes", None): # can be NONE
154
  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
155
+ self._attr_preset_modes = presets
156
 
157
+ async def async_added_to_hass(self) -> None:
158
+ """Run when entity about to be added."""
 
 
 
 
159
 
160
+ extra_data = await self.async_get_last_extra_data()
161
+ if extra_data is not None:
162
+ data = extra_data.as_dict()
163
+ self._last_active_schedule = data.get("last_active_schedule")
164
+ self._previous_action_mode = (
165
+ data.get("previous_action_mode") or HVACAction.HEATING.value
166
+ )
167
+
168
+ await super().async_added_to_hass()
169
 
170
  @property
171
  def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
172
  """Return text specific state data to be restored."""
173
  return PlugwiseClimateExtraStoredData(
174
+ self._last_active_schedule,
175
+ self._previous_action_mode,
176
  )
177
 
178
  @property
179
+ def current_temperature(self) -> float | None:
180
+ """Return the current temperature."""
181
+ return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE)
182
+
183
+ @property
184
+ def target_temperature(self) -> float | None:
185
  """Return the temperature we try to reach.
186
 
187
  Connected to the HVACMode combination of AUTO-HEAT.
188
  """
189
 
190
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP)
191
 
192
  @property
193
+ def target_temperature_high(self) -> float | None:
194
  """Return the temperature we try to reach in case of cooling.
195
 
196
  Connected to the HVACMode combination of AUTO-HEAT_COOL.
197
  """
198
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_HIGH)
199
 
200
  @property
201
+ def target_temperature_low(self) -> float | None:
202
  """Return the heating temperature we try to reach in case of heating.
203
 
204
  Connected to the HVACMode combination AUTO-HEAT_COOL.
205
  """
206
+ return self.device.get(THERMOSTAT, {}).get(TARGET_TEMP_LOW)
207
 
208
  @property
209
  def hvac_mode(self) -> HVACMode:
210
  """Return HVAC operation ie. auto, cool, heat, heat_cool, or off mode."""
211
+ mode = self.device.get(CLIMATE_MODE)
212
+ if mode is None:
213
+ return HVACMode.HEAT # pragma: no cover
214
+ try:
215
+ hvac = HVACMode(mode)
216
+ except ValueError: # pragma: no cover
217
+ return HVACMode.HEAT # pragma: no cover
218
+ if hvac not in self.hvac_modes:
219
+ return HVACMode.HEAT # pragma: no cover
220
+
221
+ return hvac
222
 
223
  @property
224
  def hvac_modes(self) -> list[HVACMode]:
225
  """Return a list of available HVACModes."""
226
  hvac_modes: list[HVACMode] = []
227
+ if REGULATION_MODES in self._gateway_data:
228
  hvac_modes.append(HVACMode.OFF)
229
 
230
+ if self.device.get(AVAILABLE_SCHEDULES, []):
231
  hvac_modes.append(HVACMode.AUTO)
232
 
233
+ if self._api.cooling_present:
234
+ if REGULATION_MODES in self._gateway_data:
235
+ if "heating" in self._gateway_data[REGULATION_MODES]:
236
  hvac_modes.append(HVACMode.HEAT)
237
+ if "cooling" in self._gateway_data[REGULATION_MODES]:
238
  hvac_modes.append(HVACMode.COOL)
239
  else:
240
  hvac_modes.append(HVACMode.HEAT_COOL)
 
244
  return hvac_modes
245
 
246
  @property
247
+ def hvac_action(self) -> HVACAction: # pw-beta add to Core
248
  """Return the current running hvac operation if supported."""
249
  # Keep track of the previous hvac_action mode.
250
  # When no cooling available, _previous_action_mode is always heating
251
  if (
252
+ REGULATION_MODES in self._gateway_data
253
+ and HVACAction.COOLING.value in self._gateway_data[REGULATION_MODES]
254
  ):
255
+ mode = self._gateway_data[SELECT_REGULATION_MODE]
256
  if mode in (HVACAction.COOLING.value, HVACAction.HEATING.value):
257
  self._previous_action_mode = mode
258
 
259
+ if (action := self.device.get(CONTROL_STATE)) is not None:
260
  return HVACAction(action)
261
 
262
  return HVACAction.IDLE
 
264
  @property
265
  def preset_mode(self) -> str | None:
266
  """Return the current preset mode."""
267
+ return self.device.get(ACTIVE_PRESET)
268
 
269
  @plugwise_command
270
  async def async_set_temperature(self, **kwargs: Any) -> None:
271
  """Set new target temperature."""
272
  data: dict[str, Any] = {}
273
  if ATTR_TEMPERATURE in kwargs:
274
+ data[TARGET_TEMP] = kwargs.get(ATTR_TEMPERATURE)
275
  if ATTR_TARGET_TEMP_HIGH in kwargs:
276
+ data[TARGET_TEMP_HIGH] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
277
  if ATTR_TARGET_TEMP_LOW in kwargs:
278
+ data[TARGET_TEMP_LOW] = kwargs.get(ATTR_TARGET_TEMP_LOW)
279
+
280
+ # Upstream removed input-valid check
281
 
282
  if mode := kwargs.get(ATTR_HVAC_MODE):
283
  await self.async_set_hvac_mode(mode)
284
 
285
+ await self._api.set_temperature(self._location, data)
286
+
287
+ def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str:
288
+ """Return the API regulation value for a manual HVAC mode.
289
 
290
+ The function inputs are limited to the HVACModes HEAT and COOL.
291
+ """
292
  if hvac_mode == HVACMode.HEAT:
293
+ mode = HVACAction.HEATING.value
294
  if hvac_mode == HVACMode.COOL:
295
+ mode = HVACAction.COOLING.value
296
+ return mode
297
 
298
  @plugwise_command
299
  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
300
  """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule)."""
301
+
302
+ # Early exit if no mode change
303
  if hvac_mode == self.hvac_mode:
304
  return
305
 
306
+ # Adam only: set to HVACMode.OFF
 
 
 
307
  if hvac_mode == HVACMode.OFF:
308
+ await self._api.set_regulation_mode(hvac_mode.value)
309
  return
310
 
311
+ current_schedule = self.device.get("select_schedule")
312
+ schedule_is_active = current_schedule not in (None, "off")
313
+ # Adam only: transition from HVACMode.OFF
314
+ if self.hvac_mode == HVACMode.OFF:
315
+ if hvac_mode == HVACMode.AUTO:
316
+ if not schedule_is_active and self._last_active_schedule is None:
317
+ raise HomeAssistantError(
318
+ translation_domain=DOMAIN,
319
+ translation_key=ERROR_NO_SCHEDULE,
320
+ )
321
+ await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule)
322
+ await self._api.set_regulation_mode(self._previous_action_mode)
323
+ return
324
+
325
+ # Transition to manual mode
326
+ if schedule_is_active:
327
+ await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule)
328
+ self._last_active_schedule = current_schedule
329
+ regulation = self._regulation_mode_for_hvac(hvac_mode)
330
+ await self._api.set_regulation_mode(regulation)
331
  return
332
 
333
+ # Common - transition from auto = schedule off
334
+ if self.hvac_mode == HVACMode.AUTO:
335
+ await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule)
336
+ self._last_active_schedule = current_schedule
 
 
 
 
 
 
 
 
 
 
 
 
337
  return
338
 
339
+ # Common - transition to auto = schedule on
340
+ if self._last_active_schedule is None:
 
 
 
 
 
 
341
  raise HomeAssistantError(
342
  translation_domain=DOMAIN,
343
  translation_key=ERROR_NO_SCHEDULE,
344
  )
345
+ await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule)
 
 
 
 
346
 
347
  @plugwise_command
348
  async def async_set_preset_mode(self, preset_mode: str) -> None:
349
  """Set the preset mode."""
350
+ await self._api.set_preset(self._location, preset_mode)
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/config_flow.py RENAMED
@@ -1,5 +1,10 @@
1
  """Config flow for Plugwise integration."""
2
 
 
 
 
 
 
3
  import logging
4
  from typing import Any, Self
5
 
@@ -14,7 +19,15 @@
14
  )
15
  import voluptuous as vol
16
 
17
- from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
 
 
 
 
 
 
 
 
18
  from homeassistant.const import (
19
  ATTR_CONFIGURATION_URL,
20
  CONF_BASE,
@@ -22,9 +35,13 @@
22
  CONF_NAME,
23
  CONF_PASSWORD,
24
  CONF_PORT,
 
25
  CONF_USERNAME,
26
  )
27
- from homeassistant.core import HomeAssistant
 
 
 
28
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
29
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
30
 
@@ -30,22 +47,35 @@
30
 
31
  from .const import (
32
  ANNA_WITH_ADAM,
 
33
  DEFAULT_PORT,
 
34
  DEFAULT_USERNAME,
35
  DOMAIN,
36
  FLOW_SMILE,
37
  FLOW_STRETCH,
 
 
38
  SMILE,
39
  SMILE_OPEN_THERM,
40
  SMILE_THERMO,
41
  STRETCH,
42
  STRETCH_USERNAME,
43
- UNKNOWN_SMILE,
 
 
44
  ZEROCONF_MAP,
45
  )
46
 
 
 
 
 
 
47
  _LOGGER = logging.getLogger(__name__)
48
 
 
 
49
  SMILE_RECONF_SCHEMA = vol.Schema(
50
  {
51
  vol.Required(CONF_HOST): str,
@@ -53,21 +83,31 @@
53
  )
54
 
55
 
56
- def smile_user_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
57
  """Generate base schema for gateways."""
58
- schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
59
-
60
- if not discovery_info:
61
- schema = schema.extend(
62
  {
63
  vol.Required(CONF_HOST): str,
 
64
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
65
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
66
  ),
67
  }
68
  )
69
 
70
- return schema
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
@@ -101,7 +141,7 @@
101
  errors[CONF_BASE] = "invalid_auth"
102
  except InvalidSetupError:
103
  errors[CONF_BASE] = "invalid_setup"
104
- except InvalidXMLError, ResponseError:
105
  errors[CONF_BASE] = "response_error"
106
  except UnsupportedDeviceError:
107
  errors[CONF_BASE] = "unsupported"
@@ -117,9 +157,10 @@
117
  """Handle a config flow for Plugwise Smile."""
118
 
119
  VERSION = 1
 
120
 
121
  discovery_info: ZeroconfServiceInfo | None = None
122
- product: str = UNKNOWN_SMILE
123
  _username: str = DEFAULT_USERNAME
124
 
125
  async def async_step_zeroconf(
@@ -128,17 +169,21 @@
128
  """Prepare configuration for a discovered Plugwise Smile."""
129
  self.discovery_info = discovery_info
130
  _properties = discovery_info.properties
131
-
 
132
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
 
 
 
133
  if config_entry := await self.async_set_unique_id(unique_id):
134
  try:
135
  await validate_input(
136
  self.hass,
137
  {
138
  CONF_HOST: discovery_info.host,
 
139
  CONF_PORT: discovery_info.port,
140
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
141
- CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
142
  },
143
  )
144
  except Exception: # noqa: BLE001
@@ -151,12 +196,6 @@
151
  }
152
  )
153
 
154
- if DEFAULT_USERNAME not in unique_id:
155
- self._username = STRETCH_USERNAME
156
- self.product = _product = _properties.get("product", UNKNOWN_SMILE)
157
- _version = _properties.get("version", "n/a")
158
- _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
159
-
160
  # This is an Anna, but we already have config entries.
161
  # Assuming that the user has already configured Adam, aborting discovery.
162
  if self._async_current_entries() and _product == SMILE_THERMO:
@@ -166,14 +205,15 @@
166
  # In that case, we need to cancel the Anna flow, as the Adam should
167
  # be added.
168
  if self.hass.config_entries.flow.async_has_matching_flow(self):
169
- return self.async_abort(reason=ANNA_WITH_ADAM)
170
 
 
171
  self.context.update(
172
  {
173
- "title_placeholders": {CONF_NAME: _name},
174
  ATTR_CONFIGURATION_URL: (
175
  f"http://{discovery_info.host}:{discovery_info.port}"
176
- ),
177
  }
178
  )
179
  return await self.async_step_user()
@@ -190,6 +230,7 @@
190
 
191
  return False
192
 
 
193
  async def async_step_user(
194
  self, user_input: dict[str, Any] | None = None
195
  ) -> ConfigFlowResult:
@@ -206,18 +247,19 @@
206
  api, errors = await verify_connection(self.hass, user_input)
207
  if api:
208
  await self.async_set_unique_id(
209
- api.smile.hostname or api.gateway_id,
210
- raise_on_progress=False,
211
  )
212
  self._abort_if_unique_id_configured()
213
  return self.async_create_entry(title=api.smile.name, data=user_input)
214
 
 
215
  return self.async_show_form(
216
  step_id=SOURCE_USER,
217
- data_schema=smile_user_schema(self.discovery_info),
218
  errors=errors,
219
  )
220
 
 
221
  async def async_step_reconfigure(
222
  self, user_input: dict[str, Any] | None = None
223
  ) -> ConfigFlowResult:
@@ -227,7 +269,7 @@
227
  reconfigure_entry = self._get_reconfigure_entry()
228
 
229
  if user_input:
230
- # Keep current username and password
231
  full_input = {
232
  CONF_HOST: user_input.get(CONF_HOST),
233
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
@@ -238,8 +280,7 @@
238
  api, errors = await verify_connection(self.hass, full_input)
239
  if api:
240
  await self.async_set_unique_id(
241
- api.smile.hostname or api.gateway_id,
242
- raise_on_progress=False,
243
  )
244
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
245
  return self.async_update_reload_and_abort(
@@ -256,3 +297,66 @@
256
  description_placeholders={"title": reconfigure_entry.title},
257
  errors=errors,
258
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """Config flow for Plugwise integration."""
2
 
3
+ # pylint: disable=hass-config-flow-polling-field
4
+
5
+ from __future__ import annotations
6
+
7
+ from copy import deepcopy
8
  import logging
9
  from typing import Any, Self
10
 
 
19
  )
20
  import voluptuous as vol
21
 
22
+ from homeassistant.config_entries import (
23
+ SOURCE_USER,
24
+ ConfigEntry,
25
+ ConfigFlow,
26
+ ConfigFlowResult,
27
+ OptionsFlow,
28
+ )
29
+
30
+ # Upstream
31
  from homeassistant.const import (
32
  ATTR_CONFIGURATION_URL,
33
  CONF_BASE,
 
35
  CONF_NAME,
36
  CONF_PASSWORD,
37
  CONF_PORT,
38
+ CONF_SCAN_INTERVAL,
39
  CONF_USERNAME,
40
  )
41
+
42
+ # Upstream
43
+ from homeassistant.core import HomeAssistant, callback
44
+ from homeassistant.helpers import config_validation as cv
45
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
46
  from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
47
 
 
47
 
48
  from .const import (
49
  ANNA_WITH_ADAM,
50
+ CONF_REFRESH_INTERVAL, # pw-beta option
51
  DEFAULT_PORT,
52
+ DEFAULT_UPDATE_INTERVAL,
53
  DEFAULT_USERNAME,
54
  DOMAIN,
55
  FLOW_SMILE,
56
  FLOW_STRETCH,
57
+ INIT,
58
+ P1_UPDATE_INTERVAL,
59
  SMILE,
60
  SMILE_OPEN_THERM,
61
  SMILE_THERMO,
62
  STRETCH,
63
  STRETCH_USERNAME,
64
+ THERMOSTAT,
65
+ TITLE_PLACEHOLDERS,
66
+ VERSION,
67
  ZEROCONF_MAP,
68
  )
69
 
70
+ # Upstream
71
+ from .coordinator import PlugwiseDataUpdateCoordinator
72
+
73
+ type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
74
+
75
  _LOGGER = logging.getLogger(__name__)
76
 
77
+ # Upstream basically the whole file (excluding the pw-beta options)
78
+
79
  SMILE_RECONF_SCHEMA = vol.Schema(
80
  {
81
  vol.Required(CONF_HOST): str,
 
83
  )
84
 
85
 
86
+ def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> vol.Schema:
87
  """Generate base schema for gateways."""
88
+ if not cf_input: # no discovery- or user-input available
89
+ return vol.Schema(
 
 
90
  {
91
  vol.Required(CONF_HOST): str,
92
+ vol.Required(CONF_PASSWORD): str,
93
  vol.Required(CONF_USERNAME, default=SMILE): vol.In(
94
  {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
95
  ),
96
  }
97
  )
98
 
99
+ if isinstance(cf_input, ZeroconfServiceInfo):
100
+ return vol.Schema({vol.Required(CONF_PASSWORD): str})
101
+
102
+ return vol.Schema(
103
+ {
104
+ vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
105
+ vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
106
+ vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
107
+ {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
108
+ ),
109
+ }
110
+ )
111
 
112
 
113
  async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
 
141
  errors[CONF_BASE] = "invalid_auth"
142
  except InvalidSetupError:
143
  errors[CONF_BASE] = "invalid_setup"
144
+ except (InvalidXMLError, ResponseError):
145
  errors[CONF_BASE] = "response_error"
146
  except UnsupportedDeviceError:
147
  errors[CONF_BASE] = "unsupported"
 
157
  """Handle a config flow for Plugwise Smile."""
158
 
159
  VERSION = 1
160
+ MINOR_VERSION = 1
161
 
162
  discovery_info: ZeroconfServiceInfo | None = None
163
+ product: str = "Unknown Smile"
164
  _username: str = DEFAULT_USERNAME
165
 
166
  async def async_step_zeroconf(
 
169
  """Prepare configuration for a discovered Plugwise Smile."""
170
  self.discovery_info = discovery_info
171
  _properties = discovery_info.properties
172
+ _version = _properties.get(VERSION, "n/a")
173
+ self.product = _product = _properties.get("product", "Unknown Smile")
174
  unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
175
+ if DEFAULT_USERNAME not in unique_id:
176
+ self._username = STRETCH_USERNAME
177
+
178
  if config_entry := await self.async_set_unique_id(unique_id):
179
  try:
180
  await validate_input(
181
  self.hass,
182
  {
183
  CONF_HOST: discovery_info.host,
184
+ CONF_PASSWORD: config_entry.data[CONF_PASSWORD],
185
  CONF_PORT: discovery_info.port,
186
  CONF_USERNAME: config_entry.data[CONF_USERNAME],
 
187
  },
188
  )
189
  except Exception: # noqa: BLE001
 
196
  }
197
  )
198
 
 
 
 
 
 
 
199
  # This is an Anna, but we already have config entries.
200
  # Assuming that the user has already configured Adam, aborting discovery.
201
  if self._async_current_entries() and _product == SMILE_THERMO:
 
205
  # In that case, we need to cancel the Anna flow, as the Adam should
206
  # be added.
207
  if self.hass.config_entries.flow.async_has_matching_flow(self):
208
+ return self.async_abort(reason="anna_with_adam")
209
 
210
+ _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
211
  self.context.update(
212
  {
213
+ TITLE_PLACEHOLDERS: {CONF_NAME: _name},
214
  ATTR_CONFIGURATION_URL: (
215
  f"http://{discovery_info.host}:{discovery_info.port}"
216
+ )
217
  }
218
  )
219
  return await self.async_step_user()
 
230
 
231
  return False
232
 
233
+
234
  async def async_step_user(
235
  self, user_input: dict[str, Any] | None = None
236
  ) -> ConfigFlowResult:
 
247
  api, errors = await verify_connection(self.hass, user_input)
248
  if api:
249
  await self.async_set_unique_id(
250
+ api.smile.hostname or api.gateway_id, raise_on_progress=False
 
251
  )
252
  self._abort_if_unique_id_configured()
253
  return self.async_create_entry(title=api.smile.name, data=user_input)
254
 
255
+ configure_input = self.discovery_info or user_input
256
  return self.async_show_form(
257
  step_id=SOURCE_USER,
258
+ data_schema=smile_user_schema(configure_input),
259
  errors=errors,
260
  )
261
 
262
+
263
  async def async_step_reconfigure(
264
  self, user_input: dict[str, Any] | None = None
265
  ) -> ConfigFlowResult:
 
269
  reconfigure_entry = self._get_reconfigure_entry()
270
 
271
  if user_input:
272
+ # Redefine ingest existing username and password
273
  full_input = {
274
  CONF_HOST: user_input.get(CONF_HOST),
275
  CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
 
280
  api, errors = await verify_connection(self.hass, full_input)
281
  if api:
282
  await self.async_set_unique_id(
283
+ api.smile.hostname or api.gateway_id, raise_on_progress=False
 
284
  )
285
  self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
286
  return self.async_update_reload_and_abort(
 
297
  description_placeholders={"title": reconfigure_entry.title},
298
  errors=errors,
299
  )
300
+
301
+
302
+ @staticmethod
303
+ @callback
304
+ def async_get_options_flow(
305
+ config_entry: PlugwiseConfigEntry,
306
+ ) -> PlugwiseOptionsFlowHandler: # pw-beta options
307
+ """Get the options flow for this handler."""
308
+ return PlugwiseOptionsFlowHandler(config_entry)
309
+
310
+
311
+ # pw-beta - change the scan-interval via CONFIGURE
312
+ # pw-beta - change the frontend refresh interval via CONFIGURE
313
+ class PlugwiseOptionsFlowHandler(OptionsFlow): # pw-beta options
314
+ """Plugwise option flow."""
315
+
316
+ def __init__(self, config_entry: ConfigEntry) -> None:
317
+ """Initialize options flow."""
318
+ self.options = deepcopy(dict(config_entry.options))
319
+
320
+ def _create_options_schema(self, coordinator: PlugwiseDataUpdateCoordinator) -> vol.Schema:
321
+ interval = DEFAULT_UPDATE_INTERVAL
322
+ if coordinator.api.smile.type == "power":
323
+ interval = P1_UPDATE_INTERVAL
324
+ schema = {
325
+ vol.Optional(
326
+ CONF_SCAN_INTERVAL,
327
+ default=self.options.get(CONF_SCAN_INTERVAL, interval.seconds),
328
+ ): vol.All(cv.positive_int, vol.Clamp(min=10)),
329
+ } # pw-beta
330
+
331
+ if coordinator.api.smile.type == THERMOSTAT:
332
+ schema.update({
333
+ vol.Optional(
334
+ CONF_REFRESH_INTERVAL,
335
+ default=self.options.get(CONF_REFRESH_INTERVAL, 1.5),
336
+ ): vol.All(vol.Coerce(float), vol.Range(min=1.5, max=10.0)),
337
+ }) # pw-beta
338
+
339
+ return vol.Schema(schema)
340
+
341
+ async def async_step_none(
342
+ self, user_input: dict[str, Any] | None = None
343
+ ) -> ConfigFlowResult: # pragma: no cover
344
+ """No options available."""
345
+ if user_input is not None:
346
+ # Apparently not possible to abort an options flow at the moment
347
+ return self.async_create_entry(title="", data=self.options)
348
+ return self.async_show_form(step_id="none")
349
+
350
+ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
351
+ """Manage the Plugwise options."""
352
+ if not self.config_entry.data.get(CONF_HOST):
353
+ return await self.async_step_none(user_input) # pragma: no cover
354
+
355
+ if user_input is not None:
356
+ return self.async_create_entry(title="", data=user_input)
357
+
358
+ coordinator = self.config_entry.runtime_data
359
+ return self.async_show_form(
360
+ step_id=INIT,
361
+ data_schema=self._create_options_schema(coordinator)
362
+ )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/const.py RENAMED
@@ -6,28 +6,156 @@
6
 
7
  from homeassistant.const import Platform
8
 
 
 
9
  DOMAIN: Final = "plugwise"
10
 
11
  LOGGER = logging.getLogger(__package__)
12
 
13
- ANNA_WITH_ADAM: Final = "anna_with_adam"
14
  API: Final = "api"
15
- AVAILABLE: Final = "available"
16
- DEV_CLASS: Final = "dev_class"
17
- FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
18
- FLOW_STRETCH: Final = "stretch (Stretch)"
19
- FLOW_TYPE: Final = "flow_type"
20
  GATEWAY: Final = "gateway"
21
  LOCATION: Final = "location"
22
- PW_TYPE: Final = "plugwise_type"
23
  REBOOT: Final = "reboot"
24
  SMILE: Final = "smile"
25
- SMILE_OPEN_THERM: Final = "smile_open_therm"
26
- SMILE_THERMO: Final = "smile_thermo"
27
  STRETCH: Final = "stretch"
28
  STRETCH_USERNAME: Final = "stretch"
29
- UNKNOWN_SMILE: Final = "Unknown Smile"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
31
  PLATFORMS: Final[list[str]] = [
32
  Platform.BINARY_SENSOR,
33
  Platform.BUTTON,
@@ -37,6 +165,18 @@
37
  Platform.SENSOR,
38
  Platform.SWITCH,
39
  ]
 
 
 
 
 
 
 
 
 
 
 
 
40
  ZEROCONF_MAP: Final[dict[str, str]] = {
41
  "smile": "Smile P1",
42
  "smile_thermo": "Smile Anna",
@@ -55,7 +195,7 @@
55
  "select_gateway_mode",
56
  "select_regulation_mode",
57
  "select_schedule",
58
- "select_zone_profile",
59
  ]
60
  type SelectOptionsType = Literal[
61
  "available_schedules",
@@ -64,25 +204,3 @@
64
  "regulation_modes",
65
  "zone_profiles",
66
  ]
67
-
68
- # Default directives
69
- DEFAULT_MAX_TEMP: Final = 30
70
- DEFAULT_MIN_TEMP: Final = 4
71
- DEFAULT_PORT: Final = 80
72
- DEFAULT_UPDATE_INTERVAL = timedelta(seconds=60)
73
- DEFAULT_USERNAME: Final = "smile"
74
- P1_UPDATE_INTERVAL = timedelta(seconds=10)
75
-
76
- MASTER_THERMOSTATS: Final[list[str]] = [
77
- "thermostat",
78
- "thermostatic_radiator_valve",
79
- "zone_thermometer",
80
- "zone_thermostat",
81
- ]
82
-
83
- # Select constants
84
- SELECT_DHW_MODE: Final = "select_dhw_mode"
85
- SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
86
- SELECT_REGULATION_MODE: Final = "select_regulation_mode"
87
- SELECT_SCHEDULE: Final = "select_schedule"
88
- SELECT_ZONE_PROFILE: Final = "select_zone_profile"
 
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
+ SWITCH_GROUPS: Final[tuple[str, str]] = ("report", "switching")
28
+ UNIQUE_IDS: Final = "unique_ids"
29
+ ZIGBEE_MAC_ADDRESS: Final = "zigbee_mac_address"
30
+
31
+ # Binary Sensor constants
32
+ BINARY_SENSORS: Final = "binary_sensors"
33
+ BATTERY_STATE: Final = "low_battery"
34
+ COMPRESSOR_STATE: Final = "compressor_state"
35
+ COOLING_ENABLED: Final = "cooling_enabled"
36
+ COOLING_STATE: Final = "cooling_state"
37
+ DHW_STATE: Final = "dhw_state"
38
+ FLAME_STATE: Final = "flame_state"
39
+ HEATING_STATE: Final = "heating_state"
40
+ PLUGWISE_NOTIFICATION: Final = "plugwise_notification"
41
+ SECONDARY_BOILER_STATE: Final = "secondary_boiler_state"
42
+
43
+ # Climate constants
44
+ ACTIVE_PRESET: Final = "active_preset"
45
+ CLIMATE_MODE: Final = "climate_mode"
46
+ CONTROL_STATE: Final = "control_state"
47
+ COOLING_PRESENT: Final ="cooling_present"
48
+ DEV_CLASS: Final = "dev_class"
49
+ NONE : Final = "None"
50
+ TARGET_TEMP: Final = "setpoint"
51
+ TARGET_TEMP_HIGH: Final = "setpoint_high"
52
+ TARGET_TEMP_LOW: Final = "setpoint_low"
53
+ THERMOSTAT: Final = "thermostat"
54
+
55
+ # Config_flow constants
56
+ ANNA_WITH_ADAM: Final = "anna_with_adam"
57
+ CONTEXT: Final = "context"
58
+ FLOW_ID: Final = "flow_id"
59
+ FLOW_NET: Final = "Network: Smile/Stretch"
60
+ FLOW_SMILE: Final = "Smile (Adam/Anna/P1)"
61
+ FLOW_STRETCH: Final = "Stretch (Stretch)"
62
+ FLOW_TYPE: Final = "flow_type"
63
+ INIT: Final = "init"
64
+ PRODUCT: Final = "product"
65
+ SMILE_OPEN_THERM: Final = "smile_open_therm"
66
+ SMILE_THERMO: Final = "smile_thermo"
67
+ TITLE_PLACEHOLDERS: Final = "title_placeholders"
68
+ VERSION: Final = "version"
69
+
70
+ # Entity constants
71
+ AVAILABLE: Final = "available"
72
+ FIRMWARE: Final = "firmware"
73
+ HARDWARE: Final = "hardware"
74
+ MODEL: Final = "model"
75
+ MODEL_ID: Final = "model_id"
76
+ VENDOR: Final = "vendor"
77
+
78
+ # Number constants
79
+ MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
80
+ MAX_DHW_TEMP: Final = "max_dhw_temperature"
81
+ LOWER_BOUND: Final = "lower_bound"
82
+ RESOLUTION: Final = "resolution"
83
+ TEMPERATURE_OFFSET: Final = "temperature_offset"
84
+ UPPER_BOUND: Final = "upper_bound"
85
+
86
+ # Sensor constants
87
+ DHW_TEMP: Final = "dhw_temperature"
88
+ DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
89
+ EL_CONSUMED: Final = "electricity_consumed"
90
+ EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
91
+ EL_CONS_OP_CUMULATIVE: Final = "electricity_consumed_off_peak_cumulative"
92
+ EL_CONS_OP_INTERVAL: Final = "electricity_consumed_off_peak_interval"
93
+ EL_CONS_OP_POINT: Final = "electricity_consumed_off_peak_point"
94
+ EL_CONS_P_CUMULATIVE: Final = "electricity_consumed_peak_cumulative"
95
+ EL_CONS_P_INTERVAL: Final = "electricity_consumed_peak_interval"
96
+ EL_CONS_P_POINT: Final = "electricity_consumed_peak_point"
97
+ EL_CONS_POINT: Final = "electricity_consumed_point"
98
+ EL_PH1_CONSUMED: Final = "electricity_phase_one_consumed"
99
+ EL_PH2_CONSUMED: Final = "electricity_phase_two_consumed"
100
+ EL_PH3_CONSUMED: Final = "electricity_phase_three_consumed"
101
+ EL_PH1_PRODUCED: Final = "electricity_phase_one_produced"
102
+ EL_PH2_PRODUCED: Final = "electricity_phase_two_produced"
103
+ EL_PH3_PRODUCED: Final = "electricity_phase_three_produced"
104
+ EL_PRODUCED: Final = "electricity_produced"
105
+ EL_PROD_INTERVAL: Final = "electricity_produced_interval"
106
+ EL_PROD_OP_CUMULATIVE: Final = "electricity_produced_off_peak_cumulative"
107
+ EL_PROD_OP_INTERVAL: Final = "electricity_produced_off_peak_interval"
108
+ EL_PROD_OP_POINT: Final = "electricity_produced_off_peak_point"
109
+ EL_PROD_P_CUMULATIVE: Final = "electricity_produced_peak_cumulative"
110
+ EL_PROD_P_INTERVAL: Final = "electricity_produced_peak_interval"
111
+ EL_PROD_P_POINT: Final = "electricity_produced_peak_point"
112
+ EL_PROD_POINT: Final = "electricity_produced_point"
113
+ GAS_CONS_CUMULATIVE: Final = "gas_consumed_cumulative"
114
+ GAS_CONS_INTERVAL: Final = "gas_consumed_interval"
115
+ INTENDED_BOILER_TEMP: Final = "intended_boiler_temperature"
116
+ MOD_LEVEL: Final = "modulation_level"
117
+ NET_EL_POINT: Final = "net_electricity_point"
118
+ NET_EL_CUMULATIVE: Final = "net_electricity_cumulative"
119
+ OUTDOOR_AIR_TEMP: Final = "outdoor_air_temperature"
120
+ OUTDOOR_TEMP: Final = "outdoor_temperature"
121
+ RETURN_TEMP: Final = "return_temperature"
122
+ SENSORS: Final = "sensors"
123
+ TEMP_DIFF: Final = "temperature_difference"
124
+ VALVE_POS: Final = "valve_position"
125
+ VOLTAGE_PH1: Final = "voltage_phase_one"
126
+ VOLTAGE_PH2: Final = "voltage_phase_two"
127
+ VOLTAGE_PH3: Final = "voltage_phase_three"
128
+ WATER_TEMP: Final = "water_temperature"
129
+ WATER_PRESSURE: Final = "water_pressure"
130
+
131
+ # Select constants
132
+ AVAILABLE_SCHEDULES: Final = "available_schedules"
133
+ DHW_MODES: Final = "dhw_modes"
134
+ GATEWAY_MODES: Final = "gateway_modes"
135
+ REGULATION_MODES: Final = "regulation_modes"
136
+ ZONE_PROFILES: Final = "zone_profiles"
137
+ SELECT_DHW_MODE: Final = "select_dhw_mode"
138
+ SELECT_GATEWAY_MODE: Final = "select_gateway_mode"
139
+ SELECT_REGULATION_MODE: Final = "select_regulation_mode"
140
+ SELECT_SCHEDULE: Final = "select_schedule"
141
+ SELECT_ZONE_PROFILE: Final = "select_zone_profile"
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_TIMEOUT: Final[int] = 30
154
+ DEFAULT_UPDATE_INTERVAL: Final = timedelta(seconds=60)
155
+ DEFAULT_USERNAME: Final = "smile"
156
+ P1_UPDATE_INTERVAL: Final = timedelta(seconds=10)
157
 
158
+ # --- Const for Plugwise Smile and Stretch
159
  PLATFORMS: Final[list[str]] = [
160
  Platform.BINARY_SENSOR,
161
  Platform.BUTTON,
 
165
  Platform.SENSOR,
166
  Platform.SWITCH,
167
  ]
168
+ SERVICE_DELETE: Final = "delete_notification"
169
+ SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"]
170
+
171
+ # Climate const:
172
+ MASTER_THERMOSTATS: Final[list[str]] = [
173
+ "thermostat",
174
+ "zone_thermometer",
175
+ "zone_thermostat",
176
+ "thermostatic_radiator_valve",
177
+ ]
178
+
179
+ # Config_flow const:
180
  ZEROCONF_MAP: Final[dict[str, str]] = {
181
  "smile": "Smile P1",
182
  "smile_thermo": "Smile Anna",
 
195
  "select_gateway_mode",
196
  "select_regulation_mode",
197
  "select_schedule",
198
+ "select_zone_profile"
199
  ]
200
  type SelectOptionsType = Literal[
201
  "available_schedules",
 
204
  "regulation_modes",
205
  "zone_profiles",
206
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/coordinator.py RENAMED
@@ -1,6 +1,7 @@
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
- from packaging.version import Version
 
4
  from plugwise import GwEntityData, Smile
5
  from plugwise.exceptions import (
6
  ConnectionFailedError,
@@ -13,21 +14,29 @@
13
  )
14
 
15
  from homeassistant.config_entries import ConfigEntry
16
- from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 
 
 
 
 
 
17
  from homeassistant.core import HomeAssistant
18
  from homeassistant.exceptions import ConfigEntryError
19
  from homeassistant.helpers import device_registry as dr
20
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
21
  from homeassistant.helpers.debounce import Debouncer
22
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
23
 
24
  from .const import (
25
- DEFAULT_PORT,
26
  DEFAULT_UPDATE_INTERVAL,
27
- DEFAULT_USERNAME,
28
  DOMAIN,
 
29
  LOGGER,
30
  P1_UPDATE_INTERVAL,
 
31
  )
32
 
33
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
@@ -38,29 +47,36 @@
38
 
39
  config_entry: PlugwiseConfigEntry
40
 
41
- def __init__(self, hass: HomeAssistant, config_entry: PlugwiseConfigEntry) -> None:
 
 
 
 
 
42
  """Initialize the coordinator."""
43
  super().__init__(
44
  hass,
45
  LOGGER,
46
  config_entry=config_entry,
47
  name=DOMAIN,
 
 
48
  update_interval=DEFAULT_UPDATE_INTERVAL,
49
  # Don't refresh immediately, give the device time to process
50
  # the change in state before we query it.
51
  request_refresh_debouncer=Debouncer(
52
  hass,
53
  LOGGER,
54
- cooldown=1.5,
55
  immediate=False,
56
  ),
57
  )
58
 
59
  self.api = Smile(
60
  host=self.config_entry.data[CONF_HOST],
61
- username=self.config_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
62
  password=self.config_entry.data[CONF_PASSWORD],
63
- port=self.config_entry.data.get(CONF_PORT, DEFAULT_PORT),
 
64
  websession=async_get_clientsession(hass, verify_ssl=False),
65
  )
66
  self._connected: bool = False
@@ -76,8 +92,15 @@
76
  """
77
  version = await self.api.connect()
78
  self._connected = isinstance(version, Version)
79
- if self._connected and self.api.smile.type == "power":
80
- self.update_interval = P1_UPDATE_INTERVAL
 
 
 
 
 
 
 
81
 
82
  async def _async_setup(self) -> None:
83
  """Initialize the update_data process."""
@@ -129,25 +152,27 @@
129
  translation_key="unsupported_firmware",
130
  ) from err
131
 
 
132
  self._add_remove_devices(data)
133
  self._update_device_firmware(data)
134
  return data
135
 
136
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
137
  """Add new Plugwise devices, remove non-existing devices."""
 
 
 
 
 
 
 
 
138
  set_of_data = set(data)
139
- # Check for new or removed devices,
140
- # 'new_devices' contains all devices present in 'data'
141
- # at init ('self._current_devices' is empty) this is
142
- # required for the proper initialization of all the
143
- # present platform entities.
144
  self.new_devices = set_of_data - self._current_devices
145
  for device_id in self.new_devices:
146
- self._firmware_list.setdefault(device_id, data[device_id].get("firmware"))
147
 
148
- current_devices = (
149
- self._stored_devices if not self._current_devices else self._current_devices
150
- )
151
  self._current_devices = set_of_data
152
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
153
  self._remove_devices(removed_devices)
@@ -156,9 +181,7 @@
156
  """Clean registries when removed devices found."""
157
  device_reg = dr.async_get(self.hass)
158
  for device_id in removed_devices:
159
- if (
160
- device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
161
- ) is not None:
162
  device_reg.async_update_device(
163
  device_entry.id, remove_config_entry_id=self.config_entry.entry_id
164
  )
@@ -171,14 +194,15 @@
171
 
172
  self._firmware_list.pop(device_id, None)
173
 
 
174
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
175
  """Detect firmware changes and update the device registry."""
176
  for device_id, device in data.items():
177
  # Only update firmware when the key is present and not None, to avoid
178
  # wiping stored firmware on partial or transient updates.
179
- if "firmware" not in device:
180
  continue
181
- new_firmware = device.get("firmware")
182
  if new_firmware is None:
183
  continue
184
  if (
@@ -192,9 +216,7 @@
192
  def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
193
  """Update device sw_version in device_registry."""
194
  device_reg = dr.async_get(self.hass)
195
- if (
196
- device_entry := device_reg.async_get_device({(DOMAIN, device_id)})
197
- ) is not None:
198
  device_reg.async_update_device(device_entry.id, sw_version=firmware)
199
  LOGGER.debug(
200
  "Firmware in device_registry updated for %s %s %s",
 
1
  """DataUpdateCoordinator for Plugwise."""
2
 
3
+ from datetime import timedelta
4
+
5
  from plugwise import GwEntityData, Smile
6
  from plugwise.exceptions import (
7
  ConnectionFailedError,
 
14
  )
15
 
16
  from homeassistant.config_entries import ConfigEntry
17
+ from homeassistant.const import (
18
+ CONF_HOST,
19
+ CONF_PASSWORD,
20
+ CONF_PORT,
21
+ CONF_SCAN_INTERVAL, # pw-beta options
22
+ CONF_USERNAME,
23
+ )
24
  from homeassistant.core import HomeAssistant
25
  from homeassistant.exceptions import ConfigEntryError
26
  from homeassistant.helpers import device_registry as dr
27
  from homeassistant.helpers.aiohttp_client import async_get_clientsession
28
  from homeassistant.helpers.debounce import Debouncer
29
  from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
30
+ from packaging.version import Version
31
 
32
  from .const import (
 
33
  DEFAULT_UPDATE_INTERVAL,
34
+ DEV_CLASS,
35
  DOMAIN,
36
+ FIRMWARE,
37
  LOGGER,
38
  P1_UPDATE_INTERVAL,
39
+ SWITCH_GROUPS,
40
  )
41
 
42
  type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]
 
47
 
48
  config_entry: PlugwiseConfigEntry
49
 
50
+ def __init__(
51
+ self,
52
+ hass: HomeAssistant,
53
+ cooldown: float,
54
+ config_entry: PlugwiseConfigEntry,
55
+ ) -> None: # pw-beta cooldown
56
  """Initialize the coordinator."""
57
  super().__init__(
58
  hass,
59
  LOGGER,
60
  config_entry=config_entry,
61
  name=DOMAIN,
62
+ # Core directly updates from const's DEFAULT_SCAN_INTERVAL
63
+ # Upstream check correct progress for adjusting
64
  update_interval=DEFAULT_UPDATE_INTERVAL,
65
  # Don't refresh immediately, give the device time to process
66
  # the change in state before we query it.
67
  request_refresh_debouncer=Debouncer(
68
  hass,
69
  LOGGER,
70
+ cooldown=cooldown,
71
  immediate=False,
72
  ),
73
  )
74
 
75
  self.api = Smile(
76
  host=self.config_entry.data[CONF_HOST],
 
77
  password=self.config_entry.data[CONF_PASSWORD],
78
+ port=self.config_entry.data[CONF_PORT],
79
+ username=self.config_entry.data[CONF_USERNAME],
80
  websession=async_get_clientsession(hass, verify_ssl=False),
81
  )
82
  self._connected: bool = False
 
92
  """
93
  version = await self.api.connect()
94
  self._connected = isinstance(version, Version)
95
+ if self._connected:
96
+ if self.api.smile.type == "power":
97
+ self.update_interval = P1_UPDATE_INTERVAL
98
+ if (custom_time := self.config_entry.options.get(CONF_SCAN_INTERVAL)) is not None:
99
+ self.update_interval = timedelta(
100
+ seconds=int(custom_time)
101
+ ) # pragma: no cover # pw-beta options
102
+
103
+ LOGGER.debug("DUC update interval: %s", self.update_interval) # pw-beta options
104
 
105
  async def _async_setup(self) -> None:
106
  """Initialize the update_data process."""
 
152
  translation_key="unsupported_firmware",
153
  ) from err
154
 
155
+ LOGGER.debug("%s data: %s", self.api.smile.name, data)
156
  self._add_remove_devices(data)
157
  self._update_device_firmware(data)
158
  return data
159
 
160
  def _add_remove_devices(self, data: dict[str, GwEntityData]) -> None:
161
  """Add new Plugwise devices, remove non-existing devices."""
162
+ # Block switch-groups, use HA group helper instead to create switch-groups
163
+ for device_id, device in data.copy().items():
164
+ if device.get(DEV_CLASS) in SWITCH_GROUPS:
165
+ data.pop(device_id)
166
+
167
+ # Collect new or removed devices,
168
+ # 'new_devices' contains all devices present in 'data' at init ('self._current_devices' is empty)
169
+ # this is required for the initialization of the available platform entities.
170
  set_of_data = set(data)
 
 
 
 
 
171
  self.new_devices = set_of_data - self._current_devices
172
  for device_id in self.new_devices:
173
+ self._firmware_list.setdefault(device_id, data[device_id].get(FIRMWARE))
174
 
175
+ current_devices = self._stored_devices if not self._current_devices else self._current_devices
 
 
176
  self._current_devices = set_of_data
177
  if removed_devices := (current_devices - set_of_data): # device(s) to remove
178
  self._remove_devices(removed_devices)
 
181
  """Clean registries when removed devices found."""
182
  device_reg = dr.async_get(self.hass)
183
  for device_id in removed_devices:
184
+ if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
 
 
185
  device_reg.async_update_device(
186
  device_entry.id, remove_config_entry_id=self.config_entry.entry_id
187
  )
 
194
 
195
  self._firmware_list.pop(device_id, None)
196
 
197
+
198
  def _update_device_firmware(self, data: dict[str, GwEntityData]) -> None:
199
  """Detect firmware changes and update the device registry."""
200
  for device_id, device in data.items():
201
  # Only update firmware when the key is present and not None, to avoid
202
  # wiping stored firmware on partial or transient updates.
203
+ if FIRMWARE not in device:
204
  continue
205
+ new_firmware = device.get(FIRMWARE)
206
  if new_firmware is None:
207
  continue
208
  if (
 
216
  def _update_firmware_in_dr(self, device_id: str, firmware: str | None) -> bool:
217
  """Update device sw_version in device_registry."""
218
  device_reg = dr.async_get(self.hass)
219
+ if (device_entry := device_reg.async_get_device({(DOMAIN, device_id)})) is not None:
 
 
220
  device_reg.async_update_device(device_entry.id, sw_version=firmware)
221
  LOGGER.debug(
222
  "Firmware in device_registry updated for %s %s %s",
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/diagnostics.py RENAMED
@@ -1,5 +1,7 @@
1
  """Diagnostics support for Plugwise."""
2
 
 
 
3
  from typing import Any
4
 
5
  from homeassistant.core import HomeAssistant
 
1
  """Diagnostics support for Plugwise."""
2
 
3
+ from __future__ import annotations
4
+
5
  from typing import Any
6
 
7
  from homeassistant.core import HomeAssistant
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/entity.py RENAMED
@@ -1,6 +1,8 @@
1
  """Generic Plugwise Entity Class."""
2
 
3
- from plugwise import GwEntityData
 
 
4
 
5
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
6
  from homeassistant.helpers.device_registry import (
@@ -10,7 +12,19 @@
10
  )
11
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
12
 
13
- from .const import AVAILABLE, DOMAIN
 
 
 
 
 
 
 
 
 
 
 
 
14
  from .coordinator import PlugwiseDataUpdateCoordinator
15
 
16
 
@@ -41,9 +55,9 @@
41
 
42
  # Build connections set
43
  connections = set()
44
- if mac := self.device.get("mac_address"):
45
  connections.add((CONNECTION_NETWORK_MAC, mac))
46
- if zigbee_mac := self.device.get("zigbee_mac_address"):
47
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
48
 
49
  # Set base device info
@@ -51,12 +65,12 @@
51
  configuration_url=configuration_url,
52
  identifiers={(DOMAIN, device_id)},
53
  connections=connections,
54
- manufacturer=self.device.get("vendor"),
55
- model=self.device.get("model"),
56
- model_id=self.device.get("model_id"),
57
  name=api.smile.name,
58
- sw_version=self.device.get("firmware"),
59
- hw_version=self.device.get("hardware"),
60
  )
61
 
62
  # Add extra info if not the gateway device
@@ -72,8 +86,10 @@
72
  def available(self) -> bool:
73
  """Return if entity is available."""
74
  return (
 
 
75
  self._dev_id in self.coordinator.data
76
- and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
77
  and super().available
78
  )
79
 
 
1
  """Generic Plugwise Entity Class."""
2
 
3
+ from __future__ import annotations
4
+
5
+ from plugwise.constants import GwEntityData
6
 
7
  from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
8
  from homeassistant.helpers.device_registry import (
 
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
 
 
55
 
56
  # Build connections set
57
  connections = set()
58
+ if mac := self.device.get(MAC_ADDRESS):
59
  connections.add((CONNECTION_NETWORK_MAC, mac))
60
+ if zigbee_mac := self.device.get(ZIGBEE_MAC_ADDRESS):
61
  connections.add((CONNECTION_ZIGBEE, zigbee_mac))
62
 
63
  # Set base device info
 
65
  configuration_url=configuration_url,
66
  identifiers={(DOMAIN, device_id)},
67
  connections=connections,
68
+ manufacturer=self.device.get(VENDOR),
69
+ model=self.device.get(MODEL),
70
+ model_id=self.device.get(MODEL_ID),
71
  name=api.smile.name,
72
+ sw_version=self.device.get(FIRMWARE),
73
+ hw_version=self.device.get(HARDWARE),
74
  )
75
 
76
  # Add extra info if not the gateway device
 
86
  def available(self) -> bool:
87
  """Return if entity is available."""
88
  return (
89
+ # Upstream: Do not change the AVAILABLE line below: some Plugwise devices and zones
90
+ # Upstream: do not provide their availability-status!
91
  self._dev_id in self.coordinator.data
92
+ and (self.device.get(AVAILABLE, True) is True)
93
  and super().available
94
  )
95
 
/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.11.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.11.3"],
11
+ "version": "0.64.1",
12
  "zeroconf": ["_plugwise._tcp.local."]
13
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/number.py RENAMED
@@ -1,5 +1,7 @@
1
  """Number platform for Plugwise integration."""
2
 
 
 
3
  from dataclasses import dataclass
4
 
5
  from homeassistant.components.number import (
@@ -12,7 +14,18 @@
12
  from homeassistant.core import HomeAssistant, callback
13
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
14
 
15
- from .const import NumberType
 
 
 
 
 
 
 
 
 
 
 
16
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
17
  from .entity import PlugwiseEntity
18
  from .util import plugwise_command
@@ -27,24 +40,25 @@
27
  key: NumberType
28
 
29
 
 
30
  NUMBER_TYPES = (
31
  PlugwiseNumberEntityDescription(
32
- key="maximum_boiler_temperature",
33
- translation_key="maximum_boiler_temperature",
34
  device_class=NumberDeviceClass.TEMPERATURE,
35
  entity_category=EntityCategory.CONFIG,
36
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
37
  ),
38
  PlugwiseNumberEntityDescription(
39
- key="max_dhw_temperature",
40
- translation_key="max_dhw_temperature",
41
  device_class=NumberDeviceClass.TEMPERATURE,
42
  entity_category=EntityCategory.CONFIG,
43
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
44
  ),
45
  PlugwiseNumberEntityDescription(
46
- key="temperature_offset",
47
- translation_key="temperature_offset",
48
  device_class=NumberDeviceClass.TEMPERATURE,
49
  entity_category=EntityCategory.CONFIG,
50
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@@ -57,7 +71,8 @@
57
  entry: PlugwiseConfigEntry,
58
  async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
- """Set up Plugwise number platform."""
 
61
  coordinator = entry.runtime_data
62
 
63
  @callback
@@ -66,12 +81,28 @@
66
  if not coordinator.new_devices:
67
  return
68
 
69
- async_add_entities(
70
- PlugwiseNumberEntity(coordinator, device_id, description)
71
- for device_id in coordinator.new_devices
72
- for description in NUMBER_TYPES
73
- if description.key in coordinator.data[device_id]
74
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  _add_entities()
77
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -90,26 +121,28 @@
90
  ) -> None:
91
  """Initiate Plugwise Number."""
92
  super().__init__(coordinator, device_id)
93
- self._attr_mode = NumberMode.BOX
94
- self._attr_native_max_value = self.device[description.key]["upper_bound"]
95
- self._attr_native_min_value = self.device[description.key]["lower_bound"]
96
- self._attr_unique_id = f"{device_id}-{description.key}"
97
  self.device_id = device_id
98
  self.entity_description = description
 
 
 
 
 
99
 
100
- native_step = self.device[description.key]["resolution"]
101
- if description.key != "temperature_offset":
102
  native_step = max(native_step, 0.5)
103
  self._attr_native_step = native_step
104
 
105
  @property
106
- def native_value(self) -> float:
107
  """Return the present setpoint value."""
108
- return self.device[self.entity_description.key]["setpoint"]
109
 
110
  @plugwise_command
111
  async def async_set_native_value(self, value: float) -> None:
112
  """Change to the new setpoint value."""
113
- await self.coordinator.api.set_number(
114
- self.device_id, self.entity_description.key, value
 
115
  )
 
1
  """Number platform for Plugwise integration."""
2
 
3
+ from __future__ import annotations
4
+
5
  from dataclasses import dataclass
6
 
7
  from homeassistant.components.number import (
 
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.device_id = device_id
125
  self.entity_description = description
126
+ self._attr_unique_id = f"{device_id}-{description.key}"
127
+ self._attr_mode = NumberMode.BOX
128
+ ctrl = self.device.get(description.key, {})
129
+ self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0) # Upstream const
130
+ self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0) # Upstream const
131
 
132
+ native_step = ctrl.get(RESOLUTION, 0.5) # 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
 
137
  @property
138
+ def native_value(self) -> float | None:
139
  """Return the present setpoint value."""
140
+ return self.device.get(self.entity_description.key, {}).get("setpoint")
141
 
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
@@ -1,5 +1,7 @@
1
  """Plugwise Select component for Home Assistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
 
5
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -8,14 +10,23 @@
8
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
9
 
10
  from .const import (
 
 
 
 
 
 
11
  SELECT_DHW_MODE,
12
  SELECT_GATEWAY_MODE,
13
  SELECT_REGULATION_MODE,
14
  SELECT_SCHEDULE,
15
  SELECT_ZONE_PROFILE,
 
16
  SelectOptionsType,
17
  SelectType,
18
  )
 
 
19
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
20
  from .entity import PlugwiseEntity
21
  from .util import plugwise_command
@@ -31,35 +42,36 @@
31
  options_key: SelectOptionsType
32
 
33
 
 
34
  SELECT_TYPES = (
35
  PlugwiseSelectEntityDescription(
36
  key=SELECT_SCHEDULE,
37
  translation_key=SELECT_SCHEDULE,
38
- options_key="available_schedules",
39
  ),
40
  PlugwiseSelectEntityDescription(
41
  key=SELECT_REGULATION_MODE,
42
  translation_key=SELECT_REGULATION_MODE,
43
  entity_category=EntityCategory.CONFIG,
44
- options_key="regulation_modes",
45
  ),
46
  PlugwiseSelectEntityDescription(
47
  key=SELECT_DHW_MODE,
48
  translation_key=SELECT_DHW_MODE,
49
  entity_category=EntityCategory.CONFIG,
50
- options_key="dhw_modes",
51
  ),
52
  PlugwiseSelectEntityDescription(
53
  key=SELECT_GATEWAY_MODE,
54
  translation_key=SELECT_GATEWAY_MODE,
55
  entity_category=EntityCategory.CONFIG,
56
- options_key="gateway_modes",
57
  ),
58
  PlugwiseSelectEntityDescription(
59
  key=SELECT_ZONE_PROFILE,
60
  translation_key=SELECT_ZONE_PROFILE,
61
  entity_category=EntityCategory.CONFIG,
62
- options_key="zone_profiles",
63
  ),
64
  )
65
 
@@ -69,7 +81,7 @@
69
  entry: PlugwiseConfigEntry,
70
  async_add_entities: AddConfigEntryEntitiesCallback,
71
  ) -> None:
72
- """Set up the Smile selector from a config entry."""
73
  coordinator = entry.runtime_data
74
 
75
  @callback
@@ -78,12 +90,27 @@
78
  if not coordinator.new_devices:
79
  return
80
 
81
- async_add_entities(
82
- PlugwiseSelectEntity(coordinator, device_id, description)
83
- for device_id in coordinator.new_devices
84
- for description in SELECT_TYPES
85
- if coordinator.data[device_id].get(description.options_key)
86
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  _add_entities()
89
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -106,25 +133,30 @@
106
  self.entity_description = entity_description
107
 
108
  self._location = device_id
109
- if (location := self.device.get("location")) is not None:
110
  self._location = location
111
 
112
  @property
113
  def current_option(self) -> str | None:
114
  """Return the selected entity option to represent the entity state."""
115
- return self.device[self.entity_description.key]
116
 
117
  @property
118
  def options(self) -> list[str]:
119
  """Return the available select-options."""
120
- return self.device[self.entity_description.options_key]
121
 
122
  @plugwise_command
123
  async def async_select_option(self, option: str) -> None:
124
  """Change to the selected entity option.
125
 
126
- self._location and STATE_ON are required for the thermostat-schedule select.
127
  """
128
  await self.coordinator.api.set_select(
129
  self.entity_description.key, self._location, option, STATE_ON
130
  )
 
 
 
 
 
 
1
  """Plugwise Select component for Home Assistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from dataclasses import dataclass
6
 
7
  from homeassistant.components.select import SelectEntity, SelectEntityDescription
 
10
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
11
 
12
  from .const import (
13
+ AVAILABLE_SCHEDULES,
14
+ DHW_MODES,
15
+ GATEWAY_MODES,
16
+ LOCATION,
17
+ LOGGER,
18
+ REGULATION_MODES,
19
  SELECT_DHW_MODE,
20
  SELECT_GATEWAY_MODE,
21
  SELECT_REGULATION_MODE,
22
  SELECT_SCHEDULE,
23
  SELECT_ZONE_PROFILE,
24
+ ZONE_PROFILES,
25
  SelectOptionsType,
26
  SelectType,
27
  )
28
+
29
+ # Upstream consts
30
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
31
  from .entity import PlugwiseEntity
32
  from .util import plugwise_command
 
42
  options_key: SelectOptionsType
43
 
44
 
45
+ # Upstream
46
  SELECT_TYPES = (
47
  PlugwiseSelectEntityDescription(
48
  key=SELECT_SCHEDULE,
49
  translation_key=SELECT_SCHEDULE,
50
+ options_key=AVAILABLE_SCHEDULES,
51
  ),
52
  PlugwiseSelectEntityDescription(
53
  key=SELECT_REGULATION_MODE,
54
  translation_key=SELECT_REGULATION_MODE,
55
  entity_category=EntityCategory.CONFIG,
56
+ options_key=REGULATION_MODES,
57
  ),
58
  PlugwiseSelectEntityDescription(
59
  key=SELECT_DHW_MODE,
60
  translation_key=SELECT_DHW_MODE,
61
  entity_category=EntityCategory.CONFIG,
62
+ options_key=DHW_MODES,
63
  ),
64
  PlugwiseSelectEntityDescription(
65
  key=SELECT_GATEWAY_MODE,
66
  translation_key=SELECT_GATEWAY_MODE,
67
  entity_category=EntityCategory.CONFIG,
68
+ options_key=GATEWAY_MODES,
69
  ),
70
  PlugwiseSelectEntityDescription(
71
  key=SELECT_ZONE_PROFILE,
72
  translation_key=SELECT_ZONE_PROFILE,
73
  entity_category=EntityCategory.CONFIG,
74
+ options_key=ZONE_PROFILES,
75
  ),
76
  )
77
 
 
81
  entry: PlugwiseConfigEntry,
82
  async_add_entities: AddConfigEntryEntitiesCallback,
83
  ) -> None:
84
+ """Set up Plugwise selector from a config entry."""
85
  coordinator = entry.runtime_data
86
 
87
  @callback
 
90
  if not coordinator.new_devices:
91
  return
92
 
93
+ # Upstream consts
94
+ # async_add_entities(
95
+ # PlugwiseSelectEntity(coordinator, device_id, description)
96
+ # for device_id in coordinator.new_devices
97
+ # for description in SELECT_TYPES
98
+ # if description.options_key in coordinator.data.devices[device_id]
99
+ # )
100
+ # pw-beta alternative for debugging
101
+ entities: list[PlugwiseSelectEntity] = []
102
+ for device_id in coordinator.new_devices:
103
+ device = coordinator.data[device_id]
104
+ for description in SELECT_TYPES:
105
+ if device.get(description.options_key):
106
+ entities.append(
107
+ PlugwiseSelectEntity(coordinator, device_id, description)
108
+ )
109
+ LOGGER.debug(
110
+ "Add %s %s selector", device["name"], description.translation_key
111
+ )
112
+
113
+ async_add_entities(entities)
114
 
115
  _add_entities()
116
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
133
  self.entity_description = entity_description
134
 
135
  self._location = device_id
136
+ if (location := self.device.get(LOCATION)) is not None:
137
  self._location = location
138
 
139
  @property
140
  def current_option(self) -> str | None:
141
  """Return the selected entity option to represent the entity state."""
142
+ return self.device.get(self.entity_description.key)
143
 
144
  @property
145
  def options(self) -> list[str]:
146
  """Return the available select-options."""
147
+ return self.device.get(self.entity_description.options_key, [])
148
 
149
  @plugwise_command
150
  async def async_select_option(self, option: str) -> None:
151
  """Change to the selected entity option.
152
 
153
+ Location ID and STATE_ON are required for the thermostat-schedule select.
154
  """
155
  await self.coordinator.api.set_select(
156
  self.entity_description.key, self._location, option, STATE_ON
157
  )
158
+ LOGGER.debug(
159
+ "Set %s to %s was successful",
160
+ self.entity_description.key,
161
+ option,
162
+ )
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/sensor.py RENAMED
@@ -1,5 +1,7 @@
1
  """Plugwise Sensor component for Home Assistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
 
5
  from plugwise.constants import SensorType
@@ -11,6 +13,7 @@
11
  SensorStateClass,
12
  )
13
  from homeassistant.const import (
 
14
  LIGHT_LUX,
15
  PERCENTAGE,
16
  EntityCategory,
@@ -25,6 +28,57 @@
25
  from homeassistant.core import HomeAssistant, callback
26
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
29
  from .entity import PlugwiseEntity
30
 
@@ -39,299 +93,301 @@
39
  key: SensorType
40
 
41
 
42
- SENSORS: tuple[PlugwiseSensorEntityDescription, ...] = (
 
43
  PlugwiseSensorEntityDescription(
44
- key="setpoint",
45
- translation_key="setpoint",
46
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
47
  device_class=SensorDeviceClass.TEMPERATURE,
48
  state_class=SensorStateClass.MEASUREMENT,
49
  ),
50
  PlugwiseSensorEntityDescription(
51
- key="setpoint_high",
52
  translation_key="cooling_setpoint",
53
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
54
  device_class=SensorDeviceClass.TEMPERATURE,
55
  state_class=SensorStateClass.MEASUREMENT,
56
  ),
57
  PlugwiseSensorEntityDescription(
58
- key="setpoint_low",
59
  translation_key="heating_setpoint",
60
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
61
  device_class=SensorDeviceClass.TEMPERATURE,
62
  state_class=SensorStateClass.MEASUREMENT,
63
  ),
64
  PlugwiseSensorEntityDescription(
65
- key="temperature",
66
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
67
  device_class=SensorDeviceClass.TEMPERATURE,
68
  state_class=SensorStateClass.MEASUREMENT,
69
  ),
70
  PlugwiseSensorEntityDescription(
71
- key="intended_boiler_temperature",
72
- translation_key="intended_boiler_temperature",
73
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
74
  device_class=SensorDeviceClass.TEMPERATURE,
75
  entity_category=EntityCategory.DIAGNOSTIC,
76
  state_class=SensorStateClass.MEASUREMENT,
77
  ),
78
  PlugwiseSensorEntityDescription(
79
- key="temperature_difference",
80
- translation_key="temperature_difference",
81
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
82
  device_class=SensorDeviceClass.TEMPERATURE,
83
  entity_category=EntityCategory.DIAGNOSTIC,
84
  state_class=SensorStateClass.MEASUREMENT,
85
  ),
86
  PlugwiseSensorEntityDescription(
87
- key="outdoor_temperature",
88
- translation_key="outdoor_temperature",
89
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
90
  device_class=SensorDeviceClass.TEMPERATURE,
91
  entity_category=EntityCategory.DIAGNOSTIC,
92
  state_class=SensorStateClass.MEASUREMENT,
 
93
  ),
94
  PlugwiseSensorEntityDescription(
95
- key="outdoor_air_temperature",
96
- translation_key="outdoor_air_temperature",
97
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
98
  device_class=SensorDeviceClass.TEMPERATURE,
99
  entity_category=EntityCategory.DIAGNOSTIC,
100
  state_class=SensorStateClass.MEASUREMENT,
101
  ),
102
  PlugwiseSensorEntityDescription(
103
- key="water_temperature",
104
- translation_key="water_temperature",
105
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
106
  device_class=SensorDeviceClass.TEMPERATURE,
107
  entity_category=EntityCategory.DIAGNOSTIC,
108
  state_class=SensorStateClass.MEASUREMENT,
109
  ),
110
  PlugwiseSensorEntityDescription(
111
- key="return_temperature",
112
- translation_key="return_temperature",
113
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
114
  device_class=SensorDeviceClass.TEMPERATURE,
115
  entity_category=EntityCategory.DIAGNOSTIC,
116
  state_class=SensorStateClass.MEASUREMENT,
117
  ),
118
  PlugwiseSensorEntityDescription(
119
- key="electricity_consumed",
120
- translation_key="electricity_consumed",
121
  native_unit_of_measurement=UnitOfPower.WATT,
122
  device_class=SensorDeviceClass.POWER,
123
  state_class=SensorStateClass.MEASUREMENT,
124
  ),
125
  PlugwiseSensorEntityDescription(
126
- key="electricity_produced",
127
- translation_key="electricity_produced",
128
  native_unit_of_measurement=UnitOfPower.WATT,
129
  device_class=SensorDeviceClass.POWER,
130
  state_class=SensorStateClass.MEASUREMENT,
131
  entity_registry_enabled_default=False,
132
  ),
133
  PlugwiseSensorEntityDescription(
134
- key="electricity_consumed_interval",
135
- translation_key="electricity_consumed_interval",
136
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
137
  device_class=SensorDeviceClass.ENERGY,
138
  state_class=SensorStateClass.TOTAL,
139
  ),
140
  PlugwiseSensorEntityDescription(
141
- key="electricity_consumed_peak_interval",
142
- translation_key="electricity_consumed_peak_interval",
143
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
144
  device_class=SensorDeviceClass.ENERGY,
145
  state_class=SensorStateClass.TOTAL,
146
  ),
147
  PlugwiseSensorEntityDescription(
148
- key="electricity_consumed_off_peak_interval",
149
- translation_key="electricity_consumed_off_peak_interval",
150
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
151
  device_class=SensorDeviceClass.ENERGY,
152
  state_class=SensorStateClass.TOTAL,
153
  ),
154
  PlugwiseSensorEntityDescription(
155
- key="electricity_produced_interval",
156
- translation_key="electricity_produced_interval",
157
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
158
  device_class=SensorDeviceClass.ENERGY,
159
  state_class=SensorStateClass.TOTAL,
160
  entity_registry_enabled_default=False,
161
  ),
162
  PlugwiseSensorEntityDescription(
163
- key="electricity_produced_peak_interval",
164
- translation_key="electricity_produced_peak_interval",
165
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
166
  device_class=SensorDeviceClass.ENERGY,
167
  state_class=SensorStateClass.TOTAL,
168
  ),
169
  PlugwiseSensorEntityDescription(
170
- key="electricity_produced_off_peak_interval",
171
- translation_key="electricity_produced_off_peak_interval",
172
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
173
  device_class=SensorDeviceClass.ENERGY,
174
  state_class=SensorStateClass.TOTAL,
175
  ),
176
  PlugwiseSensorEntityDescription(
177
- key="electricity_consumed_point",
178
- translation_key="electricity_consumed_point",
179
  device_class=SensorDeviceClass.POWER,
180
  native_unit_of_measurement=UnitOfPower.WATT,
181
  state_class=SensorStateClass.MEASUREMENT,
182
  ),
183
  PlugwiseSensorEntityDescription(
184
- key="electricity_consumed_off_peak_point",
185
- translation_key="electricity_consumed_off_peak_point",
186
  native_unit_of_measurement=UnitOfPower.WATT,
187
  device_class=SensorDeviceClass.POWER,
188
  state_class=SensorStateClass.MEASUREMENT,
189
  ),
190
  PlugwiseSensorEntityDescription(
191
- key="electricity_consumed_peak_point",
192
- translation_key="electricity_consumed_peak_point",
193
  native_unit_of_measurement=UnitOfPower.WATT,
194
  device_class=SensorDeviceClass.POWER,
195
  state_class=SensorStateClass.MEASUREMENT,
196
  ),
197
  PlugwiseSensorEntityDescription(
198
- key="electricity_consumed_off_peak_cumulative",
199
- translation_key="electricity_consumed_off_peak_cumulative",
200
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
201
  device_class=SensorDeviceClass.ENERGY,
202
  state_class=SensorStateClass.TOTAL_INCREASING,
203
  ),
204
  PlugwiseSensorEntityDescription(
205
- key="electricity_consumed_peak_cumulative",
206
- translation_key="electricity_consumed_peak_cumulative",
207
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
208
  device_class=SensorDeviceClass.ENERGY,
209
  state_class=SensorStateClass.TOTAL_INCREASING,
210
  ),
211
  PlugwiseSensorEntityDescription(
212
- key="electricity_produced_point",
213
- translation_key="electricity_produced_point",
214
  device_class=SensorDeviceClass.POWER,
215
  native_unit_of_measurement=UnitOfPower.WATT,
216
  state_class=SensorStateClass.MEASUREMENT,
217
  ),
218
  PlugwiseSensorEntityDescription(
219
- key="electricity_produced_off_peak_point",
220
- translation_key="electricity_produced_off_peak_point",
221
  native_unit_of_measurement=UnitOfPower.WATT,
222
  device_class=SensorDeviceClass.POWER,
223
  state_class=SensorStateClass.MEASUREMENT,
224
  ),
225
  PlugwiseSensorEntityDescription(
226
- key="electricity_produced_peak_point",
227
- translation_key="electricity_produced_peak_point",
228
  native_unit_of_measurement=UnitOfPower.WATT,
229
  device_class=SensorDeviceClass.POWER,
230
  state_class=SensorStateClass.MEASUREMENT,
231
  ),
232
  PlugwiseSensorEntityDescription(
233
- key="electricity_produced_off_peak_cumulative",
234
- translation_key="electricity_produced_off_peak_cumulative",
235
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
236
  device_class=SensorDeviceClass.ENERGY,
237
  state_class=SensorStateClass.TOTAL_INCREASING,
238
  ),
239
  PlugwiseSensorEntityDescription(
240
- key="electricity_produced_peak_cumulative",
241
- translation_key="electricity_produced_peak_cumulative",
242
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
243
  device_class=SensorDeviceClass.ENERGY,
244
  state_class=SensorStateClass.TOTAL_INCREASING,
245
  ),
246
  PlugwiseSensorEntityDescription(
247
- key="electricity_phase_one_consumed",
248
- translation_key="electricity_phase_one_consumed",
249
  device_class=SensorDeviceClass.POWER,
250
  native_unit_of_measurement=UnitOfPower.WATT,
251
  state_class=SensorStateClass.MEASUREMENT,
252
  ),
253
  PlugwiseSensorEntityDescription(
254
- key="electricity_phase_two_consumed",
255
- translation_key="electricity_phase_two_consumed",
256
  device_class=SensorDeviceClass.POWER,
257
  native_unit_of_measurement=UnitOfPower.WATT,
258
  state_class=SensorStateClass.MEASUREMENT,
259
  ),
260
  PlugwiseSensorEntityDescription(
261
- key="electricity_phase_three_consumed",
262
- translation_key="electricity_phase_three_consumed",
263
  device_class=SensorDeviceClass.POWER,
264
  native_unit_of_measurement=UnitOfPower.WATT,
265
  state_class=SensorStateClass.MEASUREMENT,
266
  ),
267
  PlugwiseSensorEntityDescription(
268
- key="electricity_phase_one_produced",
269
- translation_key="electricity_phase_one_produced",
270
  device_class=SensorDeviceClass.POWER,
271
  native_unit_of_measurement=UnitOfPower.WATT,
272
  state_class=SensorStateClass.MEASUREMENT,
273
  ),
274
  PlugwiseSensorEntityDescription(
275
- key="electricity_phase_two_produced",
276
- translation_key="electricity_phase_two_produced",
277
  device_class=SensorDeviceClass.POWER,
278
  native_unit_of_measurement=UnitOfPower.WATT,
279
  state_class=SensorStateClass.MEASUREMENT,
280
  ),
281
  PlugwiseSensorEntityDescription(
282
- key="electricity_phase_three_produced",
283
- translation_key="electricity_phase_three_produced",
284
  device_class=SensorDeviceClass.POWER,
285
  native_unit_of_measurement=UnitOfPower.WATT,
286
  state_class=SensorStateClass.MEASUREMENT,
287
  ),
288
  PlugwiseSensorEntityDescription(
289
- key="voltage_phase_one",
290
- translation_key="voltage_phase_one",
291
  device_class=SensorDeviceClass.VOLTAGE,
292
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
293
  state_class=SensorStateClass.MEASUREMENT,
294
  entity_registry_enabled_default=False,
295
  ),
296
  PlugwiseSensorEntityDescription(
297
- key="voltage_phase_two",
298
- translation_key="voltage_phase_two",
299
  device_class=SensorDeviceClass.VOLTAGE,
300
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
301
  state_class=SensorStateClass.MEASUREMENT,
302
  entity_registry_enabled_default=False,
303
  ),
304
  PlugwiseSensorEntityDescription(
305
- key="voltage_phase_three",
306
- translation_key="voltage_phase_three",
307
  device_class=SensorDeviceClass.VOLTAGE,
308
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
309
  state_class=SensorStateClass.MEASUREMENT,
310
  entity_registry_enabled_default=False,
311
  ),
312
  PlugwiseSensorEntityDescription(
313
- key="gas_consumed_interval",
314
- translation_key="gas_consumed_interval",
315
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
316
  state_class=SensorStateClass.MEASUREMENT,
317
  ),
318
  PlugwiseSensorEntityDescription(
319
- key="gas_consumed_cumulative",
320
- translation_key="gas_consumed_cumulative",
321
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
322
  device_class=SensorDeviceClass.GAS,
323
  state_class=SensorStateClass.TOTAL,
324
  ),
325
  PlugwiseSensorEntityDescription(
326
- key="net_electricity_point",
327
- translation_key="net_electricity_point",
328
  native_unit_of_measurement=UnitOfPower.WATT,
329
  device_class=SensorDeviceClass.POWER,
330
  state_class=SensorStateClass.MEASUREMENT,
331
  ),
332
  PlugwiseSensorEntityDescription(
333
- key="net_electricity_cumulative",
334
- translation_key="net_electricity_cumulative",
335
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
336
  device_class=SensorDeviceClass.ENERGY,
337
  state_class=SensorStateClass.TOTAL,
@@ -351,22 +407,22 @@
351
  state_class=SensorStateClass.MEASUREMENT,
352
  ),
353
  PlugwiseSensorEntityDescription(
354
- key="modulation_level",
355
- translation_key="modulation_level",
356
  native_unit_of_measurement=PERCENTAGE,
357
  entity_category=EntityCategory.DIAGNOSTIC,
358
  state_class=SensorStateClass.MEASUREMENT,
359
  ),
360
  PlugwiseSensorEntityDescription(
361
- key="valve_position",
362
- translation_key="valve_position",
363
  native_unit_of_measurement=PERCENTAGE,
364
  entity_category=EntityCategory.DIAGNOSTIC,
365
  state_class=SensorStateClass.MEASUREMENT,
366
  ),
367
  PlugwiseSensorEntityDescription(
368
- key="water_pressure",
369
- translation_key="water_pressure",
370
  native_unit_of_measurement=UnitOfPressure.BAR,
371
  device_class=SensorDeviceClass.PRESSURE,
372
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -379,16 +435,16 @@
379
  state_class=SensorStateClass.MEASUREMENT,
380
  ),
381
  PlugwiseSensorEntityDescription(
382
- key="dhw_temperature",
383
- translation_key="dhw_temperature",
384
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
385
  device_class=SensorDeviceClass.TEMPERATURE,
386
  entity_category=EntityCategory.DIAGNOSTIC,
387
  state_class=SensorStateClass.MEASUREMENT,
388
  ),
389
  PlugwiseSensorEntityDescription(
390
- key="domestic_hot_water_setpoint",
391
- translation_key="domestic_hot_water_setpoint",
392
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
393
  device_class=SensorDeviceClass.TEMPERATURE,
394
  entity_category=EntityCategory.DIAGNOSTIC,
@@ -402,7 +458,8 @@
402
  entry: PlugwiseConfigEntry,
403
  async_add_entities: AddConfigEntryEntitiesCallback,
404
  ) -> None:
405
- """Set up the Smile sensors from a config entry."""
 
406
  coordinator = entry.runtime_data
407
 
408
  @callback
@@ -411,13 +468,29 @@
411
  if not coordinator.new_devices:
412
  return
413
 
414
- async_add_entities(
415
- PlugwiseSensorEntity(coordinator, device_id, description)
416
- for device_id in coordinator.new_devices
417
- if (sensors := coordinator.data[device_id].get("sensors"))
418
- for description in SENSORS
419
- if description.key in sensors
420
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
  _add_entities()
423
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -436,10 +509,10 @@
436
  ) -> None:
437
  """Initialise the sensor."""
438
  super().__init__(coordinator, device_id)
439
- self._attr_unique_id = f"{device_id}-{description.key}"
440
  self.entity_description = description
 
441
 
442
  @property
443
- def native_value(self) -> int | float:
444
  """Return the value reported by the sensor."""
445
- return self.device["sensors"][self.entity_description.key]
 
1
  """Plugwise Sensor component for Home Assistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from dataclasses import dataclass
6
 
7
  from plugwise.constants import SensorType
 
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
  ),
105
  PlugwiseSensorEntityDescription(
106
+ key=TARGET_TEMP_HIGH,
107
  translation_key="cooling_setpoint",
108
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
109
  device_class=SensorDeviceClass.TEMPERATURE,
110
  state_class=SensorStateClass.MEASUREMENT,
111
  ),
112
  PlugwiseSensorEntityDescription(
113
+ key=TARGET_TEMP_LOW,
114
  translation_key="heating_setpoint",
115
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
116
  device_class=SensorDeviceClass.TEMPERATURE,
117
  state_class=SensorStateClass.MEASUREMENT,
118
  ),
119
  PlugwiseSensorEntityDescription(
120
+ key=ATTR_TEMPERATURE,
121
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
122
  device_class=SensorDeviceClass.TEMPERATURE,
123
  state_class=SensorStateClass.MEASUREMENT,
124
  ),
125
  PlugwiseSensorEntityDescription(
126
+ key=INTENDED_BOILER_TEMP,
127
+ translation_key=INTENDED_BOILER_TEMP,
128
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
129
  device_class=SensorDeviceClass.TEMPERATURE,
130
  entity_category=EntityCategory.DIAGNOSTIC,
131
  state_class=SensorStateClass.MEASUREMENT,
132
  ),
133
  PlugwiseSensorEntityDescription(
134
+ key=TEMP_DIFF,
135
+ translation_key=TEMP_DIFF,
136
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
137
  device_class=SensorDeviceClass.TEMPERATURE,
138
  entity_category=EntityCategory.DIAGNOSTIC,
139
  state_class=SensorStateClass.MEASUREMENT,
140
  ),
141
  PlugwiseSensorEntityDescription(
142
+ key=OUTDOOR_TEMP,
143
+ translation_key=OUTDOOR_TEMP,
144
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
145
  device_class=SensorDeviceClass.TEMPERATURE,
146
  entity_category=EntityCategory.DIAGNOSTIC,
147
  state_class=SensorStateClass.MEASUREMENT,
148
+ suggested_display_precision=1,
149
  ),
150
  PlugwiseSensorEntityDescription(
151
+ key=OUTDOOR_AIR_TEMP,
152
+ translation_key=OUTDOOR_AIR_TEMP,
153
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
154
  device_class=SensorDeviceClass.TEMPERATURE,
155
  entity_category=EntityCategory.DIAGNOSTIC,
156
  state_class=SensorStateClass.MEASUREMENT,
157
  ),
158
  PlugwiseSensorEntityDescription(
159
+ key=WATER_TEMP,
160
+ translation_key=WATER_TEMP,
161
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
162
  device_class=SensorDeviceClass.TEMPERATURE,
163
  entity_category=EntityCategory.DIAGNOSTIC,
164
  state_class=SensorStateClass.MEASUREMENT,
165
  ),
166
  PlugwiseSensorEntityDescription(
167
+ key=RETURN_TEMP,
168
+ translation_key=RETURN_TEMP,
169
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
170
  device_class=SensorDeviceClass.TEMPERATURE,
171
  entity_category=EntityCategory.DIAGNOSTIC,
172
  state_class=SensorStateClass.MEASUREMENT,
173
  ),
174
  PlugwiseSensorEntityDescription(
175
+ key=EL_CONSUMED,
176
+ translation_key=EL_CONSUMED,
177
  native_unit_of_measurement=UnitOfPower.WATT,
178
  device_class=SensorDeviceClass.POWER,
179
  state_class=SensorStateClass.MEASUREMENT,
180
  ),
181
  PlugwiseSensorEntityDescription(
182
+ key=EL_PRODUCED,
183
+ translation_key=EL_PRODUCED,
184
  native_unit_of_measurement=UnitOfPower.WATT,
185
  device_class=SensorDeviceClass.POWER,
186
  state_class=SensorStateClass.MEASUREMENT,
187
  entity_registry_enabled_default=False,
188
  ),
189
  PlugwiseSensorEntityDescription(
190
+ key=EL_CONS_INTERVAL,
191
+ translation_key=EL_CONS_INTERVAL,
192
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
193
  device_class=SensorDeviceClass.ENERGY,
194
  state_class=SensorStateClass.TOTAL,
195
  ),
196
  PlugwiseSensorEntityDescription(
197
+ key=EL_CONS_P_INTERVAL,
198
+ translation_key=EL_CONS_P_INTERVAL,
199
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
200
  device_class=SensorDeviceClass.ENERGY,
201
  state_class=SensorStateClass.TOTAL,
202
  ),
203
  PlugwiseSensorEntityDescription(
204
+ key=EL_CONS_OP_INTERVAL,
205
+ translation_key=EL_CONS_OP_INTERVAL,
206
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
207
  device_class=SensorDeviceClass.ENERGY,
208
  state_class=SensorStateClass.TOTAL,
209
  ),
210
  PlugwiseSensorEntityDescription(
211
+ key=EL_PROD_INTERVAL,
212
+ translation_key=EL_PROD_INTERVAL,
213
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
214
  device_class=SensorDeviceClass.ENERGY,
215
  state_class=SensorStateClass.TOTAL,
216
  entity_registry_enabled_default=False,
217
  ),
218
  PlugwiseSensorEntityDescription(
219
+ key=EL_PROD_P_INTERVAL,
220
+ translation_key=EL_PROD_P_INTERVAL,
221
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
222
  device_class=SensorDeviceClass.ENERGY,
223
  state_class=SensorStateClass.TOTAL,
224
  ),
225
  PlugwiseSensorEntityDescription(
226
+ key=EL_PROD_OP_INTERVAL,
227
+ translation_key=EL_PROD_OP_INTERVAL,
228
  native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
229
  device_class=SensorDeviceClass.ENERGY,
230
  state_class=SensorStateClass.TOTAL,
231
  ),
232
  PlugwiseSensorEntityDescription(
233
+ key=EL_CONS_POINT,
234
+ translation_key=EL_CONS_POINT,
235
  device_class=SensorDeviceClass.POWER,
236
  native_unit_of_measurement=UnitOfPower.WATT,
237
  state_class=SensorStateClass.MEASUREMENT,
238
  ),
239
  PlugwiseSensorEntityDescription(
240
+ key=EL_CONS_OP_POINT,
241
+ translation_key=EL_CONS_OP_POINT,
242
  native_unit_of_measurement=UnitOfPower.WATT,
243
  device_class=SensorDeviceClass.POWER,
244
  state_class=SensorStateClass.MEASUREMENT,
245
  ),
246
  PlugwiseSensorEntityDescription(
247
+ key=EL_CONS_P_POINT,
248
+ translation_key=EL_CONS_P_POINT,
249
  native_unit_of_measurement=UnitOfPower.WATT,
250
  device_class=SensorDeviceClass.POWER,
251
  state_class=SensorStateClass.MEASUREMENT,
252
  ),
253
  PlugwiseSensorEntityDescription(
254
+ key=EL_CONS_OP_CUMULATIVE,
255
+ translation_key=EL_CONS_OP_CUMULATIVE,
256
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
257
  device_class=SensorDeviceClass.ENERGY,
258
  state_class=SensorStateClass.TOTAL_INCREASING,
259
  ),
260
  PlugwiseSensorEntityDescription(
261
+ key=EL_CONS_P_CUMULATIVE,
262
+ translation_key=EL_CONS_P_CUMULATIVE,
263
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
264
  device_class=SensorDeviceClass.ENERGY,
265
  state_class=SensorStateClass.TOTAL_INCREASING,
266
  ),
267
  PlugwiseSensorEntityDescription(
268
+ key=EL_PROD_POINT,
269
+ translation_key=EL_PROD_POINT,
270
  device_class=SensorDeviceClass.POWER,
271
  native_unit_of_measurement=UnitOfPower.WATT,
272
  state_class=SensorStateClass.MEASUREMENT,
273
  ),
274
  PlugwiseSensorEntityDescription(
275
+ key=EL_PROD_OP_POINT,
276
+ translation_key=EL_PROD_OP_POINT,
277
  native_unit_of_measurement=UnitOfPower.WATT,
278
  device_class=SensorDeviceClass.POWER,
279
  state_class=SensorStateClass.MEASUREMENT,
280
  ),
281
  PlugwiseSensorEntityDescription(
282
+ key=EL_PROD_P_POINT,
283
+ translation_key=EL_PROD_P_POINT,
284
  native_unit_of_measurement=UnitOfPower.WATT,
285
  device_class=SensorDeviceClass.POWER,
286
  state_class=SensorStateClass.MEASUREMENT,
287
  ),
288
  PlugwiseSensorEntityDescription(
289
+ key=EL_PROD_OP_CUMULATIVE,
290
+ translation_key=EL_PROD_OP_CUMULATIVE,
291
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
292
  device_class=SensorDeviceClass.ENERGY,
293
  state_class=SensorStateClass.TOTAL_INCREASING,
294
  ),
295
  PlugwiseSensorEntityDescription(
296
+ key=EL_PROD_P_CUMULATIVE,
297
+ translation_key=EL_PROD_P_CUMULATIVE,
298
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
299
  device_class=SensorDeviceClass.ENERGY,
300
  state_class=SensorStateClass.TOTAL_INCREASING,
301
  ),
302
  PlugwiseSensorEntityDescription(
303
+ key=EL_PH1_CONSUMED,
304
+ translation_key=EL_PH1_CONSUMED,
305
  device_class=SensorDeviceClass.POWER,
306
  native_unit_of_measurement=UnitOfPower.WATT,
307
  state_class=SensorStateClass.MEASUREMENT,
308
  ),
309
  PlugwiseSensorEntityDescription(
310
+ key=EL_PH2_CONSUMED,
311
+ translation_key=EL_PH2_CONSUMED,
312
  device_class=SensorDeviceClass.POWER,
313
  native_unit_of_measurement=UnitOfPower.WATT,
314
  state_class=SensorStateClass.MEASUREMENT,
315
  ),
316
  PlugwiseSensorEntityDescription(
317
+ key=EL_PH3_CONSUMED,
318
+ translation_key=EL_PH3_CONSUMED,
319
  device_class=SensorDeviceClass.POWER,
320
  native_unit_of_measurement=UnitOfPower.WATT,
321
  state_class=SensorStateClass.MEASUREMENT,
322
  ),
323
  PlugwiseSensorEntityDescription(
324
+ key=EL_PH1_PRODUCED,
325
+ translation_key=EL_PH1_PRODUCED,
326
  device_class=SensorDeviceClass.POWER,
327
  native_unit_of_measurement=UnitOfPower.WATT,
328
  state_class=SensorStateClass.MEASUREMENT,
329
  ),
330
  PlugwiseSensorEntityDescription(
331
+ key=EL_PH2_PRODUCED,
332
+ translation_key=EL_PH2_PRODUCED,
333
  device_class=SensorDeviceClass.POWER,
334
  native_unit_of_measurement=UnitOfPower.WATT,
335
  state_class=SensorStateClass.MEASUREMENT,
336
  ),
337
  PlugwiseSensorEntityDescription(
338
+ key=EL_PH3_PRODUCED,
339
+ translation_key=EL_PH3_PRODUCED,
340
  device_class=SensorDeviceClass.POWER,
341
  native_unit_of_measurement=UnitOfPower.WATT,
342
  state_class=SensorStateClass.MEASUREMENT,
343
  ),
344
  PlugwiseSensorEntityDescription(
345
+ key=VOLTAGE_PH1,
346
+ translation_key=VOLTAGE_PH1,
347
  device_class=SensorDeviceClass.VOLTAGE,
348
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
349
  state_class=SensorStateClass.MEASUREMENT,
350
  entity_registry_enabled_default=False,
351
  ),
352
  PlugwiseSensorEntityDescription(
353
+ key=VOLTAGE_PH2,
354
+ translation_key=VOLTAGE_PH2,
355
  device_class=SensorDeviceClass.VOLTAGE,
356
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
357
  state_class=SensorStateClass.MEASUREMENT,
358
  entity_registry_enabled_default=False,
359
  ),
360
  PlugwiseSensorEntityDescription(
361
+ key=VOLTAGE_PH3,
362
+ translation_key=VOLTAGE_PH3,
363
  device_class=SensorDeviceClass.VOLTAGE,
364
  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
365
  state_class=SensorStateClass.MEASUREMENT,
366
  entity_registry_enabled_default=False,
367
  ),
368
  PlugwiseSensorEntityDescription(
369
+ key=GAS_CONS_INTERVAL,
370
+ translation_key=GAS_CONS_INTERVAL,
371
  native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
372
  state_class=SensorStateClass.MEASUREMENT,
373
  ),
374
  PlugwiseSensorEntityDescription(
375
+ key=GAS_CONS_CUMULATIVE,
376
+ translation_key=GAS_CONS_CUMULATIVE,
377
  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
378
  device_class=SensorDeviceClass.GAS,
379
  state_class=SensorStateClass.TOTAL,
380
  ),
381
  PlugwiseSensorEntityDescription(
382
+ key=NET_EL_POINT,
383
+ translation_key=NET_EL_POINT,
384
  native_unit_of_measurement=UnitOfPower.WATT,
385
  device_class=SensorDeviceClass.POWER,
386
  state_class=SensorStateClass.MEASUREMENT,
387
  ),
388
  PlugwiseSensorEntityDescription(
389
+ key=NET_EL_CUMULATIVE,
390
+ translation_key=NET_EL_CUMULATIVE,
391
  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
392
  device_class=SensorDeviceClass.ENERGY,
393
  state_class=SensorStateClass.TOTAL,
 
407
  state_class=SensorStateClass.MEASUREMENT,
408
  ),
409
  PlugwiseSensorEntityDescription(
410
+ key=MOD_LEVEL,
411
+ translation_key=MOD_LEVEL,
412
  native_unit_of_measurement=PERCENTAGE,
413
  entity_category=EntityCategory.DIAGNOSTIC,
414
  state_class=SensorStateClass.MEASUREMENT,
415
  ),
416
  PlugwiseSensorEntityDescription(
417
+ key=VALVE_POS,
418
+ translation_key=VALVE_POS,
419
  native_unit_of_measurement=PERCENTAGE,
420
  entity_category=EntityCategory.DIAGNOSTIC,
421
  state_class=SensorStateClass.MEASUREMENT,
422
  ),
423
  PlugwiseSensorEntityDescription(
424
+ key=WATER_PRESSURE,
425
+ translation_key=WATER_PRESSURE,
426
  native_unit_of_measurement=UnitOfPressure.BAR,
427
  device_class=SensorDeviceClass.PRESSURE,
428
  entity_category=EntityCategory.DIAGNOSTIC,
 
435
  state_class=SensorStateClass.MEASUREMENT,
436
  ),
437
  PlugwiseSensorEntityDescription(
438
+ key=DHW_TEMP,
439
+ translation_key=DHW_TEMP,
440
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
441
  device_class=SensorDeviceClass.TEMPERATURE,
442
  entity_category=EntityCategory.DIAGNOSTIC,
443
  state_class=SensorStateClass.MEASUREMENT,
444
  ),
445
  PlugwiseSensorEntityDescription(
446
+ key=DHW_SETPOINT,
447
+ translation_key=DHW_SETPOINT,
448
  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
449
  device_class=SensorDeviceClass.TEMPERATURE,
450
  entity_category=EntityCategory.DIAGNOSTIC,
 
458
  entry: PlugwiseConfigEntry,
459
  async_add_entities: AddConfigEntryEntitiesCallback,
460
  ) -> None:
461
+ """Set up Plugwise sensors from a config entry."""
462
+ # Upstream as Plugwise not Smile
463
  coordinator = entry.runtime_data
464
 
465
  @callback
 
468
  if not coordinator.new_devices:
469
  return
470
 
471
+ # Upstream consts
472
+ # async_add_entities(
473
+ # PlugwiseSensorEntity(coordinator, device_id, description)
474
+ # for device_id in coordinator.new_devices
475
+ # if (sensors := coordinator.data.devices[device_id].get(SENSORS))
476
+ # for description in PLUGWISE_SENSORS
477
+ # if description.key in sensors
478
+ # )
479
+ # pw-beta alternative for debugging
480
+ entities: list[PlugwiseSensorEntity] = []
481
+ for device_id in coordinator.new_devices:
482
+ device = coordinator.data[device_id]
483
+ if not (sensors := device.get(SENSORS)):
484
+ continue
485
+ for description in PLUGWISE_SENSORS:
486
+ if description.key not in sensors:
487
+ continue
488
+ entities.append(PlugwiseSensorEntity(coordinator, device_id, description))
489
+ LOGGER.debug(
490
+ "Add %s %s sensor", device["name"], description.translation_key or description.key
491
+ )
492
+
493
+ async_add_entities(entities)
494
 
495
  _add_entities()
496
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
 
509
  ) -> None:
510
  """Initialise the sensor."""
511
  super().__init__(coordinator, device_id)
 
512
  self.entity_description = description
513
+ self._attr_unique_id = f"{device_id}-{description.key}"
514
 
515
  @property
516
+ def native_value(self) -> int | float | None:
517
  """Return the value reported by the sensor."""
518
+ return self.device.get(SENSORS, {}).get(self.entity_description.key) # Upstream consts
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/strings.json RENAMED
@@ -1,46 +1,44 @@
1
  {
2
  "config": {
3
  "abort": {
4
- "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
5
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
6
- "not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address.",
7
- "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
8
  },
9
  "error": {
10
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
11
- "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
12
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
13
- "response_error": "Invalid XML data, or error indication received",
14
- "unknown": "[%key:common::config_flow::error::unknown%]",
15
  "unsupported": "Device with unsupported firmware"
16
  },
17
  "step": {
18
  "reconfigure": {
19
  "data": {
20
- "host": "[%key:common::config_flow::data::ip%]",
21
- "port": "[%key:common::config_flow::data::port%]"
22
  },
23
  "data_description": {
24
- "host": "[%key:component::plugwise::config::step::user::data_description::host%]",
25
- "port": "[%key:component::plugwise::config::step::user::data_description::port%]"
26
  },
27
  "description": "Update configuration for {title}."
28
  },
29
  "user": {
30
  "data": {
31
- "host": "[%key:common::config_flow::data::ip%]",
32
- "password": "Smile ID",
33
- "port": "[%key:common::config_flow::data::port%]",
34
  "username": "Smile username"
35
  },
36
  "data_description": {
37
  "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.",
38
- "password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
39
  "port": "By default your Smile uses port 80, normally you should not have to change this.",
40
  "username": "Default is `smile`, or `stretch` for the legacy Stretch."
41
  },
42
- "description": "Please enter",
43
- "title": "Connect to the Smile"
44
  }
45
  }
46
  },
@@ -53,7 +51,7 @@
53
  "name": "Cooling enabled"
54
  },
55
  "cooling_state": {
56
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
57
  },
58
  "dhw_state": {
59
  "name": "DHW state"
@@ -62,7 +60,10 @@
62
  "name": "Flame state"
63
  },
64
  "heating_state": {
65
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]"
 
 
 
66
  },
67
  "plugwise_notification": {
68
  "name": "Plugwise notification"
@@ -79,20 +80,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,17 +107,17 @@
112
  "select_dhw_mode": {
113
  "name": "DHW mode",
114
  "state": {
115
- "auto": "[%key:common::state::auto%]",
116
- "boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
117
- "comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
118
- "off": "[%key:common::state::off%]"
119
  }
120
  },
121
  "select_gateway_mode": {
122
  "name": "Gateway mode",
123
  "state": {
124
  "away": "Pause",
125
- "full": "[%key:common::state::normal%]",
126
  "vacation": "Vacation"
127
  }
128
  },
@@ -131,22 +126,22 @@
131
  "state": {
132
  "bleeding_cold": "Bleeding cold",
133
  "bleeding_hot": "Bleeding hot",
134
- "cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
135
- "heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
136
- "off": "[%key:common::state::off%]"
137
  }
138
  },
139
  "select_schedule": {
140
  "name": "Thermostat schedule",
141
  "state": {
142
- "off": "[%key:common::state::off%]"
143
  }
144
  },
145
  "select_zone_profile": {
146
  "name": "Zone profile",
147
  "state": {
148
- "active": "[%key:common::state::active%]",
149
- "off": "[%key:common::state::off%]",
150
  "passive": "Passive"
151
  }
152
  }
@@ -293,13 +288,13 @@
293
  },
294
  "switch": {
295
  "cooling_ena_switch": {
296
- "name": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]"
297
  },
298
  "dhw_cm_switch": {
299
  "name": "DHW comfort mode"
300
  },
301
  "lock": {
302
- "name": "[%key:component::lock::title%]"
303
  },
304
  "relay": {
305
  "name": "Relay"
@@ -308,28 +303,50 @@
308
  },
309
  "exceptions": {
310
  "authentication_failed": {
311
- "message": "[%key:common::config_flow::error::invalid_auth%]"
312
  },
313
  "data_incomplete_or_missing": {
314
- "message": "Data incomplete or missing."
315
  },
316
  "error_communicating_with_api": {
317
- "message": "Error communicating with API: {error}."
318
  },
319
  "failed_to_connect": {
320
- "message": "[%key:common::config_flow::error::cannot_connect%]"
321
  },
322
  "invalid_setup": {
323
  "message": "Add your Adam instead of your Anna, see the documentation"
324
  },
325
  "response_error": {
326
- "message": "[%key:component::plugwise::config::error::response_error%]"
327
  },
328
  "set_schedule_first": {
329
- "message": "Failed setting HVACMode, set a schedule first."
330
  },
331
  "unsupported_firmware": {
332
- "message": "[%key:component::plugwise::config::error::unsupported%]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
  }
335
  }
 
1
  {
2
  "config": {
3
  "abort": {
4
+ "already_configured": "Device is already configured",
5
  "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
6
+ "not_the_same_smile": "The provided Smile-ID does not match your device",
7
+ "reconfigure_successful": "Reconfiguration was successful"
8
  },
9
  "error": {
10
+ "cannot_connect": "Failed to connect",
11
+ "invalid_auth": "Invalid authentication",
12
  "invalid_setup": "Add your Adam instead of your Anna, see the documentation",
13
+ "response_error": "Invalid XML-data or error indication received",
14
+ "unknown": "Unexpected error",
15
  "unsupported": "Device with unsupported firmware"
16
  },
17
  "step": {
18
  "reconfigure": {
19
  "data": {
20
+ "host": "IP address"
 
21
  },
22
  "data_description": {
23
+ "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app."
 
24
  },
25
  "description": "Update configuration for {title}."
26
  },
27
  "user": {
28
  "data": {
29
+ "host": "IP address",
30
+ "password": "Smile-ID",
31
+ "port": "Port",
32
  "username": "Smile username"
33
  },
34
  "data_description": {
35
  "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.",
36
+ "password": "The Smile-ID printed on the label on the back of your Adam, P1, Smile-T, or Stretch.",
37
  "port": "By default your Smile uses port 80, normally you should not have to change this.",
38
  "username": "Default is `smile`, or `stretch` for the legacy Stretch."
39
  },
40
+ "description": "Enter your Plugwise device data: (setup can take up to 90s)",
41
+ "title": "Set up Plugwise Adam/Smile/Stretch"
42
  }
43
  }
44
  },
 
51
  "name": "Cooling enabled"
52
  },
53
  "cooling_state": {
54
+ "name": "Cooling"
55
  },
56
  "dhw_state": {
57
  "name": "DHW state"
 
60
  "name": "Flame state"
61
  },
62
  "heating_state": {
63
+ "name": "Heating"
64
+ },
65
+ "low_battery": {
66
+ "name": "Battery status"
67
  },
68
  "plugwise_notification": {
69
  "name": "Plugwise notification"
 
80
  "climate": {
81
  "plugwise": {
82
  "state_attributes": {
 
 
 
83
  "preset_mode": {
84
  "state": {
85
  "asleep": "Night",
86
+ "away": "Away",
87
+ "home": "Home",
88
  "no_frost": "Anti-frost",
89
  "vacation": "Vacation"
90
  }
 
 
 
91
  }
92
  }
93
  }
 
107
  "select_dhw_mode": {
108
  "name": "DHW mode",
109
  "state": {
110
+ "auto": "Auto",
111
+ "boost": "Boost",
112
+ "comfort": "Comfort",
113
+ "off": "Off"
114
  }
115
  },
116
  "select_gateway_mode": {
117
  "name": "Gateway mode",
118
  "state": {
119
  "away": "Pause",
120
+ "full": "Normal",
121
  "vacation": "Vacation"
122
  }
123
  },
 
126
  "state": {
127
  "bleeding_cold": "Bleeding cold",
128
  "bleeding_hot": "Bleeding hot",
129
+ "cooling": "Cooling",
130
+ "heating": "Heating",
131
+ "off": "Off"
132
  }
133
  },
134
  "select_schedule": {
135
  "name": "Thermostat schedule",
136
  "state": {
137
+ "off": "Off"
138
  }
139
  },
140
  "select_zone_profile": {
141
  "name": "Zone profile",
142
  "state": {
143
+ "active": "Active",
144
+ "off": "Off",
145
  "passive": "Passive"
146
  }
147
  }
 
288
  },
289
  "switch": {
290
  "cooling_ena_switch": {
291
+ "name": "Cooling"
292
  },
293
  "dhw_cm_switch": {
294
  "name": "DHW comfort mode"
295
  },
296
  "lock": {
297
+ "name": "Lock"
298
  },
299
  "relay": {
300
  "name": "Relay"
 
303
  },
304
  "exceptions": {
305
  "authentication_failed": {
306
+ "message": "Invalid authentication"
307
  },
308
  "data_incomplete_or_missing": {
309
+ "message": "Data incomplete or missing"
310
  },
311
  "error_communicating_with_api": {
312
+ "message": "Error communicating with API: {error}"
313
  },
314
  "failed_to_connect": {
315
+ "message": "Failed to connect"
316
  },
317
  "invalid_setup": {
318
  "message": "Add your Adam instead of your Anna, see the documentation"
319
  },
320
  "response_error": {
321
+ "message": "Invalid XML-data or error indication received"
322
  },
323
  "set_schedule_first": {
324
+ "message": "Failed setting HVACMode, set a schedule first"
325
  },
326
  "unsupported_firmware": {
327
+ "message": "Device with unsupported firmware"
328
+ }
329
+ },
330
+ "options": {
331
+ "step": {
332
+ "init": {
333
+ "data": {
334
+ "cooling_on": "Anna: cooling-mode is on",
335
+ "refresh_interval": "Frontend refresh-time (1.5 - 5 seconds) *) beta-only option",
336
+ "scan_interval": "Scan interval (seconds) *) beta-only option"
337
+ },
338
+ "description": "Adjust Adam/Smile/Stretch options"
339
+ },
340
+ "none": {
341
+ "description": "This integration does not provide any options",
342
+ "title": "No options available"
343
+ }
344
+ }
345
+ },
346
+ "services": {
347
+ "delete_notification": {
348
+ "description": "Deletes a Plugwise notification",
349
+ "name": "Delete Plugwise notification"
350
  }
351
  }
352
  }
/home/runner/work/progress/progress/clones/beta/{ha-core/homeassistant/components → beta/custom_components}/plugwise/switch.py RENAMED
@@ -1,5 +1,7 @@
1
  """Plugwise Switch component for HomeAssistant."""
2
 
 
 
3
  from dataclasses import dataclass
4
  from typing import Any
5
 
@@ -14,6 +16,17 @@
14
  from homeassistant.core import HomeAssistant, callback
15
  from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
16
 
 
 
 
 
 
 
 
 
 
 
 
17
  from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
18
  from .entity import PlugwiseEntity
19
  from .util import plugwise_command
@@ -28,25 +41,29 @@
28
  key: SwitchType
29
 
30
 
31
- SWITCHES: tuple[PlugwiseSwitchEntityDescription, ...] = (
 
32
  PlugwiseSwitchEntityDescription(
33
- key="dhw_cm_switch",
34
- translation_key="dhw_cm_switch",
 
35
  entity_category=EntityCategory.CONFIG,
36
  ),
37
  PlugwiseSwitchEntityDescription(
38
- key="lock",
39
- translation_key="lock",
 
40
  entity_category=EntityCategory.CONFIG,
41
  ),
42
  PlugwiseSwitchEntityDescription(
43
- key="relay",
44
- translation_key="relay",
45
  device_class=SwitchDeviceClass.SWITCH,
46
  ),
47
  PlugwiseSwitchEntityDescription(
48
- key="cooling_ena_switch",
49
- translation_key="cooling_ena_switch",
 
50
  entity_category=EntityCategory.CONFIG,
51
  ),
52
  )
@@ -57,7 +74,7 @@
57
  entry: PlugwiseConfigEntry,
58
  async_add_entities: AddConfigEntryEntitiesCallback,
59
  ) -> None:
60
- """Set up the Smile switches from a config entry."""
61
  coordinator = entry.runtime_data
62
 
63
  @callback
@@ -66,13 +83,28 @@
66
  if not coordinator.new_devices:
67
  return
68
 
69
- async_add_entities(
70
- PlugwiseSwitchEntity(coordinator, device_id, description)
71
- for device_id in coordinator.new_devices
72
- if (switches := coordinator.data[device_id].get("switches"))
73
- for description in SWITCHES
74
- if description.key in switches
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  _add_entities()
78
  entry.async_on_unload(coordinator.async_add_listener(_add_entities))
@@ -91,30 +123,30 @@
91
  ) -> None:
92
  """Set up the Plugwise API."""
93
  super().__init__(coordinator, device_id)
94
- self._attr_unique_id = f"{device_id}-{description.key}"
95
  self.entity_description = description
 
96
 
97
  @property
98
- def is_on(self) -> bool:
99
  """Return True if entity is on."""
100
- return self.device["switches"][self.entity_description.key]
101
 
102
  @plugwise_command
103
  async def async_turn_on(self, **kwargs: Any) -> None:
104
  """Turn the device on."""
105
  await self.coordinator.api.set_switch_state(
106
  self._dev_id,
107
- self.device.get("members"),
108
  self.entity_description.key,
109
  "on",
110
- )
111
 
112
  @plugwise_command
113
  async def async_turn_off(self, **kwargs: Any) -> None:
114
  """Turn the device off."""
115
  await self.coordinator.api.set_switch_state(
116
  self._dev_id,
117
- self.device.get("members"),
118
  self.entity_description.key,
119
  "off",
120
- )
 
1
  """Plugwise Switch component for HomeAssistant."""
2
 
3
+ from __future__ import annotations
4
+
5
  from dataclasses import dataclass
6
  from typing import Any
7
 
 
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 | None:
131
  """Return True if entity is on."""
132
+ return self.device.get(SWITCHES, {}).get(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,10 +12,15 @@
10
  from .const import DOMAIN
11
  from .entity import PlugwiseEntity
12
 
 
 
 
 
 
13
 
14
- def plugwise_command[_PlugwiseEntityT: PlugwiseEntity, **_P, _R](
15
- func: Callable[Concatenate[_PlugwiseEntityT, _P], Awaitable[_R]],
16
- ) -> Callable[Concatenate[_PlugwiseEntityT, _P], Coroutine[Any, Any, _R]]:
17
  """Decorate Plugwise calls that send commands/make changes to the device.
18
 
19
  A decorator that wraps the passed in function, catches Plugwise errors,
@@ -21,8 +28,8 @@
21
  """
22
 
23
  async def handler(
24
- self: _PlugwiseEntityT, *args: _P.args, **kwargs: _P.kwargs
25
- ) -> _R:
26
  try:
27
  return await func(self, *args, **kwargs)
28
  except PlugwiseException as err:
 
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]],
23
+ ) -> Callable[Concatenate[PlugwiseEntityT, P], Coroutine[Any, Any, R]]:
24
  """Decorate Plugwise calls that send commands/make changes to the device.
25
 
26
  A decorator that wraps the passed in function, catches Plugwise errors,
 
28
  """
29
 
30
  async def handler(
31
+ self: PlugwiseEntityT, *args: P.args, **kwargs: P.kwargs
32
+ ) -> R:
33
  try:
34
  return await func(self, *args, **kwargs)
35
  except PlugwiseException as err: