Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.

Commit 4facc97

Browse files
shbatmclaude
andauthored
Modernize entity state via _attr_ pattern instead of @Property (#120)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: skynet <tim@bond.casa>
1 parent 6a72a87 commit 4facc97

6 files changed

Lines changed: 120 additions & 87 deletions

File tree

custom_components/isy994/climate.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,6 @@ def current_temperature(self) -> float | None:
160160
self._node.status, self._uom, self._node.precision, 1
161161
)
162162

163-
@property
164-
def target_temperature_step(self) -> float | None:
165-
"""Return the supported step of target temperature."""
166-
return 1.0
167-
168163
@property
169164
def target_temperature(self) -> float | None:
170165
"""Return the temperature we try to reach."""
@@ -190,11 +185,6 @@ def target_temperature_low(self) -> float | None:
190185
return None
191186
return convert_isy_value_to_hass(target.value, target.uom, target.precision, 1)
192187

193-
@property
194-
def fan_modes(self) -> list[str]:
195-
"""Return the list of available fan modes."""
196-
return [FAN_AUTO, FAN_ON]
197-
198188
@property
199189
def fan_mode(self) -> str:
200190
"""Return the current fan mode ie. auto, on."""

custom_components/isy994/cover.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
CoverEntityFeature,
1111
)
1212
from homeassistant.const import Platform
13-
from homeassistant.core import HomeAssistant
13+
from homeassistant.core import HomeAssistant, callback
1414
from homeassistant.exceptions import HomeAssistantError
1515
from homeassistant.helpers.entity import DeviceInfo
1616
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1717
from pyisyox.nodes import Node
1818
from pyisyox.programs import Program
1919

2020
from .const import UOM_8_BIT_RANGE
21-
from .entity import ISYNodeEntity, ISYProgramEntity
21+
from .entity import ISYNodeEntity, ISYProgramEntity, NodeEventType
2222
from .models import IsyConfigEntry
2323

2424

@@ -52,21 +52,31 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
5252
)
5353
_node: Node
5454

55-
@property
56-
def current_cover_position(self) -> int | None:
57-
"""Return the current cover position."""
55+
def _update_cover_attrs(self) -> None:
5856
if self._node.status is None:
59-
return None
57+
self._attr_current_cover_position = None
58+
self._attr_is_closed = None
59+
return
6060
if self._node.uom == UOM_8_BIT_RANGE:
61-
return round(cast(float, self._node.status) * 100.0 / 255.0)
62-
return int(sorted((0, self._node.status, 100))[1])
61+
self._attr_current_cover_position = round(
62+
cast(float, self._node.status) * 100.0 / 255.0
63+
)
64+
else:
65+
self._attr_current_cover_position = int(
66+
sorted((0, self._node.status, 100))[1]
67+
)
68+
self._attr_is_closed = bool(self._node.status == 0)
6369

64-
@property
65-
def is_closed(self) -> bool | None:
66-
"""Get whether the ISY cover device is closed."""
67-
if self._node.status is None:
68-
return None
69-
return bool(self._node.status == 0)
70+
async def async_added_to_hass(self) -> None:
71+
"""Subscribe to events and set initial state."""
72+
await super().async_added_to_hass()
73+
self._update_cover_attrs()
74+
75+
@callback
76+
def async_on_update(self, event: NodeEventType, key: str) -> None:
77+
"""Handle a control event from the ISY Node."""
78+
self._update_cover_attrs()
79+
super().async_on_update(event, key)
7080

7181
async def async_open_cover(self, **kwargs: Any) -> None:
7282
"""Send the open cover command to the ISY cover device."""
@@ -94,10 +104,16 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
94104

95105
_actions: Program
96106

97-
@property
98-
def is_closed(self) -> bool:
99-
"""Get whether the ISY cover program is closed."""
100-
return bool(self._node.status)
107+
async def async_added_to_hass(self) -> None:
108+
"""Subscribe to events and set initial state."""
109+
await super().async_added_to_hass()
110+
self._attr_is_closed = bool(self._node.status)
111+
112+
@callback
113+
def async_on_update(self, event: NodeEventType, key: str) -> None:
114+
"""Handle the update event from the ISY Node."""
115+
self._attr_is_closed = bool(self._node.status)
116+
super().async_on_update(event, key)
101117

102118
async def async_open_cover(self, **kwargs: Any) -> None:
103119
"""Send the open cover command to the ISY cover program."""

