2018-03-15 01:48:21 +00:00
|
|
|
"""Collection of useful functions for the HomeKit component."""
|
2019-02-14 15:01:46 +00:00
|
|
|
from collections import OrderedDict, namedtuple
|
2020-04-21 22:38:43 +00:00
|
|
|
import io
|
2020-05-05 00:03:46 +00:00
|
|
|
import ipaddress
|
2018-03-15 01:48:21 +00:00
|
|
|
import logging
|
2020-05-01 04:05:06 +00:00
|
|
|
import os
|
2020-04-21 22:38:43 +00:00
|
|
|
import secrets
|
2020-05-01 04:05:06 +00:00
|
|
|
import socket
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2020-04-21 22:38:43 +00:00
|
|
|
import pyqrcode
|
2018-03-15 01:48:21 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-04-09 21:13:48 +00:00
|
|
|
from homeassistant.components import fan, media_player, sensor
|
2018-03-15 01:48:21 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
ATTR_CODE,
|
|
|
|
ATTR_SUPPORTED_FEATURES,
|
|
|
|
CONF_NAME,
|
|
|
|
CONF_TYPE,
|
|
|
|
TEMP_CELSIUS,
|
|
|
|
)
|
2020-05-01 04:05:06 +00:00
|
|
|
from homeassistant.core import HomeAssistant, split_entity_id
|
2018-03-15 01:48:21 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2020-05-01 04:05:06 +00:00
|
|
|
from homeassistant.helpers.storage import STORAGE_DIR
|
2018-03-27 09:31:18 +00:00
|
|
|
import homeassistant.util.temperature as temp_util
|
2019-02-05 15:11:19 +00:00
|
|
|
|
2018-05-25 09:37:20 +00:00
|
|
|
from .const import (
|
2020-05-05 00:03:46 +00:00
|
|
|
CONF_AUDIO_MAP,
|
|
|
|
CONF_AUDIO_PACKET_SIZE,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_FEATURE,
|
|
|
|
CONF_FEATURE_LIST,
|
|
|
|
CONF_LINKED_BATTERY_SENSOR,
|
|
|
|
CONF_LOW_BATTERY_THRESHOLD,
|
2020-05-05 00:03:46 +00:00
|
|
|
CONF_MAX_FPS,
|
|
|
|
CONF_MAX_HEIGHT,
|
|
|
|
CONF_MAX_WIDTH,
|
|
|
|
CONF_STREAM_ADDRESS,
|
|
|
|
CONF_STREAM_SOURCE,
|
|
|
|
CONF_SUPPORT_AUDIO,
|
|
|
|
CONF_VIDEO_MAP,
|
|
|
|
CONF_VIDEO_PACKET_SIZE,
|
|
|
|
DEFAULT_AUDIO_MAP,
|
|
|
|
DEFAULT_AUDIO_PACKET_SIZE,
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_LOW_BATTERY_THRESHOLD,
|
2020-05-05 00:03:46 +00:00
|
|
|
DEFAULT_MAX_FPS,
|
|
|
|
DEFAULT_MAX_HEIGHT,
|
|
|
|
DEFAULT_MAX_WIDTH,
|
|
|
|
DEFAULT_VIDEO_MAP,
|
|
|
|
DEFAULT_VIDEO_PACKET_SIZE,
|
2020-05-01 04:05:06 +00:00
|
|
|
DOMAIN,
|
2019-07-31 19:25:30 +00:00
|
|
|
FEATURE_ON_OFF,
|
|
|
|
FEATURE_PLAY_PAUSE,
|
|
|
|
FEATURE_PLAY_STOP,
|
|
|
|
FEATURE_TOGGLE_MUTE,
|
2020-05-01 04:05:06 +00:00
|
|
|
HOMEKIT_FILE,
|
2020-04-21 22:38:43 +00:00
|
|
|
HOMEKIT_PAIRING_QR,
|
|
|
|
HOMEKIT_PAIRING_QR_SECRET,
|
2019-07-31 19:25:30 +00:00
|
|
|
TYPE_FAUCET,
|
|
|
|
TYPE_OUTLET,
|
|
|
|
TYPE_SHOWER,
|
|
|
|
TYPE_SPRINKLER,
|
|
|
|
TYPE_SWITCH,
|
|
|
|
TYPE_VALVE,
|
|
|
|
)
|
2018-03-15 01:48:21 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
MAX_PORT = 65535
|
2018-05-28 14:26:33 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
BASIC_INFO_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_NAME): cv.string,
|
|
|
|
vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN),
|
|
|
|
vol.Optional(
|
|
|
|
CONF_LOW_BATTERY_THRESHOLD, default=DEFAULT_LOW_BATTERY_THRESHOLD
|
|
|
|
): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
|
|
|
{vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list}
|
|
|
|
)
|
|
|
|
|
2020-05-05 00:03:46 +00:00
|
|
|
CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_STREAM_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
|
|
|
|
vol.Optional(CONF_STREAM_SOURCE): cv.string,
|
|
|
|
vol.Optional(CONF_SUPPORT_AUDIO, default=False): cv.boolean,
|
|
|
|
vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int,
|
|
|
|
vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int,
|
|
|
|
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
|
|
|
|
vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string,
|
|
|
|
vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string,
|
|
|
|
vol.Optional(
|
|
|
|
CONF_AUDIO_PACKET_SIZE, default=DEFAULT_AUDIO_PACKET_SIZE
|
|
|
|
): cv.positive_int,
|
|
|
|
vol.Optional(
|
|
|
|
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
|
|
|
|
): cv.positive_int,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CODE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
|
|
|
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
|
|
|
|
)
|
|
|
|
|
|
|
|
MEDIA_PLAYER_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_FEATURE): vol.All(
|
|
|
|
cv.string,
|
|
|
|
vol.In(
|
|
|
|
(
|
|
|
|
FEATURE_ON_OFF,
|
|
|
|
FEATURE_PLAY_PAUSE,
|
|
|
|
FEATURE_PLAY_STOP,
|
|
|
|
FEATURE_TOGGLE_MUTE,
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All(
|
|
|
|
cv.string,
|
|
|
|
vol.In(
|
|
|
|
(
|
|
|
|
TYPE_FAUCET,
|
|
|
|
TYPE_OUTLET,
|
|
|
|
TYPE_SHOWER,
|
|
|
|
TYPE_SPRINKLER,
|
|
|
|
TYPE_SWITCH,
|
|
|
|
TYPE_VALVE,
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2018-06-01 16:04:54 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2020-05-02 21:15:44 +00:00
|
|
|
HOMEKIT_CHAR_TRANSLATIONS = {
|
|
|
|
0: " ", # nul
|
|
|
|
10: " ", # nl
|
|
|
|
13: " ", # cr
|
|
|
|
33: "-", # !
|
|
|
|
34: " ", # "
|
|
|
|
36: "-", # $
|
|
|
|
37: "-", # %
|
|
|
|
40: "-", # (
|
|
|
|
41: "-", # )
|
|
|
|
42: "-", # *
|
|
|
|
43: "-", # +
|
|
|
|
47: "-", # /
|
|
|
|
58: "-", # :
|
|
|
|
59: "-", # ;
|
|
|
|
60: "-", # <
|
|
|
|
61: "-", # =
|
|
|
|
62: "-", # >
|
|
|
|
63: "-", # ?
|
|
|
|
64: "-", # @
|
|
|
|
91: "-", # [
|
|
|
|
92: "-", # \
|
|
|
|
93: "-", # ]
|
|
|
|
94: "-", # ^
|
|
|
|
95: " ", # _
|
|
|
|
96: "-", # `
|
|
|
|
123: "-", # {
|
|
|
|
124: "-", # |
|
|
|
|
125: "-", # }
|
|
|
|
126: "-", # ~
|
|
|
|
127: "-", # del
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
def validate_entity_config(values):
|
|
|
|
"""Validate config entry for CONF_ENTITY."""
|
2018-10-04 18:37:04 +00:00
|
|
|
if not isinstance(values, dict):
|
2019-07-31 19:25:30 +00:00
|
|
|
raise vol.Invalid("expected a dictionary")
|
2018-10-04 18:37:04 +00:00
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
entities = {}
|
2018-05-11 12:22:45 +00:00
|
|
|
for entity_id, config in values.items():
|
|
|
|
entity = cv.entity_id(entity_id)
|
2018-05-28 14:26:33 +00:00
|
|
|
domain, _ = split_entity_id(entity)
|
|
|
|
|
2018-03-15 01:48:21 +00:00
|
|
|
if not isinstance(config, dict):
|
2020-02-28 11:39:29 +00:00
|
|
|
raise vol.Invalid(f"The configuration for {entity} must be a dictionary.")
|
2018-03-15 01:48:21 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if domain in ("alarm_control_panel", "lock"):
|
2018-05-28 14:26:33 +00:00
|
|
|
config = CODE_SCHEMA(config)
|
|
|
|
|
2019-02-08 22:18:18 +00:00
|
|
|
elif domain == media_player.const.DOMAIN:
|
2018-05-28 14:26:33 +00:00
|
|
|
config = FEATURE_SCHEMA(config)
|
|
|
|
feature_list = {}
|
|
|
|
for feature in config[CONF_FEATURE_LIST]:
|
|
|
|
params = MEDIA_PLAYER_SCHEMA(feature)
|
|
|
|
key = params.pop(CONF_FEATURE)
|
|
|
|
if key in feature_list:
|
2019-09-03 15:27:14 +00:00
|
|
|
raise vol.Invalid(f"A feature can be added only once for {entity}")
|
2018-05-28 14:26:33 +00:00
|
|
|
feature_list[key] = params
|
|
|
|
config[CONF_FEATURE_LIST] = feature_list
|
|
|
|
|
2020-05-05 00:03:46 +00:00
|
|
|
elif domain == "camera":
|
|
|
|
config = CAMERA_SCHEMA(config)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
elif domain == "switch":
|
2018-06-01 16:04:54 +00:00
|
|
|
config = SWITCH_TYPE_SCHEMA(config)
|
|
|
|
|
2018-05-28 14:26:33 +00:00
|
|
|
else:
|
|
|
|
config = BASIC_INFO_SCHEMA(config)
|
|
|
|
|
|
|
|
entities[entity] = config
|
2018-03-15 01:48:21 +00:00
|
|
|
return entities
|
|
|
|
|
|
|
|
|
2020-05-02 21:15:44 +00:00
|
|
|
def get_media_player_features(state):
|
|
|
|
"""Determine features for media players."""
|
2018-05-25 09:37:20 +00:00
|
|
|
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
|
|
|
|
supported_modes = []
|
2019-07-31 19:25:30 +00:00
|
|
|
if features & (
|
|
|
|
media_player.const.SUPPORT_TURN_ON | media_player.const.SUPPORT_TURN_OFF
|
|
|
|
):
|
2018-05-28 14:26:33 +00:00
|
|
|
supported_modes.append(FEATURE_ON_OFF)
|
2019-07-31 19:25:30 +00:00
|
|
|
if features & (media_player.const.SUPPORT_PLAY | media_player.const.SUPPORT_PAUSE):
|
2018-05-28 14:26:33 +00:00
|
|
|
supported_modes.append(FEATURE_PLAY_PAUSE)
|
2019-07-31 19:25:30 +00:00
|
|
|
if features & (media_player.const.SUPPORT_PLAY | media_player.const.SUPPORT_STOP):
|
2018-05-28 14:26:33 +00:00
|
|
|
supported_modes.append(FEATURE_PLAY_STOP)
|
2019-02-08 22:18:18 +00:00
|
|
|
if features & media_player.const.SUPPORT_VOLUME_MUTE:
|
2018-05-28 14:26:33 +00:00
|
|
|
supported_modes.append(FEATURE_TOGGLE_MUTE)
|
2020-05-02 21:15:44 +00:00
|
|
|
return supported_modes
|
|
|
|
|
|
|
|
|
|
|
|
def validate_media_player_features(state, feature_list):
|
|
|
|
"""Validate features for media players."""
|
|
|
|
supported_modes = get_media_player_features(state)
|
|
|
|
|
|
|
|
if not supported_modes:
|
|
|
|
_LOGGER.error("%s does not support any media_player features", state.entity_id)
|
|
|
|
return False
|
|
|
|
|
|
|
|
if not feature_list:
|
|
|
|
# Auto detected
|
|
|
|
return True
|
2018-05-28 14:26:33 +00:00
|
|
|
|
|
|
|
error_list = []
|
|
|
|
for feature in feature_list:
|
|
|
|
if feature not in supported_modes:
|
|
|
|
error_list.append(feature)
|
|
|
|
|
|
|
|
if error_list:
|
2020-05-02 21:15:44 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"%s does not support media_player features: %s", state.entity_id, error_list
|
|
|
|
)
|
2018-05-28 14:26:33 +00:00
|
|
|
return False
|
|
|
|
return True
|
2018-05-25 09:37:20 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SpeedRange = namedtuple("SpeedRange", ("start", "target"))
|
2019-02-05 15:11:19 +00:00
|
|
|
SpeedRange.__doc__ += """ Maps Home Assistant speed \
|
|
|
|
values to percentage based HomeKit speeds.
|
|
|
|
start: Start of the range (inclusive).
|
|
|
|
target: Percentage to use to determine HomeKit percentages \
|
|
|
|
from HomeAssistant speed.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
class HomeKitSpeedMapping:
|
|
|
|
"""Supports conversion between Home Assistant and HomeKit fan speeds."""
|
|
|
|
|
|
|
|
def __init__(self, speed_list):
|
|
|
|
"""Initialize a new SpeedMapping object."""
|
|
|
|
if speed_list[0] != fan.SPEED_OFF:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"%s does not contain the speed setting "
|
|
|
|
"%s as its first element. "
|
|
|
|
"Assuming that %s is equivalent to 'off'.",
|
|
|
|
speed_list,
|
|
|
|
fan.SPEED_OFF,
|
|
|
|
speed_list[0],
|
|
|
|
)
|
2019-02-05 15:11:19 +00:00
|
|
|
self.speed_ranges = OrderedDict()
|
|
|
|
list_size = len(speed_list)
|
|
|
|
for index, speed in enumerate(speed_list):
|
|
|
|
# By dividing by list_size -1 the following
|
|
|
|
# desired attributes hold true:
|
|
|
|
# * index = 0 => 0%, equal to "off"
|
|
|
|
# * index = len(speed_list) - 1 => 100 %
|
|
|
|
# * all other indices are equally distributed
|
|
|
|
target = index * 100 / (list_size - 1)
|
|
|
|
start = index * 100 / list_size
|
|
|
|
self.speed_ranges[speed] = SpeedRange(start, target)
|
|
|
|
|
|
|
|
def speed_to_homekit(self, speed):
|
|
|
|
"""Map Home Assistant speed state to HomeKit speed."""
|
2019-04-10 06:35:17 +00:00
|
|
|
if speed is None:
|
|
|
|
return None
|
2019-02-05 15:11:19 +00:00
|
|
|
speed_range = self.speed_ranges[speed]
|
2020-04-30 18:34:25 +00:00
|
|
|
return round(speed_range.target)
|
2019-02-05 15:11:19 +00:00
|
|
|
|
|
|
|
def speed_to_states(self, speed):
|
|
|
|
"""Map HomeKit speed to Home Assistant speed state."""
|
|
|
|
for state, speed_range in reversed(self.speed_ranges.items()):
|
|
|
|
if speed_range.start <= speed:
|
|
|
|
return state
|
|
|
|
return list(self.speed_ranges.keys())[0]
|
|
|
|
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
def show_setup_message(hass, entry_id, bridge_name, pincode, uri):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Display persistent notification with setup information."""
|
2018-05-18 14:32:57 +00:00
|
|
|
pin = pincode.decode()
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info("Pincode: %s", pin)
|
2020-04-21 22:38:43 +00:00
|
|
|
|
|
|
|
buffer = io.BytesIO()
|
|
|
|
url = pyqrcode.create(uri)
|
|
|
|
url.svg(buffer, scale=5)
|
|
|
|
pairing_secret = secrets.token_hex(32)
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR] = buffer.getvalue()
|
|
|
|
hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET] = pairing_secret
|
2020-04-21 22:38:43 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
message = (
|
2020-05-01 04:05:06 +00:00
|
|
|
f"To set up {bridge_name} in the Home App, "
|
2020-04-21 22:38:43 +00:00
|
|
|
f"scan the QR code or enter the following code:\n"
|
|
|
|
f"### {pin}\n"
|
2020-05-01 04:05:06 +00:00
|
|
|
f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-03-15 01:48:21 +00:00
|
|
|
hass.components.persistent_notification.create(
|
2020-05-01 04:05:06 +00:00
|
|
|
message, "HomeKit Bridge Setup", entry_id
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-03-15 01:48:21 +00:00
|
|
|
|
|
|
|
|
2020-05-01 04:05:06 +00:00
|
|
|
def dismiss_setup_message(hass, entry_id):
|
2018-03-15 01:48:21 +00:00
|
|
|
"""Dismiss persistent notification and remove QR code."""
|
2020-05-01 04:05:06 +00:00
|
|
|
hass.components.persistent_notification.dismiss(entry_id)
|
2018-03-27 09:31:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
def convert_to_float(state):
|
|
|
|
"""Return float of state, catch errors."""
|
|
|
|
try:
|
|
|
|
return float(state)
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2020-05-02 21:15:44 +00:00
|
|
|
def cleanup_name_for_homekit(name):
|
|
|
|
"""Ensure the name of the device will not crash homekit."""
|
|
|
|
#
|
|
|
|
# This is not a security measure.
|
|
|
|
#
|
|
|
|
# UNICODE_EMOJI is also not allowed but that
|
|
|
|
# likely isn't a problem
|
|
|
|
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)
|
|
|
|
|
|
|
|
|
2018-03-27 09:31:18 +00:00
|
|
|
def temperature_to_homekit(temperature, unit):
|
|
|
|
"""Convert temperature to Celsius for HomeKit."""
|
2019-10-04 00:44:07 +00:00
|
|
|
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
|
2018-03-27 09:31:18 +00:00
|
|
|
|
|
|
|
|
|
|
|
def temperature_to_states(temperature, unit):
|
|
|
|
"""Convert temperature back from Celsius to Home Assistant unit."""
|
2018-11-04 21:04:51 +00:00
|
|
|
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2
|
2018-04-12 13:01:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def density_to_air_quality(density):
|
|
|
|
"""Map PM2.5 density to HomeKit AirQuality level."""
|
|
|
|
if density <= 35:
|
|
|
|
return 1
|
2018-07-23 08:16:05 +00:00
|
|
|
if density <= 75:
|
2018-04-12 13:01:41 +00:00
|
|
|
return 2
|
2018-07-23 08:16:05 +00:00
|
|
|
if density <= 115:
|
2018-04-12 13:01:41 +00:00
|
|
|
return 3
|
2018-07-23 08:16:05 +00:00
|
|
|
if density <= 150:
|
2018-04-12 13:01:41 +00:00
|
|
|
return 4
|
|
|
|
return 5
|
2020-05-01 04:05:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_persist_filename_for_entry_id(entry_id: str):
|
|
|
|
"""Determine the filename of the homekit state file."""
|
|
|
|
return f"{DOMAIN}.{entry_id}.state"
|
|
|
|
|
|
|
|
|
|
|
|
def get_aid_storage_filename_for_entry_id(entry_id: str):
|
|
|
|
"""Determine the ilename of homekit aid storage file."""
|
|
|
|
return f"{DOMAIN}.{entry_id}.aids"
|
|
|
|
|
|
|
|
|
|
|
|
def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
|
|
|
|
"""Determine the path to the homekit state file."""
|
|
|
|
return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id))
|
|
|
|
|
|
|
|
|
|
|
|
def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str):
|
|
|
|
"""Determine the path to the homekit aid storage file."""
|
|
|
|
return hass.config.path(
|
|
|
|
STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def migrate_filesystem_state_data_for_primary_imported_entry_id(
|
|
|
|
hass: HomeAssistant, entry_id: str
|
|
|
|
):
|
|
|
|
"""Migrate the old paths to the storage directory."""
|
|
|
|
legacy_persist_file_path = hass.config.path(HOMEKIT_FILE)
|
|
|
|
if os.path.exists(legacy_persist_file_path):
|
|
|
|
os.rename(
|
|
|
|
legacy_persist_file_path, get_persist_fullpath_for_entry_id(hass, entry_id)
|
|
|
|
)
|
|
|
|
|
|
|
|
legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids")
|
|
|
|
if os.path.exists(legacy_aid_storage_path):
|
|
|
|
os.rename(
|
|
|
|
legacy_aid_storage_path,
|
|
|
|
get_aid_storage_fullpath_for_entry_id(hass, entry_id),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str):
|
|
|
|
"""Remove the state files from disk."""
|
|
|
|
persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id)
|
|
|
|
aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id)
|
|
|
|
os.unlink(persist_file_path)
|
|
|
|
if os.path.exists(aid_storage_path):
|
|
|
|
os.unlink(aid_storage_path)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _get_test_socket():
|
|
|
|
"""Create a socket to test binding ports."""
|
|
|
|
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
test_socket.setblocking(False)
|
|
|
|
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
|
return test_socket
|
|
|
|
|
|
|
|
|
|
|
|
def port_is_available(port: int):
|
|
|
|
"""Check to see if a port is available."""
|
|
|
|
test_socket = _get_test_socket()
|
|
|
|
try:
|
|
|
|
test_socket.bind(("", port))
|
|
|
|
except OSError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def find_next_available_port(start_port: int):
|
|
|
|
"""Find the next available port starting with the given port."""
|
|
|
|
test_socket = _get_test_socket()
|
|
|
|
for port in range(start_port, MAX_PORT):
|
|
|
|
try:
|
|
|
|
test_socket.bind(("", port))
|
|
|
|
return port
|
|
|
|
except OSError:
|
|
|
|
if port == MAX_PORT:
|
|
|
|
raise
|
|
|
|
continue
|