Add guards for HomeKit version/names that break apple watches (#67585)

This commit is contained in:
J. Nick Koston 2022-03-03 13:03:46 -10:00 committed by GitHub
parent e7ca6b6e38
commit 6d2302b703
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 17 deletions

View file

@ -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,

View file

@ -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):

View file

@ -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

View file

@ -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"

View file

@ -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."""