Skip to content

Commit 56c6f81

Browse files
authored
Merge pull request #131 from c-st/use-same-unique-id
Auto lights fixes, code restructuring
2 parents ed8f5fb + 6ab8478 commit 56c6f81

File tree

12 files changed

+478
-381
lines changed

12 files changed

+478
-381
lines changed

custom_components/auto_areas/auto_area.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
77
from homeassistant.helpers.issue_registry import async_create_issue, IssueSeverity
88
from homeassistant.config_entries import ConfigEntry
9-
9+
from homeassistant.util import slugify
1010
from homeassistant.helpers.area_registry import AreaEntry
1111
from homeassistant.helpers.entity_registry import RegistryEntry
1212

@@ -67,7 +67,6 @@ async def async_initialize(self):
6767
"%s: Initializing after HA start",
6868
self.area_name
6969
)
70-
7170
self.auto_lights = AutoLights(self)
7271
await self.auto_lights.initialize()
7372

@@ -98,3 +97,8 @@ def get_valid_entities(self) -> list[RegistryEntry]:
9897
def area_name(self) -> str:
9998
"""Return area name or fallback."""
10099
return self.area.name if self.area is not None else "unknown"
100+
101+
@property
102+
def slugified_area_name(self) -> str:
103+
"""Return slugified area name or fallback."""
104+
return slugify(self.area.name) if self.area is not None else "unknown"

custom_components/auto_areas/auto_entity.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,6 @@ async def _handle_state_change(self, event: Event[EventStateChangedData]):
136136

137137
self._aggregated_state = self._get_state()
138138

139-
LOGGER.debug(
140-
"%s: got state %s, %d entities",
141-
self.device_class,
142-
str(self.state),
143-
len(self.entity_states.values())
144-
)
145-
146139
self.async_schedule_update_ha_state()
147140

148141
async def async_will_remove_from_hass(self) -> None:

custom_components/auto_areas/auto_lights.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ def __init__(self, auto_area) -> None:
5151
or []
5252
)
5353

54+
# Entities
5455
self.sleep_mode_entity_id = (
5556
f"{SLEEP_MODE_SWITCH_ENTITY_PREFIX}{
56-
slugify(self.auto_area.area.name)}"
57+
slugify(self.auto_area.area_name)}"
5758
)
5859
self.presence_entity_id = (
5960
f"{PRESENCE_BINARY_SENSOR_ENTITY_PREFIX}{
60-
slugify(self.auto_area.area.name)}"
61+
slugify(self.auto_area.area_name)}"
6162
)
6263
self.illuminance_entity_id = (
6364
f"{ILLUMINANCE_SENSOR_ENTITY_PREFIX}{
64-
slugify(self.auto_area.area.name)}"
65+
slugify(self.auto_area.area_name)}"
6566
)
6667

