From 5c8271552a3023808e272125f71ba79f3a1e97d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Jan 2022 05:19:12 -1000 Subject: [PATCH] Add hardware revision support to homekit (#63336) --- homeassistant/components/homekit/__init__.py | 3 ++ .../components/homekit/accessories.py | 21 +++++++++--- homeassistant/components/homekit/const.py | 2 ++ homeassistant/components/homekit/util.py | 11 +++++-- tests/components/homekit/test_accessories.py | 32 +++++++++++++++++++ tests/components/homekit/test_homekit.py | 2 ++ tests/components/homekit/test_util.py | 20 +++++++----- 7 files changed, 77 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2f2053aa83e..075215c449c 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SW_VERSION, @@ -911,6 +912,8 @@ class HomeKit: config[ATTR_MODEL] = device_entry.model if device_entry.sw_version: config[ATTR_SW_VERSION] = device_entry.sw_version + if device_entry.hw_version: + config[ATTR_HW_VERSION] = device_entry.hw_version if device_entry.config_entries: first_entry = list(device_entry.config_entries)[0] if entry := self.hass.config_entries.async_get_entry(first_entry): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 75a3a0fe797..d8d5a16ac50 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SERVICE, @@ -43,6 +44,7 @@ from .const import ( BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, + CHAR_HARDWARE_REVISION, CHAR_STATUS_LOW_BATTERY, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_CHARGING_SENSOR, @@ -59,6 +61,7 @@ from .const import ( MAX_MODEL_LENGTH, MAX_SERIAL_LENGTH, MAX_VERSION_LENGTH, + SERV_ACCESSORY_INFO, SERV_BATTERY_SERVICE, SERVICE_HOMEKIT_RESET_ACCESSORY, TYPE_FAUCET, @@ -74,7 +77,7 @@ from .util import ( async_show_setup_message, cleanup_name_for_homekit, convert_to_float, - format_sw_version, + format_version, validate_media_player_features, ) @@ -256,7 +259,7 @@ class HomeAccessory(Accessory): domain = split_entity_id(entity_id)[0].replace("_", " ") if self.config.get(ATTR_MANUFACTURER) is not None: - manufacturer = self.config[ATTR_MANUFACTURER] + manufacturer = str(self.config[ATTR_MANUFACTURER]) elif self.config.get(ATTR_INTEGRATION) is not None: manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title() elif domain: @@ -264,16 +267,19 @@ class HomeAccessory(Accessory): else: manufacturer = MANUFACTURER if self.config.get(ATTR_MODEL) is not None: - model = self.config[ATTR_MODEL] + model = str(self.config[ATTR_MODEL]) elif domain: model = domain.title() else: model = MANUFACTURER sw_version = None if self.config.get(ATTR_SW_VERSION) is not None: - sw_version = format_sw_version(self.config[ATTR_SW_VERSION]) + sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: sw_version = __version__ + hw_version = None + if self.config.get(ATTR_HW_VERSION) is not None: + hw_version = format_version(self.config[ATTR_HW_VERSION]) self.set_info_service( manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH], @@ -281,6 +287,13 @@ class HomeAccessory(Accessory): serial_number=serial_number[:MAX_SERIAL_LENGTH], firmware_revision=sw_version[:MAX_VERSION_LENGTH], ) + if hw_version: + serv_info = self.get_service(SERV_ACCESSORY_INFO) + char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION) + serv_info.add_characteristic(char) + serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version) + self.iid_manager.assign(char) + char.broker = self self.category = category self.entity_id = entity_id diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 63357fe2cf9..7eca4ae4c66 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -166,6 +166,7 @@ CHAR_CONTACT_SENSOR_STATE = "ContactSensorState" CHAR_COOLING_THRESHOLD_TEMPERATURE = "CoolingThresholdTemperature" CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel" CHAR_CURRENT_DOOR_STATE = "CurrentDoorState" +CHAR_CURRENT_FAN_STATE = "CurrentFanState" CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState" CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER = "CurrentHumidifierDehumidifierState" CHAR_CURRENT_POSITION = "CurrentPosition" @@ -176,6 +177,7 @@ CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle" CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState" CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityDehumidifierThreshold" CHAR_FIRMWARE_REVISION = "FirmwareRevision" +CHAR_HARDWARE_REVISION = "HardwareRevision" CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature" CHAR_HUE = "Hue" CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityHumidifierThreshold" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2906bf97e7e..945578dab3b 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -92,6 +92,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) + +NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") +VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") + + MAX_PORT = 65535 VALID_VIDEO_CODECS = [VIDEO_CODEC_LIBX264, VIDEO_CODEC_H264_OMX, AUDIO_CODEC_COPY] VALID_AUDIO_CODECS = [AUDIO_CODEC_OPUS, VIDEO_CODEC_COPY] @@ -412,9 +417,11 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): ) -def format_sw_version(version): +def format_version(version): """Extract the version string in a format homekit can consume.""" - match = re.search(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?", str(version).replace("-", ".")) + split_ver = str(version).replace("-", ".") + num_only = NUMBERS_ONLY_RE.sub("", split_ver) + match = VERSION_RE.search(num_only) if match: return match.group(0) return None diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index b47ab223be8..103ee9ea2da 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -19,6 +19,7 @@ from homeassistant.components.homekit.const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_HARDWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, @@ -33,6 +34,7 @@ from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, + ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SERVICE, @@ -215,6 +217,36 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert isinstance(acc.to_HAP(), dict) + + +async def test_accessory_with_hardware_revision(hass, hk_driver): + """Test HomeAccessory class with hardware revision.""" + entity_id = "sensor.accessory" + hass.states.async_set(entity_id, "on") + acc = HomeAccessory( + hass, + hk_driver, + "Home Accessory", + entity_id, + 3, + { + ATTR_MODEL: None, + ATTR_MANUFACTURER: None, + ATTR_SW_VERSION: None, + ATTR_HW_VERSION: "1.2.3", + ATTR_INTEGRATION: None, + }, + ) + acc.driver = hk_driver + serv = acc.get_service(SERV_ACCESSORY_INFO) + assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" + assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" + assert isinstance(acc.to_HAP(), dict) async def test_battery_service(hass, hk_driver, caplog): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 24b4cf5b373..0ffb4440671 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1104,6 +1104,7 @@ async def test_homekit_finds_linked_batteries( device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, sw_version="0.16.0", + hw_version="2.34", model="Powerwall 2", manufacturer="Tesla", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, @@ -1152,6 +1153,7 @@ async def test_homekit_finds_linked_batteries( "manufacturer": "Tesla", "model": "Powerwall 2", "sw_version": "0.16.0", + "hw_version": "2.34", "platform": "test", "linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging", "linked_battery_sensor": "sensor.powerwall_battery", diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 31efcc0b948..0432fb27426 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -32,7 +32,7 @@ from homeassistant.components.homekit.util import ( cleanup_name_for_homekit, convert_to_float, density_to_air_quality, - format_sw_version, + format_version, state_needs_accessory_mode, temperature_to_homekit, temperature_to_states, @@ -343,13 +343,17 @@ async def test_port_is_available_skips_existing_entries(hass): async_find_next_available_port(hass, 65530) -async def test_format_sw_version(): - """Test format_sw_version method.""" - assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8" - assert format_sw_version("undefined-undefined-1.6.8") == "1.6.8" - assert format_sw_version("56.0-76060") == "56.0.76060" - assert format_sw_version(3.6) == "3.6" - assert format_sw_version("unknown") is None +async def test_format_version(): + """Test format_version method.""" + assert format_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8" + assert format_version("undefined-undefined-1.6.8") == "1.6.8" + assert format_version("56.0-76060") == "56.0.76060" + assert format_version(3.6) == "3.6" + assert format_version("AK001-ZJ100") == "001.100" + assert format_version("HF-LPB100-") == "100" + assert format_version("AK001-ZJ2149") == "001.2149" + assert format_version("0.1") == "0.1" + assert format_version("unknown") is None async def test_accessory_friendly_name():