1 """Support for Homekit sensors."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from dataclasses
import dataclass
7 from enum
import IntEnum
9 from aiohomekit.model
import Accessory, Transport
10 from aiohomekit.model.characteristics
import Characteristic, CharacteristicsTypes
11 from aiohomekit.model.characteristics.const
import (
12 CurrentAirPurifierStateValues,
13 ThreadNodeCapabilities,
16 from aiohomekit.model.services
import Service, ServicesTypes
19 async_ble_device_from_address,
20 async_last_service_info,
25 SensorEntityDescription,
30 CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
31 CONCENTRATION_PARTS_PER_MILLION,
34 SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
37 UnitOfElectricCurrent,
38 UnitOfElectricPotential,
49 from .
import KNOWN_DEVICES
50 from .connection
import HKDevice
51 from .entity
import CharacteristicEntity, HomeKitEntity
52 from .utils
import folded_name
55 @dataclass(frozen=True)
57 """Describes Homekit sensor."""
59 probe: Callable[[Characteristic], bool] |
None =
None
60 format: Callable[[Characteristic], str] |
None =
None
61 enum: dict[IntEnum, str] |
None =
None
65 """Return the thread device type as a string.
67 The underlying value is a bitmask, but we want to turn that to
68 a human readable string. Some devices will have multiple capabilities.
69 For example, an NL55 is SLEEPY | MINIMAL. In that case we return the
72 https://openthread.io/guides/thread-primer/node-roles-and-types
75 val = ThreadNodeCapabilities(char.value)
77 if val & ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE:
79 return "border_router_capable"
81 if val & ThreadNodeCapabilities.ROUTER_ELIGIBLE:
83 return "router_eligible"
85 if val & ThreadNodeCapabilities.FULL:
89 if val & ThreadNodeCapabilities.MINIMAL:
93 if val & ThreadNodeCapabilities.SLEEPY:
102 """Return the thread status as a string.
104 The underlying value is a bitmask, but we want to turn that to
105 a human readable string. So we check the flags in order. E.g. BORDER_ROUTER implies
106 ROUTER, so its more important to show that value.
109 val = ThreadStatus(char.value)
111 if val & ThreadStatus.BORDER_ROUTER:
116 return "border_router"
118 if val & ThreadStatus.LEADER:
125 if val & ThreadStatus.ROUTER:
130 if val & ThreadStatus.CHILD:
135 if val & ThreadStatus.JOINING:
139 if val & ThreadStatus.DETACHED:
148 SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
150 key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT,
152 device_class=SensorDeviceClass.POWER,
153 state_class=SensorStateClass.MEASUREMENT,
154 native_unit_of_measurement=UnitOfPower.WATT,
157 key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS,
159 device_class=SensorDeviceClass.CURRENT,
160 state_class=SensorStateClass.MEASUREMENT,
161 native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
164 key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_AMPS_20,
166 device_class=SensorDeviceClass.CURRENT,
167 state_class=SensorStateClass.MEASUREMENT,
168 native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
171 key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_KW_HOUR,
173 device_class=SensorDeviceClass.ENERGY,
174 state_class=SensorStateClass.TOTAL_INCREASING,
175 native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
178 key=CharacteristicsTypes.VENDOR_EVE_ENERGY_WATT,
180 device_class=SensorDeviceClass.POWER,
181 state_class=SensorStateClass.MEASUREMENT,
182 native_unit_of_measurement=UnitOfPower.WATT,
185 key=CharacteristicsTypes.VENDOR_EVE_ENERGY_KW_HOUR,
187 device_class=SensorDeviceClass.ENERGY,
188 state_class=SensorStateClass.TOTAL_INCREASING,
189 native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
192 key=CharacteristicsTypes.VENDOR_EVE_ENERGY_VOLTAGE,
194 device_class=SensorDeviceClass.VOLTAGE,
195 state_class=SensorStateClass.MEASUREMENT,
196 native_unit_of_measurement=UnitOfElectricPotential.VOLT,
199 key=CharacteristicsTypes.VENDOR_EVE_ENERGY_AMPERE,
201 device_class=SensorDeviceClass.CURRENT,
202 state_class=SensorStateClass.MEASUREMENT,
203 native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
206 key=CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY,
208 device_class=SensorDeviceClass.POWER,
209 state_class=SensorStateClass.MEASUREMENT,
210 native_unit_of_measurement=UnitOfPower.WATT,
213 key=CharacteristicsTypes.VENDOR_KOOGEEK_REALTIME_ENERGY_2,
215 device_class=SensorDeviceClass.POWER,
216 state_class=SensorStateClass.MEASUREMENT,
217 native_unit_of_measurement=UnitOfPower.WATT,
220 key=CharacteristicsTypes.VENDOR_EVE_DEGREE_AIR_PRESSURE,
222 device_class=SensorDeviceClass.PRESSURE,
223 state_class=SensorStateClass.MEASUREMENT,
224 native_unit_of_measurement=UnitOfPressure.HPA,
227 key=CharacteristicsTypes.VENDOR_VOCOLINC_OUTLET_ENERGY,
229 device_class=SensorDeviceClass.POWER,
230 state_class=SensorStateClass.MEASUREMENT,
231 native_unit_of_measurement=UnitOfPower.WATT,
234 key=CharacteristicsTypes.TEMPERATURE_CURRENT,
235 name=
"Current Temperature",
236 device_class=SensorDeviceClass.TEMPERATURE,
237 state_class=SensorStateClass.MEASUREMENT,
238 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
241 probe=(
lambda char: char.service.type != ServicesTypes.TEMPERATURE_SENSOR),
244 key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
245 name=
"Current Humidity",
246 device_class=SensorDeviceClass.HUMIDITY,
247 state_class=SensorStateClass.MEASUREMENT,
248 native_unit_of_measurement=PERCENTAGE,
251 probe=(
lambda char: char.service.type != ServicesTypes.HUMIDITY_SENSOR),
254 key=CharacteristicsTypes.AIR_QUALITY,
256 device_class=SensorDeviceClass.AQI,
257 state_class=SensorStateClass.MEASUREMENT,
260 key=CharacteristicsTypes.DENSITY_PM25,
261 name=
"PM2.5 Density",
262 device_class=SensorDeviceClass.PM25,
263 state_class=SensorStateClass.MEASUREMENT,
264 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
267 key=CharacteristicsTypes.DENSITY_PM10,
269 device_class=SensorDeviceClass.PM10,
270 state_class=SensorStateClass.MEASUREMENT,
271 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
274 key=CharacteristicsTypes.DENSITY_OZONE,
275 name=
"Ozone Density",
276 device_class=SensorDeviceClass.OZONE,
277 state_class=SensorStateClass.MEASUREMENT,
278 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
281 key=CharacteristicsTypes.DENSITY_NO2,
282 name=
"Nitrogen Dioxide Density",
283 device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
284 state_class=SensorStateClass.MEASUREMENT,
285 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
288 key=CharacteristicsTypes.DENSITY_SO2,
289 name=
"Sulphur Dioxide Density",
290 device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
291 state_class=SensorStateClass.MEASUREMENT,
292 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
295 key=CharacteristicsTypes.DENSITY_VOC,
296 name=
"Volatile Organic Compound Density",
297 device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
298 state_class=SensorStateClass.MEASUREMENT,
299 native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
302 key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES,
303 name=
"Thread Capabilities",
304 entity_category=EntityCategory.DIAGNOSTIC,
305 format=thread_node_capability_to_str,
306 device_class=SensorDeviceClass.ENUM,
308 "border_router_capable",
315 translation_key=
"thread_node_capabilities",
318 key=CharacteristicsTypes.THREAD_STATUS,
319 name=
"Thread Status",
320 entity_category=EntityCategory.DIAGNOSTIC,
321 format=thread_status_to_str,
322 device_class=SensorDeviceClass.ENUM,
332 translation_key=
"thread_status",
335 key=CharacteristicsTypes.AIR_PURIFIER_STATE_CURRENT,
336 name=
"Air Purifier Status",
337 entity_category=EntityCategory.DIAGNOSTIC,
338 device_class=SensorDeviceClass.ENUM,
340 CurrentAirPurifierStateValues.INACTIVE:
"inactive",
341 CurrentAirPurifierStateValues.IDLE:
"idle",
342 CurrentAirPurifierStateValues.ACTIVE:
"purifying",
344 translation_key=
"air_purifier_state_current",
347 key=CharacteristicsTypes.VENDOR_NETATMO_NOISE,
349 state_class=SensorStateClass.MEASUREMENT,
350 native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
351 device_class=SensorDeviceClass.SOUND_PRESSURE,
354 key=CharacteristicsTypes.FILTER_LIFE_LEVEL,
355 name=
"Filter lifetime",
356 state_class=SensorStateClass.MEASUREMENT,
357 native_unit_of_measurement=PERCENTAGE,
360 key=CharacteristicsTypes.VENDOR_EVE_THERMO_VALVE_POSITION,
361 name=
"Valve position",
362 translation_key=
"valve_position",
363 entity_category=EntityCategory.DIAGNOSTIC,
364 state_class=SensorStateClass.MEASUREMENT,
365 native_unit_of_measurement=PERCENTAGE,
371 """Representation of a HomeKit sensor."""
373 _attr_state_class = SensorStateClass.MEASUREMENT
377 """Return the name of the device."""
378 full_name = super().name
385 return f
"{full_name} {default_name}"
390 """Representation of a Homekit humidity sensor."""
392 _attr_device_class = SensorDeviceClass.HUMIDITY
393 _attr_native_unit_of_measurement = PERCENTAGE
396 """Define the homekit characteristics the entity is tracking."""
397 return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT]
401 """Return the default name of the device."""
406 """Return the current humidity."""
407 return self.
serviceservice.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
411 """Representation of a Homekit temperature sensor."""
413 _attr_device_class = SensorDeviceClass.TEMPERATURE
414 _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
417 """Define the homekit characteristics the entity is tracking."""
418 return [CharacteristicsTypes.TEMPERATURE_CURRENT]
422 """Return the default name of the device."""
427 """Return the current temperature in Celsius."""
428 return self.
serviceservice.value(CharacteristicsTypes.TEMPERATURE_CURRENT)
432 """Representation of a Homekit light level sensor."""
434 _attr_device_class = SensorDeviceClass.ILLUMINANCE
435 _attr_native_unit_of_measurement = LIGHT_LUX
438 """Define the homekit characteristics the entity is tracking."""
439 return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT]
443 """Return the default name of the device."""
448 """Return the current light level in lux."""
449 return self.
serviceservice.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT)
453 """Representation of a Homekit Carbon Dioxide sensor."""
455 _attr_device_class = SensorDeviceClass.CO2
456 _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
459 """Define the homekit characteristics the entity is tracking."""
460 return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL]
464 """Return the default name of the device."""
465 return "Carbon Dioxide"
469 """Return the current CO2 level in ppm."""
470 return self.
serviceservice.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL)
474 """Representation of a Homekit battery sensor."""
476 _attr_device_class = SensorDeviceClass.BATTERY
477 _attr_native_unit_of_measurement = PERCENTAGE
478 _attr_entity_category = EntityCategory.DIAGNOSTIC
481 """Define the homekit characteristics the entity is tracking."""
483 CharacteristicsTypes.BATTERY_LEVEL,
484 CharacteristicsTypes.STATUS_LO_BATT,
485 CharacteristicsTypes.CHARGING_STATE,
490 """Return the default name of the device."""
495 """Return the sensor icon."""
498 return "mdi:battery-unknown"
504 if is_charging
and native_value > 10:
505 percentage =
int(round(native_value / 20 - 0.01)) * 20
506 icon += f
"-charging-{percentage}"
511 elif native_value < 95:
512 percentage =
max(
int(round(native_value / 10 - 0.01)) * 10, 10)
513 icon += f
"-{percentage}"
519 """Return true if battery level is low."""
520 return self.
serviceservice.value(CharacteristicsTypes.STATUS_LO_BATT) == 1
524 """Return true if currently charging."""
528 return self.
serviceservice.value(CharacteristicsTypes.CHARGING_STATE) == 1
532 """Return the current battery level percentage."""
533 return self.
serviceservice.value(CharacteristicsTypes.BATTERY_LEVEL)
537 """A simple sensor for a single characteristic.
539 This may be an additional secondary entity that is part of another service. An
540 example is a switch that has an energy sensor.
542 These *have* to have a different unique_id to the normal sensors as there could
543 be multiple entities per HomeKit service (this was not previously the case).
546 entity_description: HomeKitSensorEntityDescription
552 char: Characteristic,
553 description: HomeKitSensorEntityDescription,
555 """Initialise a secondary HomeKit characteristic sensor."""
562 """Define the homekit characteristics the entity is tracking."""
563 return [self.
_char_char.type]
567 """Return the name of the device if any."""
569 return f
"{name} {self.entity_description.name}"
570 return f
"{self.entity_description.name}"
574 """Return the current sensor value."""
579 return self.
_char_char.value
583 ServicesTypes.HUMIDITY_SENSOR: HomeKitHumiditySensor,
584 ServicesTypes.TEMPERATURE_SENSOR: HomeKitTemperatureSensor,
585 ServicesTypes.LIGHT_SENSOR: HomeKitLightSensor,
586 ServicesTypes.CARBON_DIOXIDE_SENSOR: HomeKitCarbonDioxideSensor,
587 ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor,
591 REQUIRED_CHAR_BY_TYPE = {
592 ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL,
597 """HomeKit Controller RSSI sensor."""
599 _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH
600 _attr_entity_category = EntityCategory.DIAGNOSTIC
601 _attr_entity_registry_enabled_default =
False
602 _attr_has_entity_name =
True
603 _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
604 _attr_should_poll =
False
606 def __init__(self, accessory: HKDevice, devinfo: ConfigType) ->
None:
607 """Initialise a HomeKit Controller RSSI sensor."""
608 super().
__init__(accessory, devinfo)
612 """Define the homekit characteristics the entity cares about."""
617 """Return if the bluetooth device is available."""
618 address = self.
_accessory_accessory.pairing_data[
"AccessoryAddress"]
623 """Return the name of the sensor."""
624 return "Signal strength"
628 """Return the old ID of this device."""
629 serial = self.
accessory_infoaccessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
630 return f
"homekit-{serial}-rssi"
634 """Return the current rssi value."""
635 address = self.
_accessory_accessory.pairing_data[
"AccessoryAddress"]
637 return last_service_info.rssi
if last_service_info
else None
642 config_entry: ConfigEntry,
643 async_add_entities: AddEntitiesCallback,
645 """Set up Homekit sensors."""
646 hkid = config_entry.data[
"AccessoryPairingID"]
647 conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
650 def async_add_service(service: Service) -> bool:
651 if not (entity_class := ENTITY_TYPES.get(service.type)):
654 required_char := REQUIRED_CHAR_BY_TYPE.get(service.type)
655 )
and not service.has(required_char):
657 info = {
"aid": service.accessory.aid,
"iid": service.iid}
658 entity: HomeKitSensor = entity_class(conn, info)
659 conn.async_migrate_unique_id(
660 entity.old_unique_id, entity.unique_id, Platform.SENSOR
665 conn.add_listener(async_add_service)
669 if not (description := SIMPLE_SENSOR.get(char.type)):
671 if description.probe
and not description.probe(char):
673 info = {
"aid": char.service.accessory.aid,
"iid": char.service.iid}
675 conn.async_migrate_unique_id(
676 entity.old_unique_id, entity.unique_id, Platform.SENSOR
682 conn.add_char_factory(async_add_characteristic)
685 def async_add_accessory(accessory: Accessory) -> bool:
686 if conn.pairing.transport != Transport.BLE:
689 accessory_info = accessory.services.first(
690 service_type=ServicesTypes.ACCESSORY_INFORMATION
692 assert accessory_info
693 info = {
"aid": accessory.aid,
"iid": accessory_info.iid}
695 conn.async_migrate_unique_id(
696 entity.old_unique_id, entity.unique_id, Platform.SENSOR
701 conn.add_accessory_factory(async_add_accessory)
str|None default_name(self)
bool is_low_battery(self)
list[str] get_characteristic_types(self)
list[str] get_characteristic_types(self)
list[str] get_characteristic_types(self)
list[str] get_characteristic_types(self)
list[str] get_characteristic_types(self)
str|int|float native_value(self)
list[str] get_characteristic_types(self)
None __init__(self, HKDevice conn, ConfigType info, Characteristic char, HomeKitSensorEntityDescription description)
StateType|date|datetime|Decimal native_value(self)
BLEDevice|None async_ble_device_from_address(HomeAssistant hass, str address, bool connectable=True)
BluetoothServiceInfoBleak|None async_last_service_info(HomeAssistant hass, str address, bool connectable=True)
bool async_add_characteristic(Characteristic char)
str thread_node_capability_to_str(Characteristic char)
str thread_status_to_str(Characteristic char)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
str folded_name(str name)