Adopt Hue integration to latest changes in Hue firmware (#101001)
parent
dde4b07c29
commit
415042f356
|
@ -11,6 +11,6 @@
|
|||
"iot_class": "local_push",
|
||||
"loggers": ["aiohue"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohue==4.6.2"],
|
||||
"requirements": ["aiohue==4.7.0"],
|
||||
"zeroconf": ["_hue._tcp.local."]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Support for Hue binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypeAlias
|
||||
from typing import TypeAlias
|
||||
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.config import (
|
||||
|
@ -9,9 +9,17 @@ from aiohue.v2.controllers.config import (
|
|||
EntertainmentConfigurationController,
|
||||
)
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.sensors import MotionController
|
||||
from aiohue.v2.controllers.sensors import (
|
||||
CameraMotionController,
|
||||
ContactController,
|
||||
MotionController,
|
||||
TamperController,
|
||||
)
|
||||
from aiohue.v2.models.camera_motion import CameraMotion
|
||||
from aiohue.v2.models.contact import Contact, ContactState
|
||||
from aiohue.v2.models.entertainment_configuration import EntertainmentStatus
|
||||
from aiohue.v2.models.motion import Motion
|
||||
from aiohue.v2.models.tamper import Tamper, TamperState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
@ -25,8 +33,16 @@ from ..bridge import HueBridge
|
|||
from ..const import DOMAIN
|
||||
from .entity import HueBaseEntity
|
||||
|
||||
SensorType: TypeAlias = Motion | EntertainmentConfiguration
|
||||
ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController
|
||||
SensorType: TypeAlias = (
|
||||
CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper
|
||||
)
|
||||
ControllerType: TypeAlias = (
|
||||
CameraMotionController
|
||||
| ContactController
|
||||
| MotionController
|
||||
| EntertainmentConfigurationController
|
||||
| TamperController
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -57,8 +73,11 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
# setup for each binary-sensor-type hue resource
|
||||
register_items(api.sensors.camera_motion, HueMotionSensor)
|
||||
register_items(api.sensors.motion, HueMotionSensor)
|
||||
register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor)
|
||||
register_items(api.sensors.contact, HueContactSensor)
|
||||
register_items(api.sensors.tamper, HueTamperSensor)
|
||||
|
||||
|
||||
class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity):
|
||||
|
@ -87,12 +106,7 @@ class HueMotionSensor(HueBinarySensorBase):
|
|||
if not self.resource.enabled:
|
||||
# Force None (unknown) if the sensor is set to disabled in Hue
|
||||
return None
|
||||
return self.resource.motion.motion
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {"motion_valid": self.resource.motion.motion_valid}
|
||||
return self.resource.motion.value
|
||||
|
||||
|
||||
class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
||||
|
@ -110,3 +124,30 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase):
|
|||
"""Return sensor name."""
|
||||
type_title = self.resource.type.value.replace("_", " ").title()
|
||||
return f"{self.resource.metadata.name}: {type_title}"
|
||||
|
||||
|
||||
class HueContactSensor(HueBinarySensorBase):
|
||||
"""Representation of a Hue Contact sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.OPENING
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if not self.resource.enabled:
|
||||
# Force None (unknown) if the sensor is set to disabled in Hue
|
||||
return None
|
||||
return self.resource.contact_report.state != ContactState.CONTACT
|
||||
|
||||
|
||||
class HueTamperSensor(HueBinarySensorBase):
|
||||
"""Representation of a Hue Tamper sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.TAMPER
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
if not self.resource.tamper_reports:
|
||||
return False
|
||||
return self.resource.tamper_reports[0].state == TamperState.TAMPERED
|
||||
|
|
|
@ -100,12 +100,7 @@ class HueTemperatureSensor(HueSensorBase):
|
|||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the value reported by the sensor."""
|
||||
return round(self.resource.temperature.temperature, 1)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {"temperature_valid": self.resource.temperature.temperature_valid}
|
||||
return round(self.resource.temperature.value, 1)
|
||||
|
||||
|
||||
class HueLightLevelSensor(HueSensorBase):
|
||||
|
@ -122,14 +117,13 @@ class HueLightLevelSensor(HueSensorBase):
|
|||
# scale used because the human eye adjusts to light levels and small
|
||||
# changes at low lux levels are more noticeable than at high lux
|
||||
# levels.
|
||||
return int(10 ** ((self.resource.light.light_level - 1) / 10000))
|
||||
return int(10 ** ((self.resource.light.value - 1) / 10000))
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
return {
|
||||
"light_level": self.resource.light.light_level,
|
||||
"light_level_valid": self.resource.light.light_level_valid,
|
||||
"light_level": self.resource.light.value,
|
||||
}
|
||||
|
||||
|
||||
|
@ -149,6 +143,8 @@ class HueBatterySensor(HueSensorBase):
|
|||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
if self.resource.power_state.battery_state is None:
|
||||
return {}
|
||||
return {"battery_state": self.resource.power_state.battery_state.value}
|
||||
|
||||
|
||||
|
|
|
@ -256,7 +256,7 @@ aiohomekit==3.0.5
|
|||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.6.2
|
||||
aiohue==4.7.0
|
||||
|
||||
# homeassistant.components.imap
|
||||
aioimaplib==1.0.1
|
||||
|
|
|
@ -234,7 +234,7 @@ aiohomekit==3.0.5
|
|||
aiohttp_cors==0.7.0
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.6.2
|
||||
aiohue==4.7.0
|
||||
|
||||
# homeassistant.components.imap
|
||||
aioimaplib==1.0.1
|
||||
|
|
|
@ -2221,5 +2221,113 @@
|
|||
"id": "52612630-841e-4d39-9763-60346a0da759",
|
||||
"is_configured": true,
|
||||
"type": "geolocation"
|
||||
},
|
||||
{
|
||||
"id": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
|
||||
"product_data": {
|
||||
"model_id": "SOC001",
|
||||
"manufacturer_name": "Signify Netherlands B.V.",
|
||||
"product_name": "Hue secure contact sensor",
|
||||
"product_archetype": "unknown_archetype",
|
||||
"certified": true,
|
||||
"software_version": "2.67.9",
|
||||
"hardware_platform_type": "100b-125"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "Test contact sensor",
|
||||
"archetype": "unknown_archetype"
|
||||
},
|
||||
"identify": {},
|
||||
"services": [
|
||||
{
|
||||
"rid": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
|
||||
"rtype": "contact"
|
||||
},
|
||||
{
|
||||
"rid": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
|
||||
"rtype": "tamper"
|
||||
}
|
||||
],
|
||||
"type": "device"
|
||||
},
|
||||
{
|
||||
"id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
|
||||
"owner": {
|
||||
"rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
|
||||
"rtype": "device"
|
||||
},
|
||||
"enabled": true,
|
||||
"contact_report": {
|
||||
"changed": "2023-09-27T10:01:36.968Z",
|
||||
"state": "contact"
|
||||
},
|
||||
"type": "contact"
|
||||
},
|
||||
{
|
||||
"id": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
|
||||
"owner": {
|
||||
"rid": "0240be0e-8b79-4a53-b9bb-b17fa14d7e75",
|
||||
"rtype": "device"
|
||||
},
|
||||
"tamper_reports": [
|
||||
{
|
||||
"changed": "2023-09-25T10:02:08.774Z",
|
||||
"source": "battery_door",
|
||||
"state": "not_tampered"
|
||||
}
|
||||
],
|
||||
"type": "tamper"
|
||||
},
|
||||
{
|
||||
"id": "1cbda90c-b675-46b0-9e97-278e7e7857ed",
|
||||
"id_v1": "/sensors/249",
|
||||
"product_data": {
|
||||
"model_id": "CAMERA",
|
||||
"manufacturer_name": "Signify Netherlands B.V.",
|
||||
"product_name": "Fake Hue Test Camera",
|
||||
"product_archetype": "unknown_archetype",
|
||||
"certified": true,
|
||||
"software_version": "0.0.0",
|
||||
"hardware_platform_type": "0"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "Test Camera",
|
||||
"archetype": "unknown_archetype"
|
||||
},
|
||||
"identify": {},
|
||||
"usertest": {
|
||||
"status": "set",
|
||||
"usertest": false
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"rid": "d9f2cfee-5879-426b-aa1f-553af8f38176",
|
||||
"rtype": "camera_motion"
|
||||
}
|
||||
],
|
||||
"type": "device"
|
||||
},
|
||||
{
|
||||
"id": "d9f2cfee-5879-426b-aa1f-553af8f38176",
|
||||
"id_v1": "/sensors/249",
|
||||
"owner": {
|
||||
"rid": "1cbda90c-b675-46b0-9e97-278e7e7857ed",
|
||||
"rtype": "device"
|
||||
},
|
||||
"enabled": true,
|
||||
"motion": {
|
||||
"motion": true,
|
||||
"motion_valid": true,
|
||||
"motion_report": {
|
||||
"changed": "2023-09-27T10:06:41.822Z",
|
||||
"motion": true
|
||||
}
|
||||
},
|
||||
"sensitivity": {
|
||||
"status": "set",
|
||||
"sensitivity": 2,
|
||||
"sensitivity_max": 4
|
||||
},
|
||||
"type": "motion"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -14,8 +14,8 @@ async def test_binary_sensors(
|
|||
await setup_platform(hass, mock_bridge_v2, "binary_sensor")
|
||||
# there shouldn't have been any requests at this point
|
||||
assert len(mock_bridge_v2.mock_requests) == 0
|
||||
# 2 binary_sensors should be created from test data
|
||||
assert len(hass.states.async_all()) == 2
|
||||
# 5 binary_sensors should be created from test data
|
||||
assert len(hass.states.async_all()) == 5
|
||||
|
||||
# test motion sensor
|
||||
sensor = hass.states.get("binary_sensor.hue_motion_sensor_motion")
|
||||
|
@ -23,7 +23,6 @@ async def test_binary_sensors(
|
|||
assert sensor.state == "off"
|
||||
assert sensor.name == "Hue motion sensor Motion"
|
||||
assert sensor.attributes["device_class"] == "motion"
|
||||
assert sensor.attributes["motion_valid"] is True
|
||||
|
||||
# test entertainment room active sensor
|
||||
sensor = hass.states.get(
|
||||
|
@ -34,6 +33,51 @@ async def test_binary_sensors(
|
|||
assert sensor.name == "Entertainmentroom 1: Entertainment Configuration"
|
||||
assert sensor.attributes["device_class"] == "running"
|
||||
|
||||
# test contact sensor
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.name == "Test contact sensor Contact"
|
||||
assert sensor.attributes["device_class"] == "opening"
|
||||
# test contact sensor disabled == state unknown
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"enabled": False,
|
||||
"id": "18802b4a-b2f6-45dc-8813-99cde47f3a4a",
|
||||
"type": "contact",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_contact")
|
||||
assert sensor.state == "unknown"
|
||||
|
||||
# test tamper sensor
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper")
|
||||
assert sensor is not None
|
||||
assert sensor.state == "off"
|
||||
assert sensor.name == "Test contact sensor Tamper"
|
||||
assert sensor.attributes["device_class"] == "tamper"
|
||||
# test tamper sensor when no tamper reports exist
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": "d7fcfab0-69e1-4afb-99df-6ed505211db4",
|
||||
"tamper_reports": [],
|
||||
"type": "tamper",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
sensor = hass.states.get("binary_sensor.test_contact_sensor_tamper")
|
||||
assert sensor.state == "off"
|
||||
|
||||
# test camera_motion sensor
|
||||
sensor = hass.states.get("binary_sensor.test_camera_motion")
|
||||
assert sensor is not None
|
||||
assert sensor.state == "on"
|
||||
assert sensor.name == "Test Camera Motion"
|
||||
assert sensor.attributes["device_class"] == "motion"
|
||||
|
||||
|
||||
async def test_binary_sensor_add_update(hass: HomeAssistant, mock_bridge_v2) -> None:
|
||||
"""Test if binary_sensor get added/updated from events."""
|
||||
|
|
|
@ -28,7 +28,6 @@ async def test_sensors(
|
|||
assert sensor.attributes["device_class"] == "temperature"
|
||||
assert sensor.attributes["state_class"] == "measurement"
|
||||
assert sensor.attributes["unit_of_measurement"] == "°C"
|
||||
assert sensor.attributes["temperature_valid"] is True
|
||||
|
||||
# test illuminance sensor
|
||||
sensor = hass.states.get("sensor.hue_motion_sensor_illuminance")
|
||||
|
@ -39,7 +38,6 @@ async def test_sensors(
|
|||
assert sensor.attributes["state_class"] == "measurement"
|
||||
assert sensor.attributes["unit_of_measurement"] == "lx"
|
||||
assert sensor.attributes["light_level"] == 18027
|
||||
assert sensor.attributes["light_level_valid"] is True
|
||||
|
||||
# test battery sensor
|
||||
sensor = hass.states.get("sensor.wall_switch_with_2_controls_battery")
|
||||
|
|
|
@ -14,8 +14,8 @@ async def test_switch(
|
|||
await setup_platform(hass, mock_bridge_v2, "switch")
|
||||
# there shouldn't have been any requests at this point
|
||||
assert len(mock_bridge_v2.mock_requests) == 0
|
||||
# 2 entities should be created from test data
|
||||
assert len(hass.states.async_all()) == 2
|
||||
# 3 entities should be created from test data
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
||||
# test config switch to enable/disable motion sensor
|
||||
test_entity = hass.states.get("switch.hue_motion_sensor_motion")
|
||||
|
|
Loading…
Reference in New Issue