diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 204f0e07d3e..fcd88563763 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -27,4 +27,6 @@ HOMEKIT_ACCESSORY_DISPATCH = { "temperature": "sensor", "battery": "sensor", "smoke": "binary_sensor", + "fan": "fan", + "fanv2": "fan", } diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py new file mode 100644 index 00000000000..06cbb986714 --- /dev/null +++ b/homeassistant/components/homekit_controller/fan.py @@ -0,0 +1,254 @@ +"""Support for Homekit fans.""" +import logging + +from homekit.model.characteristics import CharacteristicsTypes + +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) + +from . import KNOWN_DEVICES, HomeKitEntity + +_LOGGER = logging.getLogger(__name__) + +# 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that +# its consistent with homeassistant.components.homekit. +DIRECTION_TO_HK = { + DIRECTION_REVERSE: 1, + DIRECTION_FORWARD: 0, +} +HK_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_HK.items()} + +SPEED_TO_PCNT = { + SPEED_HIGH: 100, + SPEED_MEDIUM: 50, + SPEED_LOW: 25, + SPEED_OFF: 0, +} + + +class BaseHomeKitFan(HomeKitEntity, FanEntity): + """Representation of a Homekit fan.""" + + # This must be set in subclasses to the name of a boolean characteristic + # that controls whether the fan is on or off. + on_characteristic = None + + def __init__(self, *args): + """Initialise the fan.""" + self._on = None + self._features = 0 + self._rotation_direction = 0 + self._rotation_speed = 0 + self._swing_mode = 0 + + super().__init__(*args) + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.SWING_MODE, + CharacteristicsTypes.ROTATION_DIRECTION, + CharacteristicsTypes.ROTATION_SPEED, + ] + + def _setup_rotation_direction(self, char): + self._features |= SUPPORT_DIRECTION + + def _setup_rotation_speed(self, char): + self._features |= SUPPORT_SET_SPEED + + def _setup_swing_mode(self, char): + self._features |= SUPPORT_OSCILLATE + + def _update_rotation_direction(self, value): + self._rotation_direction = value + + def _update_rotation_speed(self, value): + self._rotation_speed = value + + def _update_swing_mode(self, value): + self._swing_mode = value + + @property + def is_on(self): + """Return true if device is on.""" + return self._on + + @property + def speed(self): + """Return the current speed.""" + if not self.is_on: + return SPEED_OFF + if self._rotation_speed > SPEED_TO_PCNT[SPEED_MEDIUM]: + return SPEED_HIGH + if self._rotation_speed > SPEED_TO_PCNT[SPEED_LOW]: + return SPEED_MEDIUM + if self._rotation_speed > SPEED_TO_PCNT[SPEED_OFF]: + return SPEED_LOW + return SPEED_OFF + + @property + def speed_list(self): + """Get the list of available speeds.""" + if self.supported_features & SUPPORT_SET_SPEED: + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + return [] + + @property + def current_direction(self): + """Return the current direction of the fan.""" + return HK_DIRECTION_TO_HA[self._rotation_direction] + + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return self._swing_mode == 1 + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + async def async_set_direction(self, direction): + """Set the direction of the fan.""" + if self.supported_features & SUPPORT_DIRECTION: + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["rotation.direction"], + "value": DIRECTION_TO_HK[direction], + } + ] + ) + + async def async_set_speed(self, speed): + """Set the speed of the fan.""" + if speed == SPEED_OFF: + return await self.async_turn_off() + + if self.supported_features & SUPPORT_SET_SPEED: + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["rotation.speed"], + "value": SPEED_TO_PCNT[speed], + } + ] + ) + + async def async_oscillate(self, oscillating: bool): + """Oscillate the fan.""" + if self.supported_features & SUPPORT_OSCILLATE: + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["swing-mode"], + "value": 1 if oscillating else 0, + } + ] + ) + + async def async_turn_on(self, speed=None, **kwargs): + """Turn the specified fan on.""" + + characteristics = [] + + if not self.is_on: + characteristics.append( + { + "aid": self._aid, + "iid": self._chars[self.on_characteristic], + "value": True, + } + ) + + if self.supported_features & SUPPORT_SET_SPEED and speed: + characteristics.append( + { + "aid": self._aid, + "iid": self._chars["rotation.speed"], + "value": SPEED_TO_PCNT[speed], + }, + ) + + if not characteristics: + return + + await self._accessory.put_characteristics(characteristics) + + async def async_turn_off(self, **kwargs): + """Turn the specified fan off.""" + characteristics = [ + { + "aid": self._aid, + "iid": self._chars[self.on_characteristic], + "value": False, + } + ] + await self._accessory.put_characteristics(characteristics) + + +class HomeKitFanV1(BaseHomeKitFan): + """Implement fan support for public.hap.service.fan.""" + + on_characteristic = "on" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [CharacteristicsTypes.ON] + super().get_characteristic_types() + + def _update_on(self, value): + self._on = value == 1 + + +class HomeKitFanV2(BaseHomeKitFan): + """Implement fan support for public.hap.service.fanv2.""" + + on_characteristic = "active" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [CharacteristicsTypes.ACTIVE] + super().get_characteristic_types() + + def _update_active(self, value): + self._on = value == 1 + + +ENTITY_TYPES = { + "fan": HomeKitFanV1, + "fanv2": HomeKitFanV2, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit fans.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py new file mode 100644 index 00000000000..e9fc9b522ea --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -0,0 +1,53 @@ +"""Test against characteristics captured from the Home Assistant HomeKit bridge running demo platforms.""" + +from homeassistant.components.fan import ( + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, +) + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_homeassistant_bridge_fan_setup(hass): + """Test that a SIMPLEconnect fan can be correctly setup in HA.""" + accessories = await setup_accessories_from_file( + hass, "home_assistant_bridge_fan.json" + ) + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the fan is correctly found and set up + fan_id = "fan.living_room_fan" + fan = entity_registry.async_get(fan_id) + assert fan.unique_id == "homekit-fan.living_room_fan-8" + + fan_helper = Helper( + hass, "fan.living_room_fan", pairing, accessories[0], config_entry, + ) + + fan_state = await fan_helper.poll_and_get_state() + assert fan_state.attributes["friendly_name"] == "Living Room Fan" + assert fan_state.state == "off" + assert fan_state.attributes["supported_features"] == ( + SUPPORT_DIRECTION | SUPPORT_SET_SPEED | SUPPORT_OSCILLATE + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(fan.device_id) + assert device.manufacturer == "Home Assistant" + assert device.name == "Living Room Fan" + assert device.model == "Fan" + assert device.sw_version == "0.104.0.dev0" + + bridge = device = device_registry.async_get(device.via_device_id) + assert bridge.manufacturer == "Home Assistant" + assert bridge.name == "Home Assistant Bridge" + assert bridge.model == "Bridge" + assert bridge.sw_version == "0.104.0.dev0" diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py new file mode 100644 index 00000000000..9e88d9d82e9 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -0,0 +1,46 @@ +""" +Test against characteristics captured from a SIMPLEconnect Fan. + +https://github.com/home-assistant/home-assistant/issues/26180 +""" + +from homeassistant.components.fan import SUPPORT_DIRECTION, SUPPORT_SET_SPEED + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_simpleconnect_fan_setup(hass): + """Test that a SIMPLEconnect fan can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "simpleconnect_fan.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the fan is correctly found and set up + fan_id = "fan.simpleconnect_fan_06f674" + fan = entity_registry.async_get(fan_id) + assert fan.unique_id == "homekit-1234567890abcd-8" + + fan_helper = Helper( + hass, "fan.simpleconnect_fan_06f674", pairing, accessories[0], config_entry, + ) + + fan_state = await fan_helper.poll_and_get_state() + assert fan_state.attributes["friendly_name"] == "SIMPLEconnect Fan-06F674" + assert fan_state.state == "off" + assert fan_state.attributes["supported_features"] == ( + SUPPORT_DIRECTION | SUPPORT_SET_SPEED + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(fan.device_id) + assert device.manufacturer == "Hunter Fan" + assert device.name == "SIMPLEconnect Fan-06F674" + assert device.model == "SIMPLEconnect" + assert device.sw_version == "" + assert device.via_device_id is None diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py new file mode 100644 index 00000000000..fe97451cfbb --- /dev/null +++ b/tests/components/homekit_controller/test_fan.py @@ -0,0 +1,407 @@ +"""Basic checks for HomeKit motion sensors and contact sensors.""" +from tests.components.homekit_controller.common import FakeService, setup_test_component + +V1_ON = ("fan", "on") +V1_ROTATION_DIRECTION = ("fan", "rotation.direction") +V1_ROTATION_SPEED = ("fan", "rotation.speed") + +V2_ACTIVE = ("fanv2", "active") +V2_ROTATION_DIRECTION = ("fanv2", "rotation.direction") +V2_ROTATION_SPEED = ("fanv2", "rotation.speed") +V2_SWING_MODE = ("fanv2", "swing-mode") + + +def create_fan_service(): + """ + Define fan v1 characteristics as per HAP spec. + + This service is no longer documented in R2 of the public HAP spec but existing + devices out there use it (like the SIMPLEconnect fan) + """ + service = FakeService("public.hap.service.fan") + + cur_state = service.add_characteristic("on") + cur_state.value = 0 + + cur_state = service.add_characteristic("rotation.direction") + cur_state.value = 0 + + cur_state = service.add_characteristic("rotation.speed") + cur_state.value = 0 + + return service + + +def create_fanv2_service(): + """Define fan v2 characteristics as per HAP spec.""" + service = FakeService("public.hap.service.fanv2") + + cur_state = service.add_characteristic("active") + cur_state.value = 0 + + cur_state = service.add_characteristic("rotation.direction") + cur_state.value = 0 + + cur_state = service.add_characteristic("rotation.speed") + cur_state.value = 0 + + cur_state = service.add_characteristic("swing-mode") + cur_state.value = 0 + + return service + + +async def test_fan_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit fan accessory.""" + sensor = create_fan_service() + helper = await setup_test_component(hass, [sensor]) + + helper.characteristics[V1_ON].value = False + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[V1_ON].value = True + state = await helper.poll_and_get_state() + assert state.state == "on" + + +async def test_turn_on(hass, utcnow): + """Test that we can turn a fan on.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "high"}, + blocking=True, + ) + assert helper.characteristics[V1_ON].value == 1 + assert helper.characteristics[V1_ROTATION_SPEED].value == 100 + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "medium"}, + blocking=True, + ) + assert helper.characteristics[V1_ON].value == 1 + assert helper.characteristics[V1_ROTATION_SPEED].value == 50 + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "low"}, + blocking=True, + ) + assert helper.characteristics[V1_ON].value == 1 + assert helper.characteristics[V1_ROTATION_SPEED].value == 25 + + +async def test_turn_off(hass, utcnow): + """Test that we can turn a fan off.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V1_ON].value = 1 + + await hass.services.async_call( + "fan", "turn_off", {"entity_id": "fan.testdevice"}, blocking=True, + ) + assert helper.characteristics[V1_ON].value == 0 + + +async def test_set_speed(hass, utcnow): + """Test that we set fan speed.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V1_ON].value = 1 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "high"}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_SPEED].value == 100 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "medium"}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_SPEED].value == 50 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "low"}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_SPEED].value == 25 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "off"}, + blocking=True, + ) + assert helper.characteristics[V1_ON].value == 0 + + +async def test_speed_read(hass, utcnow): + """Test that we can read a fans oscillation.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V1_ON].value = 1 + helper.characteristics[V1_ROTATION_SPEED].value = 100 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "high" + + helper.characteristics[V1_ROTATION_SPEED].value = 50 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "medium" + + helper.characteristics[V1_ROTATION_SPEED].value = 25 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "low" + + helper.characteristics[V1_ON].value = 0 + helper.characteristics[V1_ROTATION_SPEED].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "off" + + +async def test_set_direction(hass, utcnow): + """Test that we can set fan spin direction.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + await hass.services.async_call( + "fan", + "set_direction", + {"entity_id": "fan.testdevice", "direction": "reverse"}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_DIRECTION].value == 1 + + await hass.services.async_call( + "fan", + "set_direction", + {"entity_id": "fan.testdevice", "direction": "forward"}, + blocking=True, + ) + assert helper.characteristics[V1_ROTATION_DIRECTION].value == 0 + + +async def test_direction_read(hass, utcnow): + """Test that we can read a fans oscillation.""" + fan = create_fan_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V1_ROTATION_DIRECTION].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["direction"] == "forward" + + helper.characteristics[V1_ROTATION_DIRECTION].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["direction"] == "reverse" + + +async def test_fanv2_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit fan accessory.""" + sensor = create_fanv2_service() + helper = await setup_test_component(hass, [sensor]) + + helper.characteristics[V2_ACTIVE].value = False + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[V2_ACTIVE].value = True + state = await helper.poll_and_get_state() + assert state.state == "on" + + +async def test_v2_turn_on(hass, utcnow): + """Test that we can turn a fan on.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "high"}, + blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 1 + assert helper.characteristics[V2_ROTATION_SPEED].value == 100 + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "medium"}, + blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 1 + assert helper.characteristics[V2_ROTATION_SPEED].value == 50 + + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.testdevice", "speed": "low"}, + blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 1 + assert helper.characteristics[V2_ROTATION_SPEED].value == 25 + + +async def test_v2_turn_off(hass, utcnow): + """Test that we can turn a fan off.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V2_ACTIVE].value = 1 + + await hass.services.async_call( + "fan", "turn_off", {"entity_id": "fan.testdevice"}, blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 0 + + +async def test_v2_set_speed(hass, utcnow): + """Test that we set fan speed.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V2_ACTIVE].value = 1 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "high"}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_SPEED].value == 100 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "medium"}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_SPEED].value == 50 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "low"}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_SPEED].value == 25 + + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.testdevice", "speed": "off"}, + blocking=True, + ) + assert helper.characteristics[V2_ACTIVE].value == 0 + + +async def test_v2_speed_read(hass, utcnow): + """Test that we can read a fans oscillation.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V2_ACTIVE].value = 1 + helper.characteristics[V2_ROTATION_SPEED].value = 100 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "high" + + helper.characteristics[V2_ROTATION_SPEED].value = 50 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "medium" + + helper.characteristics[V2_ROTATION_SPEED].value = 25 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "low" + + helper.characteristics[V2_ACTIVE].value = 0 + helper.characteristics[V2_ROTATION_SPEED].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["speed"] == "off" + + +async def test_v2_set_direction(hass, utcnow): + """Test that we can set fan spin direction.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + await hass.services.async_call( + "fan", + "set_direction", + {"entity_id": "fan.testdevice", "direction": "reverse"}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_DIRECTION].value == 1 + + await hass.services.async_call( + "fan", + "set_direction", + {"entity_id": "fan.testdevice", "direction": "forward"}, + blocking=True, + ) + assert helper.characteristics[V2_ROTATION_DIRECTION].value == 0 + + +async def test_v2_direction_read(hass, utcnow): + """Test that we can read a fans oscillation.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V2_ROTATION_DIRECTION].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["direction"] == "forward" + + helper.characteristics[V2_ROTATION_DIRECTION].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["direction"] == "reverse" + + +async def test_v2_oscillate(hass, utcnow): + """Test that we can control a fans oscillation.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + await hass.services.async_call( + "fan", + "oscillate", + {"entity_id": "fan.testdevice", "oscillating": True}, + blocking=True, + ) + assert helper.characteristics[V2_SWING_MODE].value == 1 + + await hass.services.async_call( + "fan", + "oscillate", + {"entity_id": "fan.testdevice", "oscillating": False}, + blocking=True, + ) + assert helper.characteristics[V2_SWING_MODE].value == 0 + + +async def test_v2_oscillate_read(hass, utcnow): + """Test that we can read a fans oscillation.""" + fan = create_fanv2_service() + helper = await setup_test_component(hass, [fan]) + + helper.characteristics[V2_SWING_MODE].value = 0 + state = await helper.poll_and_get_state() + assert state.attributes["oscillating"] is False + + helper.characteristics[V2_SWING_MODE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["oscillating"] is True diff --git a/tests/fixtures/homekit_controller/home_assistant_bridge_fan.json b/tests/fixtures/homekit_controller/home_assistant_bridge_fan.json new file mode 100644 index 00000000000..77a89574c42 --- /dev/null +++ b/tests/fixtures/homekit_controller/home_assistant_bridge_fan.json @@ -0,0 +1,325 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Bridge" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Home Assistant Bridge" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "homekit.bridge" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 1256851357, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Fan" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Living Room Fan" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "fan.living_room_fan" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "Active", + "format": "uint8", + "iid": 9, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000B0-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "description": "RotationDirection", + "format": "int", + "iid": 10, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000028-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "description": "SwingMode", + "format": "uint8", + "iid": 11, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000B6-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "description": "RotationSpeed", + "format": "float", + "iid": 12, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000029-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + } + ], + "iid": 8, + "stype": "fanv2", + "type": "000000B7-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 766313939, + "services": [ + { + "characteristics": [ + { + "description": "Identify", + "format": "bool", + "iid": 2, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "description": "Manufacturer", + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Home Assistant" + }, + { + "description": "Model", + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "Fan" + }, + { + "description": "Name", + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Ceiling Fan" + }, + { + "description": "SerialNumber", + "format": "string", + "iid": 6, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "fan.ceiling_fan" + }, + { + "description": "FirmwareRevision", + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "0.104.0.dev0" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "Active", + "format": "uint8", + "iid": 9, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000B0-0000-1000-8000-0026BB765291", + "valid-values": [ + 0, + 1 + ], + "value": 0 + }, + { + "description": "RotationSpeed", + "format": "float", + "iid": 10, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000029-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + } + ], + "iid": 8, + "stype": "fanv2", + "type": "000000B7-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file diff --git a/tests/fixtures/homekit_controller/simpleconnect_fan.json b/tests/fixtures/homekit_controller/simpleconnect_fan.json new file mode 100644 index 00000000000..ecdf4fe5673 --- /dev/null +++ b/tests/fixtures/homekit_controller/simpleconnect_fan.json @@ -0,0 +1,769 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "SIMPLEconnect Fan-06F674" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Hunter Fan" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "SIMPLEconnect" + }, + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "1234567890abcd" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 7, + "perms": [ + "pr" + ], + "type": "54", + "value": "0.22" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 9, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "string", + "iid": 10, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hunter Fan" + }, + { + "ev": false, + "format": "float", + "iid": 11, + "maxValue": 100.0, + "minStep": 25.0, + "minValue": 0.0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000029-0000-1000-8000-0026BB765291", + "value": 0.0 + }, + { + "ev": false, + "format": "int", + "iid": 12, + "maxValue": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000028-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "description": "Set Fan Fast On", + "ev": false, + "format": "bool", + "iid": 13, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD83CC0-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Set Fan Fast Off", + "ev": false, + "format": "bool", + "iid": 14, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD83CC1-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Is BLDC in Scope", + "ev": false, + "format": "bool", + "iid": 15, + "perms": [ + "pr", + "ev" + ], + "type": "2BD83CC5-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "ev": false, + "format": "bool", + "iid": 16, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000005-0000-1000-8000-0026BB765291", + "value": false + }, + { + "ev": false, + "format": "int", + "iid": 17, + "perms": [ + "pr", + "ev" + ], + "type": "2BD83CC4-6C60-11E5-A837-0800200C9A66", + "value": 341 + }, + { + "ev": false, + "format": "int", + "iid": 18, + "perms": [ + "pr", + "ev" + ], + "type": "2BD83CC3-6C60-11E5-A837-0800200C9A66", + "value": 0 + } + ], + "iid": 8, + "stype": "fan", + "type": "00000040-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 20, + "perms": [ + "pr" + ], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 21, + "maxLen": 256, + "perms": [ + "pw" + ], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 22, + "perms": [ + "pr", + "ev" + ], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 23, + "perms": [ + "pw" + ], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "iid": 19, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 25, + "perms": [ + "pr" + ], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 26, + "maxLen": 256, + "perms": [ + "pw" + ], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 27, + "perms": [ + "pr", + "ev" + ], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 28, + "perms": [ + "pw" + ], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "iid": 24, + "stype": "Unknown Service: 151909D8-3802-11E4-916C-0800200C9A66", + "type": "151909D8-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 30, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "string", + "iid": 31, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hunter Light" + }, + { + "ev": false, + "format": "int", + "iid": 32, + "maxValue": 100, + "minStep": 10, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "value": 30 + }, + { + "description": "Set Light Dimming", + "ev": false, + "format": "bool", + "iid": 33, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "151909DC-3802-11E4-916C-0800200C9A66", + "value": true + }, + { + "description": "Set Light Security", + "ev": false, + "format": "bool", + "iid": 34, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD815B0-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "description": "Get Light Power", + "ev": false, + "format": "bool", + "iid": 35, + "perms": [ + "pr", + "ev" + ], + "type": "2BD815B5-6C60-11E5-A837-0800200C9A66", + "value": true + } + ], + "iid": 29, + "stype": "lightbulb", + "type": "00000043-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "int", + "iid": 37, + "perms": [ + "pr", + "ev" + ], + "type": "2BD83CC6-6C60-11E5-A837-0800200C9A66", + "value": -65 + }, + { + "ev": false, + "format": "bool", + "iid": 38, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD83CC7-6C60-11E5-A837-0800200C9A66", + "value": false + }, + { + "ev": false, + "format": "string", + "iid": 39, + "maxLen": 256, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "0049CFF1-4B37-11E5-B970-0800200C9A66", + "value": "url, data" + }, + { + "ev": false, + "format": "string", + "iid": 40, + "maxLen": 256, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD83CC2-6C60-11E5-A837-0800200C9A66", + "value": "url, data" + }, + { + "ev": false, + "format": "int", + "iid": 41, + "maxValue": 110, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "0049CFF2-4B37-11E5-B970-0800200C9A66", + "value": 0 + }, + { + "ev": false, + "format": "bool", + "iid": 42, + "perms": [ + "pr", + "ev" + ], + "type": "0049CFF3-4B37-11E5-B970-0800200C9A66", + "value": false + } + ], + "iid": 36, + "stype": "Unknown Service: 0049CFF0-4B37-11E5-B970-0800200C9A66", + "type": "0049CFF0-4B37-11E5-B970-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "string", + "iid": 44, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD815B2-6C60-11E5-A837-0800200C9A66", + "value": "NULL" + }, + { + "ev": false, + "format": "int", + "iid": 45, + "maxValue": 63, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD815B4-6C60-11E5-A837-0800200C9A66", + "value": 7 + }, + { + "ev": false, + "format": "bool", + "iid": 46, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "2BD815B3-6C60-11E5-A837-0800200C9A66", + "value": true + } + ], + "iid": 43, + "stype": "Unknown Service: 2BD815B1-6C60-11E5-A837-0800200C9A66", + "type": "2BD815B1-6C60-11E5-A837-0800200C9A66" + }, + { + "characteristics": [ + { + "ev": false, + "format": "int", + "iid": 48, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC61-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 49, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC62-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 50, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC63-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 51, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC64-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 52, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC65-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 53, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC66-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 54, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC67-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 55, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC68-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 56, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC69-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 57, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6A-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 58, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6B-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 59, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6C-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 60, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6D-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 61, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6E-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 62, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC6F-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 63, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC70-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 64, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC71-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 65, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC72-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 66, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC73-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 67, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "E836DC74-6C6E-11E5-A837-0800200C9A66", + "value": 4294967295 + }, + { + "ev": false, + "format": "int", + "iid": 68, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "CC9EA121-FC1C-11E5-A837-0800200C9A66", + "value": 4294901760 + }, + { + "ev": false, + "format": "int", + "iid": 69, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "CC9EA120-FC1C-11E5-A837-0800200C9A66", + "value": 4294901760 + } + ], + "iid": 47, + "stype": "Unknown Service: E836DC60-6C6E-11E5-A837-0800200C9A66", + "type": "E836DC60-6C6E-11E5-A837-0800200C9A66" + } + ] + } +] \ No newline at end of file