diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d348b4c1f42..4129c3225b7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -274,7 +274,7 @@ class HomeAccessory(Accessory): if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: - sw_version = __version__ + sw_version = format_version(__version__) hw_version = None if self.config.get(ATTR_HW_VERSION) is not None: hw_version = format_version(self.config[ATTR_HW_VERSION]) @@ -289,7 +289,9 @@ class HomeAccessory(Accessory): 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) + serv_info.configure_char( + CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH] + ) self.iid_manager.assign(char) char.broker = self @@ -532,7 +534,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( - firmware_revision=__version__, + firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8c64b9b0443..7fa4ffa8bf6 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") +MAX_VERSION_PART = 2**32 - 1 MAX_PORT = 65535 @@ -363,7 +364,15 @@ def convert_to_float(state): return None -def cleanup_name_for_homekit(name: str | None) -> str | None: +def coerce_int(state: str) -> int: + """Return int.""" + try: + return int(state) + except (ValueError, TypeError): + return 0 + + +def cleanup_name_for_homekit(name: str | None) -> str: """Ensure the name of the device will not crash homekit.""" # # This is not a security measure. @@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None: # UNICODE_EMOJI is also not allowed but that # likely isn't a problem if name is None: - return None + return "None" # None crashes apple watches return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] @@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): ) +def _format_version_part(version_part: str) -> str: + return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) + + def format_version(version): """Extract the version string in a format homekit can consume.""" - split_ver = str(version).replace("-", ".") + split_ver = str(version).replace("-", ".").replace(" ", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) - if match := VERSION_RE.search(num_only): - return match.group(0) - return None + if (match := VERSION_RE.search(num_only)) is None: + return None + value = ".".join(map(_format_version_part, match.group(0).split("."))) + return None if _is_zero_but_true(value) else value + + +def _is_zero_but_true(value): + """Zero but true values can crash apple watches.""" + return convert_to_float(value) == 0 def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 103ee9ea2da..704bb368d64 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -42,7 +42,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - __version__, __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): 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 hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert isinstance(acc.to_HAP(), dict) @@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): 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 hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" assert isinstance(acc.to_HAP(), dict) @@ -687,7 +692,9 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index d864a90fe61..9b6d1c9cee2 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver): assert acc.category == 10 # Sensor assert acc.char_humidity.value == 20 - assert acc.display_name is None + assert acc.display_name == "None" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0432fb27426..3dd30af2056 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import ( async_port_is_available, async_show_setup_message, cleanup_name_for_homekit, + coerce_int, convert_to_float, density_to_air_quality, format_version, @@ -349,13 +350,23 @@ async def test_format_version(): 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("AK001-ZJ100") == "1.100" assert format_version("HF-LPB100-") == "100" - assert format_version("AK001-ZJ2149") == "001.2149" + assert format_version("AK001-ZJ2149") == "1.2149" + assert format_version("13216407885") == "4294967295" # max value + assert format_version("000132 16407885") == "132.16407885" assert format_version("0.1") == "0.1" + assert format_version("0") is None assert format_version("unknown") is None +async def test_coerce_int(): + """Test coerce_int method.""" + assert coerce_int("1") == 1 + assert coerce_int("") == 0 + assert coerce_int(0) == 0 + + async def test_accessory_friendly_name(): """Test we provide a helpful friendly name."""