From 14da2fd8c9001cf6343c6bc41a3aefecac373e71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 3 Apr 2019 10:20:56 -0700 Subject: [PATCH] Google Assistant: Add support for open/close binary sensors (#22674) * Google Assistant: Add support for binary sensors * Update test --- .../components/binary_sensor/__init__.py | 116 ++++++++++++++---- .../components/google_assistant/smart_home.py | 5 +- .../components/google_assistant/trait.py | 49 +++++--- .../components/google_assistant/test_trait.py | 110 ++++++++++++----- 4 files changed, 207 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 029ed8faa6b..19054588ee7 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -15,30 +15,100 @@ DOMAIN = 'binary_sensor' SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + '.{}' + +# On means low, Off means normal +DEVICE_CLASS_BATTERY = 'battery' + +# On means cold, Off means normal +DEVICE_CLASS_COLD = 'cold' + +# On means connected, Off means disconnected +DEVICE_CLASS_CONNECTIVITY = 'connectivity' + +# On means open, Off means closed +DEVICE_CLASS_DOOR = 'door' + +# On means open, Off means closed +DEVICE_CLASS_GARAGE_DOOR = 'garage_door' + +# On means gas detected, Off means no gas (clear) +DEVICE_CLASS_GAS = 'gas' + +# On means hot, Off means normal +DEVICE_CLASS_HEAT = 'heat' + +# On means light detected, Off means no light +DEVICE_CLASS_LIGHT = 'light' + +# On means open (unlocked), Off means closed (locked) +DEVICE_CLASS_LOCK = 'lock' + +# On means wet, Off means dry +DEVICE_CLASS_MOISTURE = 'moisture' + +# On means motion detected, Off means no motion (clear) +DEVICE_CLASS_MOTION = 'motion' + +# On means moving, Off means not moving (stopped) +DEVICE_CLASS_MOVING = 'moving' + +# On means occupied, Off means not occupied (clear) +DEVICE_CLASS_OCCUPANCY = 'occupancy' + +# On means open, Off means closed +DEVICE_CLASS_OPENING = 'opening' + +# On means plugged in, Off means unplugged +DEVICE_CLASS_PLUG = 'plug' + +# On means power detected, Off means no power +DEVICE_CLASS_POWER = 'power' + +# On means home, Off means away +DEVICE_CLASS_PRESENCE = 'presence' + +# On means problem detected, Off means no problem (OK) +DEVICE_CLASS_PROBLEM = 'problem' + +# On means unsafe, Off means safe +DEVICE_CLASS_SAFETY = 'safety' + +# On means smoke detected, Off means no smoke (clear) +DEVICE_CLASS_SMOKE = 'smoke' + +# On means sound detected, Off means no sound (clear) +DEVICE_CLASS_SOUND = 'sound' + +# On means vibration detected, Off means no vibration +DEVICE_CLASS_VIBRATION = 'vibration' + +# On means open, Off means closed +DEVICE_CLASS_WINDOW = 'window' + DEVICE_CLASSES = [ - 'battery', # On means low, Off means normal - 'cold', # On means cold, Off means normal - 'connectivity', # On means connected, Off means disconnected - 'door', # On means open, Off means closed - 'garage_door', # On means open, Off means closed - 'gas', # On means gas detected, Off means no gas (clear) - 'heat', # On means hot, Off means normal - 'light', # On means light detected, Off means no light - 'lock', # On means open (unlocked), Off means closed (locked) - 'moisture', # On means wet, Off means dry - 'motion', # On means motion detected, Off means no motion (clear) - 'moving', # On means moving, Off means not moving (stopped) - 'occupancy', # On means occupied, Off means not occupied (clear) - 'opening', # On means open, Off means closed - 'plug', # On means plugged in, Off means unplugged - 'power', # On means power detected, Off means no power - 'presence', # On means home, Off means away - 'problem', # On means problem detected, Off means no problem (OK) - 'safety', # On means unsafe, Off means safe - 'smoke', # On means smoke detected, Off means no smoke (clear) - 'sound', # On means sound detected, Off means no sound (clear) - 'vibration', # On means vibration detected, Off means no vibration - 'window', # On means open, Off means closed + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index d84c8037c60..cf31e3423a7 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -9,7 +9,7 @@ from homeassistant.util.decorator import Registry from homeassistant.core import callback from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_DEVICE_CLASS, ) from homeassistant.components import ( camera, @@ -92,10 +92,11 @@ class _GoogleEntity: state = self.state domain = state.domain features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._traits = [Trait(self.hass, state, self.config) for Trait in trait.TRAITS - if Trait.supported(domain, features)] + if Trait.supported(domain, features, device_class)] return self._traits async def sync_serialize(self): diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index de3a9530b50..41549c021fe 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,6 +2,7 @@ import logging from homeassistant.components import ( + binary_sensor, camera, cover, group, @@ -127,7 +128,7 @@ class BrightnessTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain == light.DOMAIN: return features & light.SUPPORT_BRIGHTNESS @@ -193,7 +194,7 @@ class CameraStreamTrait(_Trait): stream_info = None @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain == camera.DOMAIN: return features & camera.SUPPORT_STREAM @@ -236,7 +237,7 @@ class OnOffTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" return domain in ( group.DOMAIN, @@ -285,7 +286,7 @@ class ColorSpectrumTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain != light.DOMAIN: return False @@ -341,7 +342,7 @@ class ColorTemperatureTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain != light.DOMAIN: return False @@ -414,7 +415,7 @@ class SceneTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" return domain in (scene.DOMAIN, script.DOMAIN) @@ -450,7 +451,7 @@ class DockTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" return domain == vacuum.DOMAIN @@ -484,7 +485,7 @@ class StartStopTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" return domain == vacuum.DOMAIN @@ -554,7 +555,7 @@ class TemperatureSettingTrait(_Trait): google_to_hass = {value: key for key, value in hass_to_google.items()} @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain != climate.DOMAIN: return False @@ -739,7 +740,7 @@ class LockUnlockTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" return domain == lock.DOMAIN @@ -790,7 +791,7 @@ class FanSpeedTrait(_Trait): } @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain != fan.DOMAIN: return False @@ -941,7 +942,7 @@ class ModesTrait(_Trait): } @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" if domain != media_player.DOMAIN: return False @@ -1042,13 +1043,25 @@ class OpenCloseTrait(_Trait): ] @staticmethod - def supported(domain, features): + def supported(domain, features, device_class): """Test if state is supported.""" - return domain == cover.DOMAIN + if domain == cover.DOMAIN: + return True + + return domain == binary_sensor.DOMAIN and device_class in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_LOCK, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ) def sync_attributes(self): """Return opening direction.""" - return {} + attrs = {} + if self.state.domain == binary_sensor.DOMAIN: + attrs['queryOnlyOpenClose'] = True + return attrs def query_attributes(self): """Return state query attributes.""" @@ -1073,6 +1086,12 @@ class OpenCloseTrait(_Trait): else: response['openPercent'] = 0 + elif domain == binary_sensor.DOMAIN: + if self.state.state == STATE_ON: + response['openPercent'] = 100 + else: + response['openPercent'] = 0 + return response async def execute(self, command, data, params): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 81a7fbe1bf7..d85fc692cb9 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -4,6 +4,7 @@ from unittest.mock import patch, Mock import pytest from homeassistant.components import ( + binary_sensor, camera, cover, fan, @@ -22,7 +23,7 @@ from homeassistant.components.google_assistant import trait, helpers, const from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - ATTR_ASSUMED_STATE) + ATTR_DEVICE_CLASS, ATTR_ASSUMED_STATE) from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE from homeassistant.util import color from tests.common import async_mock_service, mock_coro @@ -49,7 +50,7 @@ UNSAFE_CONFIG = helpers.Config( async def test_brightness_light(hass): """Test brightness trait support for light domain.""" assert trait.BrightnessTrait.supported(light.DOMAIN, - light.SUPPORT_BRIGHTNESS) + light.SUPPORT_BRIGHTNESS, None) trt = trait.BrightnessTrait(hass, State('light.bla', light.STATE_ON, { light.ATTR_BRIGHTNESS: 243 @@ -87,7 +88,8 @@ async def test_brightness_light(hass): async def test_brightness_media_player(hass): """Test brightness trait support for media player domain.""" assert trait.BrightnessTrait.supported(media_player.DOMAIN, - media_player.SUPPORT_VOLUME_SET) + media_player.SUPPORT_VOLUME_SET, + None) trt = trait.BrightnessTrait(hass, State( 'media_player.bla', media_player.STATE_PLAYING, { @@ -116,7 +118,7 @@ async def test_camera_stream(hass): """Test camera stream trait support for camera domain.""" hass.config.api = Mock(base_url='http://1.1.1.1:8123') assert trait.CameraStreamTrait.supported(camera.DOMAIN, - camera.SUPPORT_STREAM) + camera.SUPPORT_STREAM, None) trt = trait.CameraStreamTrait( hass, State('camera.bla', camera.STATE_IDLE, {}), BASIC_CONFIG @@ -143,7 +145,7 @@ async def test_camera_stream(hass): async def test_onoff_group(hass): """Test OnOff trait support for group domain.""" - assert trait.OnOffTrait.supported(group.DOMAIN, 0) + assert trait.OnOffTrait.supported(group.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON), BASIC_CONFIG) @@ -181,7 +183,7 @@ async def test_onoff_group(hass): async def test_onoff_input_boolean(hass): """Test OnOff trait support for input_boolean domain.""" - assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0) + assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON), BASIC_CONFIG) @@ -221,7 +223,7 @@ async def test_onoff_input_boolean(hass): async def test_onoff_switch(hass): """Test OnOff trait support for switch domain.""" - assert trait.OnOffTrait.supported(switch.DOMAIN, 0) + assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON), BASIC_CONFIG) @@ -260,7 +262,7 @@ async def test_onoff_switch(hass): async def test_onoff_fan(hass): """Test OnOff trait support for fan domain.""" - assert trait.OnOffTrait.supported(fan.DOMAIN, 0) + assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON), BASIC_CONFIG) @@ -296,7 +298,7 @@ async def test_onoff_fan(hass): async def test_onoff_light(hass): """Test OnOff trait support for light domain.""" - assert trait.OnOffTrait.supported(light.DOMAIN, 0) + assert trait.OnOffTrait.supported(light.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON), BASIC_CONFIG) @@ -334,7 +336,7 @@ async def test_onoff_light(hass): async def test_onoff_media_player(hass): """Test OnOff trait support for media_player domain.""" - assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) + assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON), BASIC_CONFIG) @@ -376,12 +378,12 @@ async def test_onoff_media_player(hass): async def test_onoff_climate(hass): """Test OnOff trait not supported for climate domain.""" assert not trait.OnOffTrait.supported( - climate.DOMAIN, climate.SUPPORT_ON_OFF) + climate.DOMAIN, climate.SUPPORT_ON_OFF, None) async def test_dock_vacuum(hass): """Test dock trait support for vacuum domain.""" - assert trait.DockTrait.supported(vacuum.DOMAIN, 0) + assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None) trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE), BASIC_CONFIG) @@ -404,7 +406,7 @@ async def test_dock_vacuum(hass): async def test_startstop_vacuum(hass): """Test startStop trait support for vacuum domain.""" - assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0) + assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None) trt = trait.StartStopTrait(hass, State('vacuum.bla', vacuum.STATE_PAUSED, { ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_PAUSE, @@ -452,9 +454,9 @@ async def test_startstop_vacuum(hass): async def test_color_spectrum_light(hass): """Test ColorSpectrum trait support for light domain.""" - assert not trait.ColorSpectrumTrait.supported(light.DOMAIN, 0) + assert not trait.ColorSpectrumTrait.supported(light.DOMAIN, 0, None) assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_COLOR) + light.SUPPORT_COLOR, None) trt = trait.ColorSpectrumTrait(hass, State('light.bla', STATE_ON, { light.ATTR_HS_COLOR: (0, 94), @@ -496,9 +498,10 @@ async def test_color_spectrum_light(hass): async def test_color_temperature_light(hass): """Test ColorTemperature trait support for light domain.""" - assert not trait.ColorTemperatureTrait.supported(light.DOMAIN, 0) + assert not trait.ColorTemperatureTrait.supported(light.DOMAIN, 0, None) assert trait.ColorTemperatureTrait.supported(light.DOMAIN, - light.SUPPORT_COLOR_TEMP) + light.SUPPORT_COLOR_TEMP, + None) trt = trait.ColorTemperatureTrait(hass, State('light.bla', STATE_ON, { light.ATTR_MIN_MIREDS: 200, @@ -552,9 +555,10 @@ async def test_color_temperature_light(hass): async def test_color_temperature_light_bad_temp(hass): """Test ColorTemperature trait support for light domain.""" - assert not trait.ColorTemperatureTrait.supported(light.DOMAIN, 0) + assert not trait.ColorTemperatureTrait.supported(light.DOMAIN, 0, None) assert trait.ColorTemperatureTrait.supported(light.DOMAIN, - light.SUPPORT_COLOR_TEMP) + light.SUPPORT_COLOR_TEMP, + None) trt = trait.ColorTemperatureTrait(hass, State('light.bla', STATE_ON, { light.ATTR_MIN_MIREDS: 200, @@ -568,7 +572,7 @@ async def test_color_temperature_light_bad_temp(hass): async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" - assert trait.SceneTrait.supported(scene.DOMAIN, 0) + assert trait.SceneTrait.supported(scene.DOMAIN, 0, None) trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE), BASIC_CONFIG) assert trt.sync_attributes() == {} @@ -585,7 +589,7 @@ async def test_scene_scene(hass): async def test_scene_script(hass): """Test Scene trait support for script domain.""" - assert trait.SceneTrait.supported(script.DOMAIN, 0) + assert trait.SceneTrait.supported(script.DOMAIN, 0, None) trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF), BASIC_CONFIG) assert trt.sync_attributes() == {} @@ -606,9 +610,9 @@ async def test_scene_script(hass): async def test_temperature_setting_climate_onoff(hass): """Test TemperatureSetting trait support for climate domain - range.""" - assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0) + assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( - climate.DOMAIN, climate.SUPPORT_OPERATION_MODE) + climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) hass.config.units.temperature_unit = TEMP_FAHRENHEIT @@ -650,9 +654,9 @@ async def test_temperature_setting_climate_onoff(hass): async def test_temperature_setting_climate_range(hass): """Test TemperatureSetting trait support for climate domain - range.""" - assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0) + assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( - climate.DOMAIN, climate.SUPPORT_OPERATION_MODE) + climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) hass.config.units.temperature_unit = TEMP_FAHRENHEIT @@ -725,9 +729,9 @@ async def test_temperature_setting_climate_range(hass): async def test_temperature_setting_climate_setpoint(hass): """Test TemperatureSetting trait support for climate domain - setpoint.""" - assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0) + assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( - climate.DOMAIN, climate.SUPPORT_OPERATION_MODE) + climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) hass.config.units.temperature_unit = TEMP_CELSIUS @@ -825,7 +829,8 @@ async def test_temperature_setting_climate_setpoint_auto(hass): async def test_lock_unlock_lock(hass): """Test LockUnlock trait locking support for lock domain.""" - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN) + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, + None) trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_UNLOCKED), @@ -850,7 +855,8 @@ async def test_lock_unlock_lock(hass): async def test_lock_unlock_unlock(hass): """Test LockUnlock trait unlocking support for lock domain.""" - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN) + assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, + None) trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_LOCKED), @@ -887,7 +893,8 @@ async def test_lock_unlock_unlock(hass): async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" - assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED) + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, + None) trt = trait.FanSpeedTrait( hass, State( @@ -970,7 +977,7 @@ async def test_fan_speed(hass): async def test_modes(hass): """Test Mode trait.""" assert trait.ModesTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE) + media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None) trt = trait.ModesTrait( hass, State( @@ -1056,9 +1063,9 @@ async def test_modes(hass): async def test_openclose_cover(hass): - """Test cover trait.""" + """Test OpenClose trait support for cover domain.""" assert trait.OpenCloseTrait.supported(cover.DOMAIN, - cover.SUPPORT_SET_POSITION) + cover.SUPPORT_SET_POSITION, None) # No position trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { @@ -1098,3 +1105,40 @@ async def test_openclose_cover(hass): ATTR_ENTITY_ID: 'cover.bla', cover.ATTR_POSITION: 50 } + + +@pytest.mark.parametrize('device_class', ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_LOCK, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, +)) +async def test_openclose_binary_sensor(hass, device_class): + """Test OpenClose trait support for binary_sensor domain.""" + assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, + 0, device_class) + + trt = trait.OpenCloseTrait(hass, State('binary_sensor.test', STATE_ON, { + ATTR_DEVICE_CLASS: device_class, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == { + 'queryOnlyOpenClose': True, + } + + assert trt.query_attributes() == { + 'openPercent': 100 + } + + trt = trait.OpenCloseTrait(hass, State('binary_sensor.test', STATE_OFF, { + ATTR_DEVICE_CLASS: device_class, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == { + 'queryOnlyOpenClose': True, + } + + assert trt.query_attributes() == { + 'openPercent': 0 + }