6768
self.light_entity_ids = [
@@ -80,17 +81,22 @@ def __init__(self, auto_area) -> None:
8081

8182
LOGGER.debug(
8283
"%s: Managing light entities: %s",
83-
self.auto_area.area.name,
84+
self.auto_area.area_name,
8485
self.light_entity_ids,
8586
)
87+
8688
if len(self.light_entity_ids) == 0:
8789
LOGGER.warning(
88-
"%s: No light entities found to manage", self.auto_area.area.name
90+
"%s: No light entities found to manage",
91+
self.auto_area.area_name
8992
)
90-
return
9193

9294
async def initialize(self):
9395
"""Start subscribing to state changes."""
96+
LOGGER.debug("AutoLights %s %s",
97+
self.presence_entity_id,
98+
self.illuminance_entity_id
99+
)
94100

95101
if self.is_sleeping_area:
96102
# set initial state
@@ -107,13 +113,23 @@ async def initialize(self):
107113
initial_presence_state = self.hass.states.get(self.presence_entity_id)
108114
if initial_presence_state and self.light_entity_ids:
109115
if initial_presence_state.state == STATE_ON:
116+
LOGGER.info(
117+
"%s: Initial presence detected. Turning lights on %s",
118+
self.auto_area.area_name,
119+
self.light_entity_ids,
120+
)
110121
await self.auto_area.hass.services.async_call(
111122
LIGHT_DOMAIN,
112123
SERVICE_TURN_ON,
113124
{ATTR_ENTITY_ID: self.light_entity_ids},
114125
)
115126
self.lights_turned_on = True
116127
else:
128+
LOGGER.info(
129+
"%s: No initial presence detected. Turning lights off %s",
130+
self.auto_area.area_name,
131+
self.light_entity_ids,
132+
)
117133
await self.auto_area.hass.services.async_call(
118134
LIGHT_DOMAIN,
119135
SERVICE_TURN_OFF,
@@ -139,6 +155,8 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
139155
from_state = event.data.get('old_state')
140156
to_state = event.data.get('new_state')
141157

158+
LOGGER.debug("handle_presence_state_change")
159+
142160
previous_state = from_state.state if from_state else ""
143161
if to_state is None:
144162
return
@@ -159,7 +177,7 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
159177
if self.sleep_mode_enabled:
160178
LOGGER.info(
161179
"%s: Sleep mode is on. Not turning on lights",
162-
self.auto_area.area.name,
180+
self.auto_area.area_name,
163181
)
164182
return
165183

@@ -171,7 +189,7 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
171189
):
172190
LOGGER.info(
173191
"%s: illuminance (%s lx) > threshold (%s lx). Not turning on lights",
174-
self.auto_area.area.name,
192+
self.auto_area.area_name,
175193
current_illuminance,
176194
self.illuminance_threshold,
177195
)
@@ -180,7 +198,7 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
180198
# turn lights on
181199
LOGGER.info(
182200
"%s: Turning lights on %s",
183-
self.auto_area.area.name,
201+
self.auto_area.area_name,
184202
self.light_entity_ids,
185203
)
186204
await self.hass.services.async_call(
@@ -195,7 +213,7 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
195213
if not self.sleep_mode_enabled:
196214
LOGGER.info(
197215
"%s: Turning lights off %s",
198-
self.auto_area.area.name,
216+
self.auto_area.area_name,
199217
self.light_entity_ids,
200218
)
201219
await self.hass.services.async_call(
@@ -207,6 +225,7 @@ async def handle_presence_state_change(self, event: Event[EventStateChangedData]
207225

208226
async def handle_sleep_mode_state_change(self, event: Event[EventStateChangedData]):
209227
"""Handle changes in sleep mode."""
228+
210229
entity_id = event.data.get('entity_id')
211230
from_state = event.data.get('old_state')
212231
to_state = event.data.get('new_state')
@@ -227,7 +246,7 @@ async def handle_sleep_mode_state_change(self, event: Event[EventStateChangedDat
227246
if current_state == STATE_ON:
228247
LOGGER.info(
229248
"%s: Sleep mode enabled - turning lights off %s",
230-
self.auto_area.area.name,
249+
self.auto_area.area_name,
231250
self.light_entity_ids,
232251
)
233252
self.sleep_mode_enabled = True
@@ -238,7 +257,10 @@ async def handle_sleep_mode_state_change(self, event: Event[EventStateChangedDat
238257
)
239258
self.lights_turned_on = False
240259
else:
241-
LOGGER.info("%s: Sleep mode disabled", self.auto_area.area.name)
260+
LOGGER.info(
261+
"%s: Sleep mode disabled",
262+
self.auto_area.area_name,
263+
)
242264
self.sleep_mode_enabled = False
243265
has_presence = (
244266
self.hass.states.get(self.presence_entity_id).state == STATE_ON
@@ -248,7 +270,7 @@ async def handle_sleep_mode_state_change(self, event: Event[EventStateChangedDat
248270
return
249271
LOGGER.info(
250272
"%s: Turning lights on due to presence %s",
251-
self.auto_area.area.name,
273+
self.auto_area.area_name,
252274
self.light_entity_ids,
253275
)
254276
await self.hass.services.async_call(
@@ -273,7 +295,7 @@ async def handle_illuminance_change(self, _event: Event[EventStateChangedData]):
273295
if self.lights_turned_on:
274296
LOGGER.debug(
275297
"%s: Lights were already turned on. Not turning on lights",
276-
self.auto_area.area.name,
298+
self.auto_area.area_name,
277299
)
278300
return
279301

@@ -283,7 +305,7 @@ async def handle_illuminance_change(self, _event: Event[EventStateChangedData]):
283305

284306
LOGGER.info(
285307
"%s: Turning lights on due to illuminance %s",
286-
self.auto_area.area.name,
308+
self.auto_area.area_name,
287309
self.light_entity_ids,
288310
)
289311
await self.hass.services.async_call(
@@ -299,7 +321,7 @@ def is_below_illuminance_threshold(self) -> bool:
299321
if current_illuminance > self.illuminance_threshold:
300322
LOGGER.debug(
301323
"%s: illuminance (%s lx) > threshold (%s lx). Not turning on lights",
302-
self.auto_area.area.name,
324+
self.auto_area.area_name,
303325
current_illuminance,
304326
self.illuminance_threshold,
305327
)
@@ -313,14 +335,17 @@ def get_current_illuminance(self) -> float | None:
313335
current_illuminance = float(
314336
self.hass.states.get(self.illuminance_entity_id).state
315337
)
316-
except ValueError:
338+
except (ValueError, AttributeError):
317339
current_illuminance = None
318340

319341
return current_illuminance
320342

321343
def cleanup(self):
322344
"""Deinitialize this area."""
323-
LOGGER.debug("%s: Disabling light control", self.auto_area.area.name)
345+
LOGGER.debug(
346+
"%s: Disabling light control",
347+
self.auto_area.area_name,
348+
)
324349
if self.unsubscribe_sleep_mode is not None:
325350
self.unsubscribe_sleep_mode()
326351

custom_components/auto_areas/binary_sensor.py

Lines changed: 1 addition & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -2,144 +2,16 @@
22

33
from __future__ import annotations
44

5-
from functools import cached_property
6-
from typing import Literal, override
7-
from homeassistant.core import Event, EventStateChangedData
8-
from homeassistant.const import STATE_ON, STATE_OFF
9-
from homeassistant.components.binary_sensor import (
10-
BinarySensorDeviceClass,
11-
BinarySensorEntity,
12-
)
135
from homeassistant.helpers.entity_platform import AddEntitiesCallback
14-
from homeassistant.helpers.event import async_track_state_change_event
15-
from custom_components.auto_areas.ha_helpers import all_states_are_off
16-
17-
from .auto_entity import AutoEntity
6+
from custom_components.auto_areas.binary_sensors.presence import PresenceBinarySensor
187

198
from .auto_area import AutoArea
209
from .const import (
2110
DOMAIN,
22-
LOGGER,
23-
PRESENCE_BINARY_SENSOR_DEVICE_CLASSES,
24-
PRESENCE_BINARY_SENSOR_ENTITY_PREFIX,
25-
PRESENCE_BINARY_SENSOR_PREFIX,
26-
PRESENCE_ON_STATES,
2711
)
2812

2913

3014
async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback):
3115
"""Set up the binary_sensor platform."""
3216
auto_area: AutoArea = hass.data[DOMAIN][entry.entry_id]
3317
async_add_entities([PresenceBinarySensor(hass, auto_area)])
34-
35-
36-
class PresenceBinarySensor(
37-
AutoEntity[BinarySensorEntity, BinarySensorDeviceClass], BinarySensorEntity
38-
):
39-
"""Set up aggregated presence binary sensor."""
40-
41-
def __init__(self, hass, auto_area: AutoArea) -> None:
42-
"""Initialize presence binary sensor."""
43-
super().__init__(
44-
hass,
45-
auto_area,
46-
BinarySensorDeviceClass.OCCUPANCY,
47-
PRESENCE_BINARY_SENSOR_PREFIX,
48-
PRESENCE_BINARY_SENSOR_ENTITY_PREFIX
49-
)
50-
self.presence: bool | None = None
51-
LOGGER.debug("Presence entities %s", self.entity_ids)
52-
53-
@cached_property
54-
def is_on(self) -> bool | None:
55-
"""Return true if the binary sensor is on."""
56-
return self.presence
57-
58-
@cached_property
59-
def state(self) -> Literal["on", "off"] | None: # type: ignore
60-
"""Return the state of the binary sensor."""
61-
if (is_on := self.is_on) is None:
62-
return None
63-
return STATE_ON if is_on else STATE_OFF
64-
65-
@override
66-
def _get_sensor_entities(self) -> list[str]:
67-
"""Retrieve all relevant presence entities."""
68-
return [
69-
entity.entity_id
70-
for entity in self.auto_area.get_valid_entities()
71-
if entity.device_class in PRESENCE_BINARY_SENSOR_DEVICE_CLASSES
72-
or entity.original_device_class in PRESENCE_BINARY_SENSOR_DEVICE_CLASSES
73-
]
74-
75-
@override
76-
async def async_added_to_hass(self):
77-
"""Start tracking sensors."""
78-
LOGGER.debug(
79-
"%s: Presence detection entities %s",
80-
self.auto_area.area_name,
81-
self.entity_ids,
82-
)
83-
84-
# Set initial presence
85-
self.presence = not all_states_are_off(
86-
self.hass,
87-
self.entity_ids,
88-
PRESENCE_ON_STATES,
89-
)
90-
self.schedule_update_ha_state()
91-
92-
LOGGER.info(
93-
"%s: Initial presence %s",
94-
self.auto_area.area_name,
95-
self.presence
96-
)
97-
98-
# Subscribe to state changes
99-
self.unsubscribe = async_track_state_change_event(
100-
self.hass,
101-
self.entity_ids,
102-
self._handle_state_change,
103-
)
104-
105-
@override
106-
async def _handle_state_change(self, event: Event[EventStateChangedData]) -> None:
107-
"""Handle state change of any tracked presence sensors."""
108-
entity_id = event.data.get('entity_id')
109-
from_state = event.data.get('old_state')
110-
to_state = event.data.get('new_state')
111-
112-
previous_state = from_state.state if from_state else ""
113-
current_state = to_state.state if to_state else ""
114-
115-
LOGGER.debug("presence sensor handling state change %s", current_state)
116-
117-
if previous_state == current_state:
118-
return
119-
120-
LOGGER.debug(
121-
"%s: State change %s: %s -> %s",
122-
self.auto_area.area_name,
123-
entity_id,
124-
previous_state,
125-
current_state,
126-
)
127-
128-
if current_state in PRESENCE_ON_STATES:
129-
if not self.presence:
130-
LOGGER.info("%s: Presence detected", self.auto_area.area_name)
131-
self.presence = True
132-
self.schedule_update_ha_state()
133-
else:
134-
if all_states_are_off(
135-
self.hass,
136-
self.entity_ids,
137-
PRESENCE_ON_STATES,
138-
):
139-
if self.presence:
140-
LOGGER.info(
141-
"%s: Presence cleared",
142-
self.auto_area.area_name
143-
)
144-
self.presence = False
145-
self.schedule_update_ha_state()

0 commit comments

Comments
 (0)