mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Add guards for HomeKit version/names that break apple watches (#67585)
This commit is contained in:
parent
e7ca6b6e38
commit
6d2302b703
@ -274,7 +274,7 @@ class HomeAccessory(Accessory):
|
|||||||
if self.config.get(ATTR_SW_VERSION) is not None:
|
if self.config.get(ATTR_SW_VERSION) is not None:
|
||||||
sw_version = format_version(self.config[ATTR_SW_VERSION])
|
sw_version = format_version(self.config[ATTR_SW_VERSION])
|
||||||
if sw_version is None:
|
if sw_version is None:
|
||||||
sw_version = __version__
|
sw_version = format_version(__version__)
|
||||||
hw_version = None
|
hw_version = None
|
||||||
if self.config.get(ATTR_HW_VERSION) is not None:
|
if self.config.get(ATTR_HW_VERSION) is not None:
|
||||||
hw_version = format_version(self.config[ATTR_HW_VERSION])
|
hw_version = format_version(self.config[ATTR_HW_VERSION])
|
||||||
@ -289,7 +289,9 @@ class HomeAccessory(Accessory):
|
|||||||
serv_info = self.get_service(SERV_ACCESSORY_INFO)
|
serv_info = self.get_service(SERV_ACCESSORY_INFO)
|
||||||
char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
|
char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
|
||||||
serv_info.add_characteristic(char)
|
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)
|
self.iid_manager.assign(char)
|
||||||
char.broker = self
|
char.broker = self
|
||||||
|
|
||||||
@ -532,7 +534,7 @@ class HomeBridge(Bridge):
|
|||||||
"""Initialize a Bridge object."""
|
"""Initialize a Bridge object."""
|
||||||
super().__init__(driver, name)
|
super().__init__(driver, name)
|
||||||
self.set_info_service(
|
self.set_info_service(
|
||||||
firmware_revision=__version__,
|
firmware_revision=format_version(__version__),
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=BRIDGE_MODEL,
|
model=BRIDGE_MODEL,
|
||||||
serial_number=BRIDGE_SERIAL_NUMBER,
|
serial_number=BRIDGE_SERIAL_NUMBER,
|
||||||
|
@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
|
NUMBERS_ONLY_RE = re.compile(r"[^\d.]+")
|
||||||
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
|
VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?")
|
||||||
|
MAX_VERSION_PART = 2**32 - 1
|
||||||
|
|
||||||
|
|
||||||
MAX_PORT = 65535
|
MAX_PORT = 65535
|
||||||
@ -363,7 +364,15 @@ def convert_to_float(state):
|
|||||||
return None
|
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."""
|
"""Ensure the name of the device will not crash homekit."""
|
||||||
#
|
#
|
||||||
# This is not a security measure.
|
# 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
|
# UNICODE_EMOJI is also not allowed but that
|
||||||
# likely isn't a problem
|
# likely isn't a problem
|
||||||
if name is None:
|
if name is None:
|
||||||
return None
|
return "None" # None crashes apple watches
|
||||||
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH]
|
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):
|
def format_version(version):
|
||||||
"""Extract the version string in a format homekit can consume."""
|
"""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)
|
num_only = NUMBERS_ONLY_RE.sub("", split_ver)
|
||||||
if match := VERSION_RE.search(num_only):
|
if (match := VERSION_RE.search(num_only)) is None:
|
||||||
return match.group(0)
|
|
||||||
return 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):
|
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):
|
||||||
|
@ -42,7 +42,6 @@ from homeassistant.const import (
|
|||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
__version__,
|
|
||||||
__version__ as hass_version,
|
__version__ as hass_version,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS
|
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
|
serv.get_characteristic(CHAR_SERIAL_NUMBER).value
|
||||||
== "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum"
|
== "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")
|
hass.states.async_set(entity_id, "on")
|
||||||
await hass.async_block_till_done()
|
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_MANUFACTURER).value == "Home Assistant Sensor"
|
||||||
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
||||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
|
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)
|
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_MANUFACTURER).value == "Home Assistant Sensor"
|
||||||
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
||||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id
|
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 serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3"
|
||||||
assert isinstance(acc.to_HAP(), dict)
|
assert isinstance(acc.to_HAP(), dict)
|
||||||
|
|
||||||
@ -687,7 +692,9 @@ def test_home_bridge(hk_driver):
|
|||||||
serv = bridge.services[0] # SERV_ACCESSORY_INFO
|
serv = bridge.services[0] # SERV_ACCESSORY_INFO
|
||||||
assert serv.display_name == SERV_ACCESSORY_INFO
|
assert serv.display_name == SERV_ACCESSORY_INFO
|
||||||
assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME
|
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_MANUFACTURER).value == MANUFACTURER
|
||||||
assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL
|
assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL
|
||||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER
|
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER
|
||||||
|
@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver):
|
|||||||
assert acc.category == 10 # Sensor
|
assert acc.category == 10 # Sensor
|
||||||
|
|
||||||
assert acc.char_humidity.value == 20
|
assert acc.char_humidity.value == 20
|
||||||
assert acc.display_name is None
|
assert acc.display_name == "None"
|
||||||
|
@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import (
|
|||||||
async_port_is_available,
|
async_port_is_available,
|
||||||
async_show_setup_message,
|
async_show_setup_message,
|
||||||
cleanup_name_for_homekit,
|
cleanup_name_for_homekit,
|
||||||
|
coerce_int,
|
||||||
convert_to_float,
|
convert_to_float,
|
||||||
density_to_air_quality,
|
density_to_air_quality,
|
||||||
format_version,
|
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("undefined-undefined-1.6.8") == "1.6.8"
|
||||||
assert format_version("56.0-76060") == "56.0.76060"
|
assert format_version("56.0-76060") == "56.0.76060"
|
||||||
assert format_version(3.6) == "3.6"
|
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("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.1") == "0.1"
|
||||||
|
assert format_version("0") is None
|
||||||
assert format_version("unknown") 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():
|
async def test_accessory_friendly_name():
|
||||||
"""Test we provide a helpful friendly name."""
|
"""Test we provide a helpful friendly name."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user