Merge pull request #67588 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-03 18:49:44 -08:00
committed by GitHub
24 changed files with 299 additions and 97 deletions

View File

@@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except FRITZ_EXCEPTIONS as ex: except FRITZ_EXCEPTIONS as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
if (
"X_AVM-DE_UPnP1" in avm_wrapper.connection.services
and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
raise ConfigEntryAuthFailed("Missing UPnP configuration")
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = avm_wrapper hass.data[DOMAIN][entry.entry_id] = avm_wrapper

View File

@@ -630,6 +630,11 @@ class AvmWrapper(FritzBoxTools):
) )
return {} return {}
async def async_get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return await self.hass.async_add_executor_job(self.get_upnp_configuration)
async def async_get_wan_link_properties(self) -> dict[str, Any]: async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service.""" """Call WANCommonInterfaceConfig service."""
@@ -698,6 +703,11 @@ class AvmWrapper(FritzBoxTools):
partial(self.set_allow_wan_access, ip_address, turn_on) partial(self.set_allow_wan_access, ip_address, turn_on)
) )
def get_upnp_configuration(self) -> dict[str, Any]:
"""Call X_AVM-DE_UPnP service."""
return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo")
def get_ontel_num_deflections(self) -> dict[str, Any]: def get_ontel_num_deflections(self) -> dict[str, Any]:
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""

View File

@@ -29,6 +29,7 @@ from .const import (
ERROR_AUTH_INVALID, ERROR_AUTH_INVALID,
ERROR_CANNOT_CONNECT, ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN, ERROR_UNKNOWN,
ERROR_UPNP_NOT_CONFIGURED,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
return ERROR_UNKNOWN return ERROR_UNKNOWN
if (
"X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services
and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"]
):
return ERROR_UPNP_NOT_CONFIGURED
return None return None
async def async_check_configured_entry(self) -> ConfigEntry | None: async def async_check_configured_entry(self) -> ConfigEntry | None:

View File

@@ -46,6 +46,7 @@ DEFAULT_USERNAME = ""
ERROR_AUTH_INVALID = "invalid_auth" ERROR_AUTH_INVALID = "invalid_auth"
ERROR_CANNOT_CONNECT = "cannot_connect" ERROR_CANNOT_CONNECT = "cannot_connect"
ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured"
ERROR_UNKNOWN = "unknown_error" ERROR_UNKNOWN = "unknown_error"
FRITZ_SERVICES = "fritz_services" FRITZ_SERVICES = "fritz_services"

View File

@@ -36,6 +36,7 @@
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"upnp_not_configured": "Missing UPnP settings on device.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"

View File

@@ -9,7 +9,8 @@
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress", "already_in_progress": "Configuration flow is already in progress",
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication" "invalid_auth": "Invalid authentication",
"upnp_not_configured": "Missing UPnP settings on device."
}, },
"flow_title": "{name}", "flow_title": "{name}",
"step": { "step": {

View File

@@ -221,12 +221,9 @@ class GrowattData:
# Create datetime from the latest entry # Create datetime from the latest entry
date_now = dt.now().date() date_now = dt.now().date()
last_updated_time = dt.parse_time(str(sorted_keys[-1])) last_updated_time = dt.parse_time(str(sorted_keys[-1]))
combined_timestamp = datetime.datetime.combine( mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now, last_updated_time date_now, last_updated_time
) )
# Convert datetime to UTC
combined_timestamp_utc = dt.as_utc(combined_timestamp)
mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat()
# Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined
# imported from grid value that is the combination of charging AND load consumption # imported from grid value that is the combination of charging AND load consumption

View File

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

View File

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

View File

@@ -75,11 +75,16 @@ from .const import (
ATTR_TOPIC, ATTR_TOPIC,
CONF_BIRTH_MESSAGE, CONF_BIRTH_MESSAGE,
CONF_BROKER, CONF_BROKER,
CONF_CERTIFICATE,
CONF_CLIENT_CERT,
CONF_CLIENT_KEY,
CONF_COMMAND_TOPIC, CONF_COMMAND_TOPIC,
CONF_ENCODING, CONF_ENCODING,
CONF_QOS, CONF_QOS,
CONF_RETAIN, CONF_RETAIN,
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
CONF_TLS_INSECURE,
CONF_TLS_VERSION,
CONF_TOPIC, CONF_TOPIC,
CONF_WILL_MESSAGE, CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG, DATA_MQTT_CONFIG,
@@ -94,6 +99,7 @@ from .const import (
DOMAIN, DOMAIN,
MQTT_CONNECTED, MQTT_CONNECTED,
MQTT_DISCONNECTED, MQTT_DISCONNECTED,
PROTOCOL_31,
PROTOCOL_311, PROTOCOL_311,
) )
from .discovery import LAST_DISCOVERY from .discovery import LAST_DISCOVERY
@@ -118,13 +124,6 @@ SERVICE_DUMP = "dump"
CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_KEEPALIVE = "keepalive" CONF_KEEPALIVE = "keepalive"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
PROTOCOL_31 = "3.1"
DEFAULT_PORT = 1883 DEFAULT_PORT = 1883
DEFAULT_KEEPALIVE = 60 DEFAULT_KEEPALIVE = 60
@@ -757,6 +756,58 @@ class Subscription:
encoding: str | None = attr.ib(default="utf-8") encoding: str | None = attr.ib(default="utf-8")
class MqttClientSetup:
"""Helper class to setup the paho mqtt client from config."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
def __init__(self, config: ConfigType) -> None:
"""Initialize the MQTT client setup helper."""
if config[CONF_PROTOCOL] == PROTOCOL_31:
proto = self.mqtt.MQTTv31
else:
proto = self.mqtt.MQTTv311
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = self.mqtt.base62(uuid.uuid4().int, padding=22)
self._client = self.mqtt.Client(client_id, protocol=proto)
# Enable logging
self._client.enable_logger()
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is not None:
self._client.username_pw_set(username, password)
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = config.get(CONF_CLIENT_KEY)
client_cert = config.get(CONF_CLIENT_CERT)
tls_insecure = config.get(CONF_TLS_INSECURE)
if certificate is not None:
self._client.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._client.tls_insecure_set(tls_insecure)
@property
def client(self) -> mqtt.Client:
"""Return the paho MQTT client."""
return self._client
class MQTT: class MQTT:
"""Home Assistant MQTT client.""" """Home Assistant MQTT client."""
@@ -821,46 +872,7 @@ class MQTT:
def init_client(self): def init_client(self):
"""Initialize paho client.""" """Initialize paho client."""
# We don't import on the top because some integrations self._mqttc = MqttClientSetup(self.conf).client
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
if self.conf[CONF_PROTOCOL] == PROTOCOL_31:
proto: int = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
if (client_id := self.conf.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
self._mqttc = mqtt.Client(client_id, protocol=proto)
# Enable logging
self._mqttc.enable_logger()
username = self.conf.get(CONF_USERNAME)
password = self.conf.get(CONF_PASSWORD)
if username is not None:
self._mqttc.username_pw_set(username, password)
if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto":
certificate = certifi.where()
client_key = self.conf.get(CONF_CLIENT_KEY)
client_cert = self.conf.get(CONF_CLIENT_CERT)
tls_insecure = self.conf.get(CONF_TLS_INSECURE)
if certificate is not None:
self._mqttc.tls_set(
certificate,
certfile=client_cert,
keyfile=client_key,
tls_version=ssl.PROTOCOL_TLS,
)
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)
self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_disconnect = self._mqtt_on_disconnect
self._mqttc.on_message = self._mqtt_on_message self._mqttc.on_message = self._mqtt_on_message

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
) )
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from . import MqttClientSetup
from .const import ( from .const import (
ATTR_PAYLOAD, ATTR_PAYLOAD,
ATTR_QOS, ATTR_QOS,
@@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
can_connect = await self.hass.async_add_executor_job( can_connect = await self.hass.async_add_executor_job(
try_connection, try_connection,
self.hass,
user_input[CONF_BROKER], user_input[CONF_BROKER],
user_input[CONF_PORT], user_input[CONF_PORT],
user_input.get(CONF_USERNAME), user_input.get(CONF_USERNAME),
@@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data = self._hassio_discovery data = self._hassio_discovery
can_connect = await self.hass.async_add_executor_job( can_connect = await self.hass.async_add_executor_job(
try_connection, try_connection,
self.hass,
data[CONF_HOST], data[CONF_HOST],
data[CONF_PORT], data[CONF_PORT],
data.get(CONF_USERNAME), data.get(CONF_USERNAME),
@@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
if user_input is not None: if user_input is not None:
can_connect = await self.hass.async_add_executor_job( can_connect = await self.hass.async_add_executor_job(
try_connection, try_connection,
self.hass,
user_input[CONF_BROKER], user_input[CONF_BROKER],
user_input[CONF_PORT], user_input[CONF_PORT],
user_input.get(CONF_USERNAME), user_input.get(CONF_USERNAME),
@@ -313,25 +317,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
) )
def try_connection(broker, port, username, password, protocol="3.1"): def try_connection(hass, broker, port, username, password, protocol="3.1"):
"""Test if we can connect to an MQTT broker.""" """Test if we can connect to an MQTT broker."""
# pylint: disable-next=import-outside-toplevel # Get the config from configuration.yaml
import paho.mqtt.client as mqtt yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
entry_config = {
if protocol == "3.1": CONF_BROKER: broker,
proto = mqtt.MQTTv31 CONF_PORT: port,
else: CONF_USERNAME: username,
proto = mqtt.MQTTv311 CONF_PASSWORD: password,
CONF_PROTOCOL: protocol,
client = mqtt.Client(protocol=proto) }
if username and password: client = MqttClientSetup({**yaml_config, **entry_config}).client
client.username_pw_set(username, password)
result = queue.Queue(maxsize=1) result = queue.Queue(maxsize=1)
def on_connect(client_, userdata, flags, result_code): def on_connect(client_, userdata, flags, result_code):
"""Handle connection result.""" """Handle connection result."""
result.put(result_code == mqtt.CONNACK_ACCEPTED) result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED)
client.on_connect = on_connect client.on_connect = on_connect

View File

@@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template"
CONF_TOPIC = "topic" CONF_TOPIC = "topic"
CONF_WILL_MESSAGE = "will_message" CONF_WILL_MESSAGE = "will_message"
CONF_CERTIFICATE = "certificate"
CONF_CLIENT_KEY = "client_key"
CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_CONFIG = "mqtt_config"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
@@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None" PAYLOAD_NONE = "None"
PROTOCOL_31 = "3.1"
PROTOCOL_311 = "3.1.1" PROTOCOL_311 = "3.1.1"

View File

@@ -2,7 +2,7 @@
"domain": "obihai", "domain": "obihai",
"name": "Obihai", "name": "Obihai",
"documentation": "https://www.home-assistant.io/integrations/obihai", "documentation": "https://www.home-assistant.io/integrations/obihai",
"requirements": ["pyobihai==1.3.1"], "requirements": ["pyobihai==1.3.2"],
"codeowners": ["@dshokouhi"], "codeowners": ["@dshokouhi"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyobihai"] "loggers": ["pyobihai"]

View File

@@ -3,7 +3,7 @@
"name": "Sonos", "name": "Sonos",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.3"], "requirements": ["soco==0.26.4"],
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."], "zeroconf": ["_sonos._tcp.local."],

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -149,10 +149,17 @@ async def _async_setup_component(
This method is a coroutine. This method is a coroutine.
""" """
integration: loader.Integration | None = None
def log_error(msg: str, link: str | None = None) -> None: def log_error(msg: str) -> None:
"""Log helper.""" """Log helper."""
_LOGGER.error("Setup failed for %s: %s", domain, msg) if integration is None:
custom = ""
link = None
else:
custom = "" if integration.is_built_in else "custom integration "
link = integration.documentation
_LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg)
async_notify_setup_error(hass, domain, link) async_notify_setup_error(hass, domain, link)
try: try:
@@ -174,7 +181,7 @@ async def _async_setup_component(
try: try:
await async_process_deps_reqs(hass, config, integration) await async_process_deps_reqs(hass, config, integration)
except HomeAssistantError as err: except HomeAssistantError as err:
log_error(str(err), integration.documentation) log_error(str(err))
return False return False
# Some integrations fail on import because they call functions incorrectly. # Some integrations fail on import because they call functions incorrectly.
@@ -182,7 +189,7 @@ async def _async_setup_component(
try: try:
component = integration.get_component() component = integration.get_component()
except ImportError as err: except ImportError as err:
log_error(f"Unable to import component: {err}", integration.documentation) log_error(f"Unable to import component: {err}")
return False return False
processed_config = await conf_util.async_process_component_config( processed_config = await conf_util.async_process_component_config(
@@ -190,7 +197,7 @@ async def _async_setup_component(
) )
if processed_config is None: if processed_config is None:
log_error("Invalid config.", integration.documentation) log_error("Invalid config.")
return False return False
start = timer() start = timer()
@@ -287,6 +294,7 @@ async def async_prepare_setup_platform(
def log_error(msg: str) -> None: def log_error(msg: str) -> None:
"""Log helper.""" """Log helper."""
_LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg)
async_notify_setup_error(hass, platform_path) async_notify_setup_error(hass, platform_path)

View File

@@ -1727,7 +1727,7 @@ pynx584==0.5
pynzbgetapi==0.2.0 pynzbgetapi==0.2.0
# homeassistant.components.obihai # homeassistant.components.obihai
pyobihai==1.3.1 pyobihai==1.3.2
# homeassistant.components.octoprint # homeassistant.components.octoprint
pyoctoprintapi==0.1.7 pyoctoprintapi==0.1.7
@@ -2235,7 +2235,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3 snapcast==2.1.3
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.26.3 soco==0.26.4
# homeassistant.components.solaredge_local # homeassistant.components.solaredge_local
solaredge-local==0.2.0 solaredge-local==0.2.0

View File

@@ -1371,7 +1371,7 @@ smarthab==0.21
smhi-pkg==1.0.15 smhi-pkg==1.0.15
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.26.3 soco==0.26.4
# homeassistant.components.solaredge # homeassistant.components.solaredge
solaredge==0.0.2 solaredge==0.0.2

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.3.0 version = 2022.3.1
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,9 @@ from unittest.mock import patch
import pytest import pytest
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant import config_entries, data_entry_flow from homeassistant import config as hass_config, config_entries, data_entry_flow
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -151,7 +152,7 @@ async def test_manual_config_set(
"discovery": True, "discovery": True,
} }
# Check we tried the connection, with precedence for config entry settings # Check we tried the connection, with precedence for config entry settings
mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None) mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None)
# Check config entry got setup # Check config entry got setup
assert len(mock_finish_setup.mock_calls) == 1 assert len(mock_finish_setup.mock_calls) == 1
@@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
mqtt.CONF_BROKER: "test-broker", mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234, mqtt.CONF_PORT: 1234,
} }
async def test_try_connection_with_advanced_parameters(
hass, mock_try_connection_success, tmp_path
):
"""Test config flow with advanced parameters from config."""
# Mock certificate files
certfile = tmp_path / "cert.pem"
certfile.write_text("## mock certificate file ##")
keyfile = tmp_path / "key.pem"
keyfile.write_text("## mock key file ##")
config = {
"certificate": "auto",
"tls_insecure": True,
"client_cert": certfile,
"client_key": keyfile,
}
new_yaml_config_file = tmp_path / "configuration.yaml"
new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
new_yaml_config_file.write_text(new_yaml_config)
assert new_yaml_config_file.read_text() == new_yaml_config
with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
await hass.async_block_till_done()
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass)
config_entry.data = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
mqtt.CONF_DISCOVERY: True,
mqtt.CONF_BIRTH_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/online",
mqtt.ATTR_PAYLOAD: "online",
mqtt.ATTR_QOS: 1,
mqtt.ATTR_RETAIN: True,
},
mqtt.CONF_WILL_MESSAGE: {
mqtt.ATTR_TOPIC: "ha_state/offline",
mqtt.ATTR_PAYLOAD: "offline",
mqtt.ATTR_QOS: 2,
mqtt.ATTR_RETAIN: False,
},
}
# Test default/suggested values from config
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "broker"
defaults = {
mqtt.CONF_BROKER: "test-broker",
mqtt.CONF_PORT: 1234,
}
suggested = {
mqtt.CONF_USERNAME: "user",
mqtt.CONF_PASSWORD: "pass",
}
for k, v in defaults.items():
assert get_default(result["data_schema"].schema, k) == v
for k, v in suggested.items():
assert get_suggested(result["data_schema"].schema, k) == v
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
mqtt.CONF_BROKER: "another-broker",
mqtt.CONF_PORT: 2345,
mqtt.CONF_USERNAME: "us3r",
mqtt.CONF_PASSWORD: "p4ss",
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "options"
# check if the username and password was set from config flow and not from configuration.yaml
assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
"us3r",
"p4ss",
)
# check if tls_insecure_set is called
assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
# check if the certificate settings were set from configuration.yaml
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"certfile"
] == str(certfile)
assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
"keyfile"
] == str(keyfile)

View File

@@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries, setup from homeassistant import config_entries, setup
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.config_validation import ( from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
@@ -621,6 +622,22 @@ async def test_integration_disabled(hass, caplog):
assert disabled_reason in caplog.text assert disabled_reason in caplog.text
async def test_integration_logs_is_custom(hass, caplog):
"""Test we highlight it's a custom component when errors happen."""
mock_integration(
hass,
MockModule("test_component1"),
built_in=False,
)
with patch(
"homeassistant.setup.async_process_deps_reqs",
side_effect=HomeAssistantError("Boom"),
):
result = await setup.async_setup_component(hass, "test_component1", {})
assert not result
assert "Setup failed for custom integration test_component1: Boom" in caplog.text
async def test_async_get_loaded_integrations(hass): async def test_async_get_loaded_integrations(hass):
"""Test we can enumerate loaded integations.""" """Test we can enumerate loaded integations."""
hass.config.components.add("notbase") hass.config.components.add("notbase")