Diff to HTML by rtfpessoa

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