1 """Support for Amcrest IP camera binary sensors."""
3 from __future__
import annotations
5 from contextlib
import suppress
6 from dataclasses
import dataclass
7 from datetime
import timedelta
9 from typing
import TYPE_CHECKING
11 from amcrest
import AmcrestError
12 import voluptuous
as vol
15 BinarySensorDeviceClass,
17 BinarySensorEntityDescription,
27 BINARY_SENSOR_SCAN_INTERVAL_SECS,
33 from .helpers
import log_update_error, service_signal
36 from .
import AmcrestDevice
39 @dataclass(frozen=True)
41 """Describe Amcrest sensor entity."""
43 event_codes: set[str] |
None =
None
44 should_poll: bool =
False
47 _LOGGER = logging.getLogger(__name__)
49 SCAN_INTERVAL =
timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
50 _ONLINE_SCAN_INTERVAL =
timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS)
52 _AUDIO_DETECTED_KEY =
"audio_detected"
53 _AUDIO_DETECTED_POLLED_KEY =
"audio_detected_polled"
54 _AUDIO_DETECTED_NAME =
"Audio Detected"
55 _AUDIO_DETECTED_EVENT_CODES = {
"AudioMutation",
"AudioIntensity"}
57 _CROSSLINE_DETECTED_KEY =
"crossline_detected"
58 _CROSSLINE_DETECTED_POLLED_KEY =
"crossline_detected_polled"
59 _CROSSLINE_DETECTED_NAME =
"CrossLine Detected"
60 _CROSSLINE_DETECTED_EVENT_CODE =
"CrossLineDetection"
62 _MOTION_DETECTED_KEY =
"motion_detected"
63 _MOTION_DETECTED_POLLED_KEY =
"motion_detected_polled"
64 _MOTION_DETECTED_NAME =
"Motion Detected"
65 _MOTION_DETECTED_EVENT_CODE =
"VideoMotion"
67 _ONLINE_KEY =
"online"
69 BINARY_SENSORS: tuple[AmcrestSensorEntityDescription, ...] = (
71 key=_AUDIO_DETECTED_KEY,
72 name=_AUDIO_DETECTED_NAME,
73 device_class=BinarySensorDeviceClass.SOUND,
74 event_codes=_AUDIO_DETECTED_EVENT_CODES,
77 key=_AUDIO_DETECTED_POLLED_KEY,
78 name=_AUDIO_DETECTED_NAME,
79 device_class=BinarySensorDeviceClass.SOUND,
80 event_codes=_AUDIO_DETECTED_EVENT_CODES,
84 key=_CROSSLINE_DETECTED_KEY,
85 name=_CROSSLINE_DETECTED_NAME,
86 device_class=BinarySensorDeviceClass.MOTION,
87 event_codes={_CROSSLINE_DETECTED_EVENT_CODE},
90 key=_CROSSLINE_DETECTED_POLLED_KEY,
91 name=_CROSSLINE_DETECTED_NAME,
92 device_class=BinarySensorDeviceClass.MOTION,
93 event_codes={_CROSSLINE_DETECTED_EVENT_CODE},
97 key=_MOTION_DETECTED_KEY,
98 name=_MOTION_DETECTED_NAME,
99 device_class=BinarySensorDeviceClass.MOTION,
100 event_codes={_MOTION_DETECTED_EVENT_CODE},
103 key=_MOTION_DETECTED_POLLED_KEY,
104 name=_MOTION_DETECTED_NAME,
105 device_class=BinarySensorDeviceClass.MOTION,
106 event_codes={_MOTION_DETECTED_EVENT_CODE},
112 device_class=BinarySensorDeviceClass.CONNECTIVITY,
116 BINARY_SENSOR_KEYS = [description.key
for description
in BINARY_SENSORS]
117 _EXCLUSIVE_OPTIONS = [
118 {_AUDIO_DETECTED_KEY, _AUDIO_DETECTED_POLLED_KEY},
119 {_MOTION_DETECTED_KEY, _MOTION_DETECTED_POLLED_KEY},
120 {_CROSSLINE_DETECTED_KEY, _CROSSLINE_DETECTED_POLLED_KEY},
123 _UPDATE_MSG =
"Updating %s binary sensor"
127 """Validate binary sensor configurations."""
128 for exclusive_options
in _EXCLUSIVE_OPTIONS:
129 if len(set(value) & exclusive_options) > 1:
131 f
"must contain at most one of {', '.join(exclusive_options)}."
139 async_add_entities: AddEntitiesCallback,
140 discovery_info: DiscoveryInfoType |
None =
None,
142 """Set up a binary sensor for an Amcrest IP Camera."""
143 if discovery_info
is None:
146 name = discovery_info[CONF_NAME]
147 device = hass.data[DATA_AMCREST][DEVICES][name]
148 binary_sensors = discovery_info[CONF_BINARY_SENSORS]
152 for entity_description
in BINARY_SENSORS
153 if entity_description.key
in binary_sensors
160 """Binary sensor for Amcrest camera."""
165 device: AmcrestDevice,
166 entity_description: AmcrestSensorEntityDescription,
168 """Initialize entity."""
172 self.entity_description: AmcrestSensorEntityDescription = entity_description
174 self.
_attr_name_attr_name = f
"{name} {entity_description.name}"
179 """Return True if entity is available."""
180 return self.entity_description.key == _ONLINE_KEY
or self.
_api_api.available
184 if self.entity_description.key == _ONLINE_KEY:
189 @Throttle(_ONLINE_SCAN_INTERVAL)
191 if not (self.
_api_api.available
or self.is_on):
193 _LOGGER.debug(_UPDATE_MSG, self.
namename)
195 if self.
_api_api.available:
199 with suppress(AmcrestError):
200 await self.
_api_api.async_current_time
207 _LOGGER.debug(_UPDATE_MSG, self.
namename)
211 except AmcrestError
as error:
215 if not (event_codes := self.entity_description.event_codes):
216 raise ValueError(f
"Binary sensor {self.name} event codes not set")
219 for event_code
in event_codes:
220 if await self.
_api_api.async_event_channels_happened(event_code):
225 except AmcrestError
as error:
230 """Set the unique id."""
232 serial_number := await self.
_api_api.async_serial_number
235 f
"{serial_number}-{self.entity_description.key}-{self._channel}"
241 _LOGGER.debug(_UPDATE_MSG, self.
namename)
247 """Update state from received event."""
248 _LOGGER.debug(_UPDATE_MSG, self.
namename)
253 """Subscribe to signals."""
254 if self.entity_description.key == _ONLINE_KEY:
272 event_codes := self.entity_description.event_codes
273 )
and not self.entity_description.should_poll:
274 for event_code
in event_codes:
None _async_update_others(self)
None _async_update_unique_id(self)
None _async_update_online(self)
None async_on_demand_update_online(self)
None __init__(self, str name, AmcrestDevice device, AmcrestSensorEntityDescription entity_description)
None async_event_received(self, bool state)
None async_added_to_hass(self)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
str|UndefinedType|None name(self)
list[str] check_binary_sensors(list[str] value)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
None log_update_error(logging.Logger logger, str action, str|UndefinedType|None name, str entity_type, Exception error, int level=logging.ERROR)
str service_signal(str service, *str args)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)