diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index c4bc0aaf000..07f7ded5447 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -17,7 +17,7 @@ from .coordinator import ( process_service_info, ) -PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/govee_ble/binary_sensor.py b/homeassistant/components/govee_ble/binary_sensor.py new file mode 100644 index 00000000000..82033300797 --- /dev/null +++ b/homeassistant/components/govee_ble/binary_sensor.py @@ -0,0 +1,104 @@ +"""Support for govee-ble binary sensors.""" + +from __future__ import annotations + +from govee_ble import ( + BinarySensorDeviceClass as GoveeBLEBinarySensorDeviceClass, + SensorUpdate, +) +from govee_ble.parser import ERROR + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .coordinator import GoveeBLEPassiveBluetoothDataProcessor +from .device import device_key_to_bluetooth_entity_key + +BINARY_SENSOR_DESCRIPTIONS = { + GoveeBLEBinarySensorDeviceClass.WINDOW: BinarySensorEntityDescription( + key=GoveeBLEBinarySensorDeviceClass.WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass_device_info(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the govee-ble BLE sensors.""" + coordinator = entry.runtime_data + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + GoveeBluetoothBinarySensorEntity, async_add_entities + ) + ) + entry.async_on_unload( + coordinator.async_register_processor(processor, BinarySensorEntityDescription) + ) + + +class GoveeBluetoothBinarySensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[bool | None, SensorUpdate] + ], + BinarySensorEntity, +): + """Representation of a govee-ble binary sensor.""" + + processor: GoveeBLEPassiveBluetoothDataProcessor + + @property + def available(self) -> bool: + """Return False if sensor is in error.""" + coordinator = self.processor.coordinator + return self.processor.entity_data.get(self.entity_key) != ERROR and ( + ((model_info := coordinator.model_info) and model_info.sleepy) + or super().available + ) + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/govee_ble/device.py b/homeassistant/components/govee_ble/device.py new file mode 100644 index 00000000000..90b602780a2 --- /dev/null +++ b/homeassistant/components/govee_ble/device.py @@ -0,0 +1,16 @@ +"""Support for govee-ble devices.""" + +from __future__ import annotations + +from govee_ble import DeviceKey + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index 8c9812249a3..a94610ef0e1 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -2,13 +2,12 @@ from __future__ import annotations -from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units +from govee_ble import DeviceClass, SensorUpdate, Units from govee_ble.parser import ERROR from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothEntityKey, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -28,6 +27,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info from .coordinator import GoveeBLEConfigEntry, GoveeBLEPassiveBluetoothDataProcessor +from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -70,13 +70,6 @@ SENSOR_DESCRIPTIONS = { } -def _device_key_to_bluetooth_entity_key( - device_key: DeviceKey, -) -> PassiveBluetoothEntityKey: - """Convert a device key to an entity key.""" - return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: @@ -87,18 +80,18 @@ def sensor_update_to_bluetooth_data_update( for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ - _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() if description.device_class and description.native_unit_of_measurement }, entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + device_key_to_bluetooth_entity_key(device_key): sensor_values.name for device_key, sensor_values in sensor_update.entity_values.items() }, ) diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index b26bfba5830..11f4065b506 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -136,3 +136,29 @@ GV5121_MOTION_SERVICE_INFO_2 = BluetoothServiceInfo( service_uuids=[], source="24:4C:AB:03:E6:B8", ) + + +GV5123_OPEN_SERVICE_INFO = BluetoothServiceInfo( + name="GV51230B3D", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"=\xec\x00\x00\xdeCw\xd5^U\xf9\x91In6\xbd\xc6\x7f\x8b,'\x06t\x97" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) + + +GV5123_CLOSED_SERVICE_INFO = BluetoothServiceInfo( + name="GV51230B3D", + address="C1:37:37:32:0F:45", + rssi=-36, + manufacturer_data={ + 61320: b"=\xec\x00\x01Y\xdbk\xd9\xbe\xd7\xaf\xf7*&\xaaK\xd7-\xfa\x94W>[\xe9" + }, + service_data={}, + service_uuids=[], + source="24:4C:AB:03:E6:B8", +) diff --git a/tests/components/govee_ble/test_binary_sensor.py b/tests/components/govee_ble/test_binary_sensor.py new file mode 100644 index 00000000000..a0acf4c461e --- /dev/null +++ b/tests/components/govee_ble/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Test the Govee BLE binary_sensor.""" + +from homeassistant.components.govee_ble.const import CONF_DEVICE_TYPE, DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from . import GV5123_CLOSED_SERVICE_INFO, GV5123_OPEN_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_window_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the window sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=GV5123_OPEN_SERVICE_INFO.address, + data={CONF_DEVICE_TYPE: "H5123"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info(hass, GV5123_OPEN_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("binary_sensor.51230f45_window") + assert motion_sensor.state == STATE_ON + + inject_bluetooth_service_info(hass, GV5123_CLOSED_SERVICE_INFO) + await hass.async_block_till_done() + + motion_sensor = hass.states.get("binary_sensor.51230f45_window") + assert motion_sensor.state == STATE_OFF + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done()