custom_components/isy994/entity.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def __init__(
121121
self._attr_has_entity_name = True
122122

123123
self._attr_name = name
124+
self._attr_available = node.enabled
124125

125126
async def async_added_to_hass(self) -> None:
126127
"""Subscribe to the node control change events."""
@@ -146,13 +147,9 @@ async def async_will_remove_from_hass(self) -> None:
146147
@callback
147148
def async_on_update(self, event: NodeEventType, key: str) -> None:
148149
"""Handle a control event from the ISY Node."""
150+
self._attr_available = self._node.enabled
149151
self.async_write_ha_state()
150152

151-
@property
152-
def available(self) -> bool:
153-
"""Return entity availability."""
154-
return self._node.enabled
155-
156153
async def async_send_node_command(self, command: str) -> None:
157154
"""Respond to an entity service command call."""
158155
if not hasattr(self._node, command):

custom_components/isy994/fan.py

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from homeassistant.components.fan import FanEntity, FanEntityFeature
99
from homeassistant.const import Platform
10-
from homeassistant.core import HomeAssistant
10+
from homeassistant.core import HomeAssistant, callback
1111
from homeassistant.helpers.entity import DeviceInfo
1212
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1313
from homeassistant.util.percentage import (
@@ -20,7 +20,7 @@
2020
from pyisyox.programs import Program
2121

2222
from .const import _LOGGER
23-
from .entity import ISYNodeEntity, ISYProgramEntity
23+
from .entity import ISYNodeEntity, ISYProgramEntity, NodeEventType
2424
from .models import IsyConfigEntry
2525

2626
SPEED_RANGE = (1, 255) # off is not included
@@ -57,26 +57,33 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
5757
)
5858
_node: Node
5959

60-
@property
61-
def percentage(self) -> int | None:
62-
"""Return the current speed percentage."""
63-
if self._node.status is None:
64-
return None
65-
return ranged_value_to_percentage(SPEED_RANGE, self._node.status)
66-
67-
@property
68-
def speed_count(self) -> int:
69-
"""Return the number of speeds the fan supports."""
70-
if self._node.protocol == Protocol.INSTEON:
71-
return 3
72-
return int_states_in_range(SPEED_RANGE)
73-
74-
@property
75-
def is_on(self) -> bool | None:
76-
"""Get if the fan is on."""
60+
def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None:
61+
"""Initialize the ISY fan entity."""
62+
super().__init__(node=node, device_info=device_info)
63+
self._attr_speed_count = (
64+
3 if node.protocol == Protocol.INSTEON else int_states_in_range(SPEED_RANGE)
65+
)
66+
67+
async def async_added_to_hass(self) -> None:
68+
"""Subscribe to events and set initial state."""
69+
await super().async_added_to_hass()
70+
self._update_fan_attrs()
71+
72+
def _update_fan_attrs(self) -> None:
7773
if self._node.status is None:
78-
return None
79-
return bool(self._node.status != 0)
74+
self._attr_is_on = None
75+
self._attr_percentage = None
76+
else:
77+
self._attr_is_on = bool(self._node.status != 0)
78+
self._attr_percentage = ranged_value_to_percentage(
79+
SPEED_RANGE, self._node.status
80+
)
81+
82+
@callback
83+
def async_on_update(self, event: NodeEventType, key: str) -> None:
84+
"""Handle a control event from the ISY Node."""
85+
self._update_fan_attrs()
86+
super().async_on_update(event, key)
8087

8188
async def async_set_percentage(self, percentage: int) -> None:
8289
"""Set node to speed percentage for the ISY fan device."""
@@ -106,24 +113,29 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
106113
"""Representation of an ISY fan program."""
107114

108115
_attr_supported_features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
116+
_attr_speed_count = int_states_in_range(SPEED_RANGE)
109117
_actions: Program
110118

111-
@property
112-
def percentage(self) -> int | None:
113-
"""Return the current speed percentage."""
119+
async def async_added_to_hass(self) -> None:
120+
"""Subscribe to events and set initial state."""
121+
await super().async_added_to_hass()
122+
self._update_fan_attrs()
123+
124+
def _update_fan_attrs(self) -> None:
114125
if self._node.status is None:
115-
return None
116-
return ranged_value_to_percentage(SPEED_RANGE, float(self._node.status))
117-
118-
@property
119-
def speed_count(self) -> int:
120-
"""Return the number of speeds the fan supports."""
121-
return int_states_in_range(SPEED_RANGE)
122-
123-
@property
124-
def is_on(self) -> bool:
125-
"""Get if the fan is on."""
126-
return bool(self._node.status != 0)
126+
self._attr_is_on = None
127+
self._attr_percentage = None
128+
else:
129+
self._attr_is_on = bool(self._node.status != 0)
130+
self._attr_percentage = ranged_value_to_percentage(
131+
SPEED_RANGE, float(self._node.status)
132+
)
133+
134+
@callback
135+
def async_on_update(self, event: NodeEventType, key: str) -> None:
136+
"""Handle the update event from the ISY Node."""
137+
self._update_fan_attrs()
138+
super().async_on_update(event, key)
127139

128140
async def async_turn_off(self, **kwargs: Any) -> None:
129141
"""Send the turn on command to ISY fan program."""

custom_components/isy994/lock.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66

77
from homeassistant.components.lock import LockEntity
88
from homeassistant.const import Platform
9-
from homeassistant.core import HomeAssistant
9+
from homeassistant.core import HomeAssistant, callback
1010
from homeassistant.exceptions import HomeAssistantError
1111
from homeassistant.helpers.entity import DeviceInfo
1212
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1313
from pyisyox.nodes import Node
1414
from pyisyox.programs import Program
1515

16-
from .entity import ISYNodeEntity, ISYProgramEntity
16+
from .entity import ISYNodeEntity, ISYProgramEntity, NodeEventType
1717
from .models import IsyConfigEntry
1818
from .services import async_setup_lock_services
1919

@@ -46,12 +46,22 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
4646

4747
_node: Node
4848

49-
@property
50-
def is_locked(self) -> bool | None:
51-
"""Get whether the lock is in locked state."""
52-
if self._node.status is None:
53-
return None
54-
return VALUE_TO_STATE.get(self._node.status)
49+
async def async_added_to_hass(self) -> None:
50+
"""Subscribe to events and set initial state."""
51+
await super().async_added_to_hass()
52+
status = self._node.status
53+
self._attr_is_locked = (
54+
VALUE_TO_STATE.get(status) if status is not None else None
55+
)
56+
57+
@callback
58+
def async_on_update(self, event: NodeEventType, key: str) -> None:
59+
"""Handle a control event from the ISY Node."""
60+
status = self._node.status
61+
self._attr_is_locked = (
62+
VALUE_TO_STATE.get(status) if status is not None else None
63+
)
64+
super().async_on_update(event, key)
5565

5666
async def async_lock(self, **kwargs: Any) -> None:
5767
"""Send the lock command to the ISY device."""
@@ -83,10 +93,16 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
8393

8494
_actions: Program
8595

86-
@property
87-
def is_locked(self) -> bool:
88-
"""Return true if the device is locked."""
89-
return bool(self._node.status)
96+
async def async_added_to_hass(self) -> None:
97+
"""Subscribe to events and set initial state."""
98+
await super().async_added_to_hass()
99+
self._attr_is_locked = bool(self._node.status)
100+
101+
@callback
102+
def async_on_update(self, event: NodeEventType, key: str) -> None:
103+
"""Handle the update event from the ISY Node."""
104+
self._attr_is_locked = bool(self._node.status)
105+
super().async_on_update(event, key)
90106

91107
async def async_lock(self, **kwargs: Any) -> None:
92108
"""Lock the device."""

custom_components/isy994/switch.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
SwitchEntityDescription,
1212
)
1313
from homeassistant.const import EntityCategory, Platform
14-
from homeassistant.core import HomeAssistant
14+
from homeassistant.core import HomeAssistant, callback
1515
from homeassistant.exceptions import HomeAssistantError
1616
from homeassistant.helpers.entity import DeviceInfo
1717
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1818
from pyisyox.nodes import Group, Node
1919
from pyisyox.nodes.nodebase import NodeBase
2020
from pyisyox.programs import Program
2121

22-
from .entity import ISYGroupEntity, ISYNodeEntity, ISYProgramEntity
22+
from .entity import ISYGroupEntity, ISYNodeEntity, ISYProgramEntity, NodeEventType
2323
from .models import IsyConfigEntry
2424

2525

@@ -165,11 +165,13 @@ def __init__(
165165
device_info=device_info,
166166
)
167167
self._attr_name = description.name # Override super
168+
# Always available; must follow super().__init__ which sets node.enabled
169+
self._attr_available = True
168170

169-
@property
170-
def available(self) -> bool:
171-
"""Return entity availability."""
172-
return True # Enable switch is always available
171+
@callback
172+
def async_on_update(self, event: NodeEventType, key: str) -> None:
173+
"""Handle a control event — availability is always True for enable switches."""
174+
self.async_write_ha_state()
173175

174176
@property
175177
def is_on(self) -> bool | None:

0 commit comments

Comments
 (0)