2020-06-23 00:49:01 +00:00
|
|
|
"""Utility functions for the MQTT integration."""
|
2022-07-12 09:07:18 +00:00
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2023-04-20 06:07:35 +00:00
|
|
|
import asyncio
|
2024-06-25 08:33:58 +00:00
|
|
|
from collections.abc import Callable, Coroutine
|
2024-05-22 01:11:27 +00:00
|
|
|
from functools import lru_cache
|
2024-05-24 11:11:52 +00:00
|
|
|
import logging
|
2022-10-24 07:58:23 +00:00
|
|
|
import os
|
|
|
|
from pathlib import Path
|
|
|
|
import tempfile
|
2020-06-23 00:49:01 +00:00
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2024-03-09 20:55:00 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
2024-05-24 11:11:52 +00:00
|
|
|
from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform
|
2024-06-25 08:33:58 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2021-02-08 09:50:38 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv, template
|
2022-10-24 07:58:23 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
2024-03-09 20:55:00 +00:00
|
|
|
from homeassistant.util.async_ import create_eager_task
|
2020-06-23 00:49:01 +00:00
|
|
|
|
|
|
|
from .const import (
|
|
|
|
ATTR_PAYLOAD,
|
|
|
|
ATTR_QOS,
|
|
|
|
ATTR_RETAIN,
|
|
|
|
ATTR_TOPIC,
|
2022-10-24 07:58:23 +00:00
|
|
|
CONF_CERTIFICATE,
|
|
|
|
CONF_CLIENT_CERT,
|
|
|
|
CONF_CLIENT_KEY,
|
|
|
|
DEFAULT_ENCODING,
|
2020-06-23 00:49:01 +00:00
|
|
|
DEFAULT_QOS,
|
|
|
|
DEFAULT_RETAIN,
|
2022-07-12 09:07:18 +00:00
|
|
|
DOMAIN,
|
2020-06-23 00:49:01 +00:00
|
|
|
)
|
2024-05-24 11:11:52 +00:00
|
|
|
from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage
|
2020-06-23 00:49:01 +00:00
|
|
|
|
2024-06-28 15:50:55 +00:00
|
|
|
AVAILABILITY_TIMEOUT = 50.0
|
2023-04-20 06:07:35 +00:00
|
|
|
|
2022-10-24 07:58:23 +00:00
|
|
|
TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
|
|
|
|
|
2022-11-10 14:24:56 +00:00
|
|
|
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
|
|
|
|
2024-06-25 08:33:58 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class EnsureJobAfterCooldown:
|
|
|
|
"""Ensure a cool down period before executing a job.
|
|
|
|
|
|
|
|
When a new execute request arrives we cancel the current request
|
|
|
|
and start a new one.
|
|
|
|
|
|
|
|
We allow patching this util, as we generally have exceptions
|
|
|
|
for sleeps/waits/debouncers/timers causing long run times in tests.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self, timeout: float, callback_job: Callable[[], Coroutine[Any, None, None]]
|
|
|
|
) -> None:
|
|
|
|
"""Initialize the timer."""
|
|
|
|
self._loop = asyncio.get_running_loop()
|
|
|
|
self._timeout = timeout
|
|
|
|
self._callback = callback_job
|
|
|
|
self._task: asyncio.Task | None = None
|
|
|
|
self._timer: asyncio.TimerHandle | None = None
|
|
|
|
self._next_execute_time = 0.0
|
|
|
|
|
|
|
|
def set_timeout(self, timeout: float) -> None:
|
|
|
|
"""Set a new timeout period."""
|
|
|
|
self._timeout = timeout
|
|
|
|
|
|
|
|
async def _async_job(self) -> None:
|
|
|
|
"""Execute after a cooldown period."""
|
|
|
|
try:
|
|
|
|
await self._callback()
|
|
|
|
except HomeAssistantError as ha_error:
|
|
|
|
_LOGGER.error("%s", ha_error)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_task_done(self, task: asyncio.Task) -> None:
|
|
|
|
"""Handle task done."""
|
|
|
|
self._task = None
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_execute(self) -> asyncio.Task:
|
|
|
|
"""Execute the job."""
|
|
|
|
if self._task:
|
|
|
|
# Task already running,
|
|
|
|
# so we schedule another run
|
|
|
|
self.async_schedule()
|
|
|
|
return self._task
|
|
|
|
|
|
|
|
self._async_cancel_timer()
|
|
|
|
self._task = create_eager_task(self._async_job())
|
|
|
|
self._task.add_done_callback(self._async_task_done)
|
|
|
|
return self._task
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_cancel_timer(self) -> None:
|
|
|
|
"""Cancel any pending task."""
|
|
|
|
if self._timer:
|
|
|
|
self._timer.cancel()
|
|
|
|
self._timer = None
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_schedule(self) -> None:
|
|
|
|
"""Ensure we execute after a cooldown period."""
|
|
|
|
# We want to reschedule the timer in the future
|
|
|
|
# every time this is called.
|
|
|
|
next_when = self._loop.time() + self._timeout
|
|
|
|
if not self._timer:
|
|
|
|
self._timer = self._loop.call_at(next_when, self._async_timer_reached)
|
|
|
|
return
|
|
|
|
|
|
|
|
if self._timer.when() < next_when:
|
|
|
|
# Timer already running, set the next execute time
|
|
|
|
# if it fires too early, it will get rescheduled
|
|
|
|
self._next_execute_time = next_when
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_timer_reached(self) -> None:
|
|
|
|
"""Handle timer fire."""
|
|
|
|
self._timer = None
|
|
|
|
if self._loop.time() >= self._next_execute_time:
|
|
|
|
self.async_execute()
|
|
|
|
return
|
|
|
|
# Timer fired too early because there were multiple
|
|
|
|
# calls async_schedule. Reschedule the timer.
|
|
|
|
self._timer = self._loop.call_at(
|
|
|
|
self._next_execute_time, self._async_timer_reached
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_cleanup(self) -> None:
|
|
|
|
"""Cleanup any pending task."""
|
|
|
|
self._async_cancel_timer()
|
|
|
|
if not self._task:
|
|
|
|
return
|
|
|
|
self._task.cancel()
|
|
|
|
try:
|
|
|
|
await self._task
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|
|
|
|
except Exception:
|
|
|
|
_LOGGER.exception("Error cleaning up task")
|
|
|
|
|
2020-06-23 00:49:01 +00:00
|
|
|
|
2024-03-09 20:55:00 +00:00
|
|
|
def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]:
|
|
|
|
"""Return the platforms to be set up."""
|
|
|
|
return {key for platform in config for key in platform}
|
|
|
|
|
|
|
|
|
|
|
|
async def async_forward_entry_setup_and_setup_discovery(
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
config_entry: ConfigEntry,
|
|
|
|
platforms: set[Platform | str],
|
|
|
|
late: bool = False,
|
2024-03-09 20:55:00 +00:00
|
|
|
) -> None:
|
|
|
|
"""Forward the config entry setup to the platforms and set up discovery."""
|
2024-05-22 09:21:51 +00:00
|
|
|
mqtt_data = hass.data[DATA_MQTT]
|
2024-03-09 20:55:00 +00:00
|
|
|
platforms_loaded = mqtt_data.platforms_loaded
|
|
|
|
new_platforms: set[Platform | str] = platforms - platforms_loaded
|
|
|
|
tasks: list[asyncio.Task] = []
|
|
|
|
if "device_automation" in new_platforms:
|
|
|
|
# Local import to avoid circular dependencies
|
|
|
|
# pylint: disable-next=import-outside-toplevel
|
|
|
|
from . import device_automation
|
|
|
|
|
|
|
|
tasks.append(
|
|
|
|
create_eager_task(device_automation.async_setup_entry(hass, config_entry))
|
|
|
|
)
|
|
|
|
if "tag" in new_platforms:
|
|
|
|
# Local import to avoid circular dependencies
|
|
|
|
# pylint: disable-next=import-outside-toplevel
|
|
|
|
from . import tag
|
|
|
|
|
|
|
|
tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry)))
|
2024-03-10 18:36:17 +00:00
|
|
|
if new_entity_platforms := (new_platforms - {"tag", "device_automation"}):
|
2024-06-13 01:06:11 +00:00
|
|
|
tasks.append(
|
|
|
|
create_eager_task(
|
|
|
|
hass.config_entries.async_forward_entry_setups(
|
|
|
|
config_entry, new_entity_platforms
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2024-03-09 20:55:00 +00:00
|
|
|
if not tasks:
|
|
|
|
return
|
|
|
|
await asyncio.gather(*tasks)
|
2024-03-10 18:36:17 +00:00
|
|
|
platforms_loaded.update(new_platforms)
|
2024-03-09 20:55:00 +00:00
|
|
|
|
|
|
|
|
2022-07-12 09:07:18 +00:00
|
|
|
def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None:
|
|
|
|
"""Return true when the MQTT config entry is enabled."""
|
2024-05-22 09:21:51 +00:00
|
|
|
# If the mqtt client is connected, skip the expensive config
|
|
|
|
# entry check as its roughly two orders of magnitude faster.
|
|
|
|
return (
|
|
|
|
DATA_MQTT in hass.data and hass.data[DATA_MQTT].client.connected
|
|
|
|
) or hass.config_entries.async_has_entries(
|
2024-05-22 01:04:31 +00:00
|
|
|
DOMAIN, include_disabled=False, include_ignore=False
|
|
|
|
)
|
2022-07-12 09:07:18 +00:00
|
|
|
|
|
|
|
|
2023-04-20 06:07:35 +00:00
|
|
|
async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool:
|
|
|
|
"""Wait for the MQTT client to become available.
|
|
|
|
|
|
|
|
Waits when mqtt set up is in progress,
|
|
|
|
It is not needed that the client is connected.
|
|
|
|
Returns True if the mqtt client is available.
|
|
|
|
Returns False when the client is not available.
|
|
|
|
"""
|
|
|
|
if not mqtt_config_entry_enabled(hass):
|
|
|
|
return False
|
|
|
|
|
|
|
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
|
|
|
if entry.state == ConfigEntryState.LOADED:
|
|
|
|
return True
|
|
|
|
|
|
|
|
state_reached_future: asyncio.Future[bool]
|
|
|
|
if DATA_MQTT_AVAILABLE not in hass.data:
|
2023-11-27 13:38:59 +00:00
|
|
|
state_reached_future = hass.loop.create_future()
|
|
|
|
hass.data[DATA_MQTT_AVAILABLE] = state_reached_future
|
2023-04-20 06:07:35 +00:00
|
|
|
else:
|
|
|
|
state_reached_future = hass.data[DATA_MQTT_AVAILABLE]
|
|
|
|
|
|
|
|
try:
|
2023-08-15 13:36:05 +00:00
|
|
|
async with asyncio.timeout(AVAILABILITY_TIMEOUT):
|
2023-04-20 06:07:35 +00:00
|
|
|
# Await the client setup or an error state was received
|
|
|
|
return await state_reached_future
|
2024-02-05 11:08:18 +00:00
|
|
|
except TimeoutError:
|
2023-04-20 06:07:35 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-10-26 11:52:34 +00:00
|
|
|
def valid_topic(topic: Any) -> str:
|
2024-05-22 03:11:05 +00:00
|
|
|
"""Validate that this is a valid topic name/filter.
|
|
|
|
|
|
|
|
This function is not cached and is not expected to be called
|
|
|
|
directly outside of this module. It is not marked as protected
|
|
|
|
only because its tested directly in test_util.py.
|
|
|
|
|
|
|
|
If it gets used outside of valid_subscribe_topic and
|
|
|
|
valid_publish_topic, it may need an lru_cache decorator or
|
|
|
|
an lru_cache decorator on the function where its used.
|
|
|
|
"""
|
2022-10-26 11:52:34 +00:00
|
|
|
validated_topic = cv.string(topic)
|
2020-06-23 00:49:01 +00:00
|
|
|
try:
|
2022-10-26 11:52:34 +00:00
|
|
|
raw_validated_topic = validated_topic.encode("utf-8")
|
2020-08-28 11:50:32 +00:00
|
|
|
except UnicodeError as err:
|
|
|
|
raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") from err
|
2022-10-26 11:52:34 +00:00
|
|
|
if not raw_validated_topic:
|
2020-06-23 00:49:01 +00:00
|
|
|
raise vol.Invalid("MQTT topic name/filter must not be empty.")
|
2022-10-26 11:52:34 +00:00
|
|
|
if len(raw_validated_topic) > 65535:
|
2020-06-23 00:49:01 +00:00
|
|
|
raise vol.Invalid(
|
|
|
|
"MQTT topic name/filter must not be longer than 65535 encoded bytes."
|
|
|
|
)
|
2024-05-22 03:11:05 +00:00
|
|
|
|
|
|
|
for char in validated_topic:
|
|
|
|
if char == "\0":
|
|
|
|
raise vol.Invalid("MQTT topic name/filter must not contain null character.")
|
|
|
|
if char <= "\u001f" or "\u007f" <= char <= "\u009f":
|
|
|
|
raise vol.Invalid(
|
|
|
|
"MQTT topic name/filter must not contain control characters."
|
|
|
|
)
|
|
|
|
if "\ufdd0" <= char <= "\ufdef" or (ord(char) & 0xFFFF) in (0xFFFE, 0xFFFF):
|
|
|
|
raise vol.Invalid("MQTT topic name/filter must not contain non-characters.")
|
2022-05-03 19:19:43 +00:00
|
|
|
|
2022-10-26 11:52:34 +00:00
|
|
|
return validated_topic
|
2020-06-23 00:49:01 +00:00
|
|
|
|
|
|
|
|
2024-05-22 03:11:05 +00:00
|
|
|
@lru_cache
|
2022-10-26 11:52:34 +00:00
|
|
|
def valid_subscribe_topic(topic: Any) -> str:
|
2020-06-23 00:49:01 +00:00
|
|
|
"""Validate that we can subscribe using this MQTT topic."""
|
2022-10-26 11:52:34 +00:00
|
|
|
validated_topic = valid_topic(topic)
|
2024-05-22 03:11:05 +00:00
|
|
|
if "+" in validated_topic:
|
|
|
|
for i in (i for i, c in enumerate(validated_topic) if c == "+"):
|
|
|
|
if (i > 0 and validated_topic[i - 1] != "/") or (
|
|
|
|
i < len(validated_topic) - 1 and validated_topic[i + 1] != "/"
|
|
|
|
):
|
|
|
|
raise vol.Invalid(
|
|
|
|
"Single-level wildcard must occupy an entire level of the filter"
|
|
|
|
)
|
2020-06-23 00:49:01 +00:00
|
|
|
|
2022-10-26 11:52:34 +00:00
|
|
|
index = validated_topic.find("#")
|
2020-06-23 00:49:01 +00:00
|
|
|
if index != -1:
|
2022-10-26 11:52:34 +00:00
|
|
|
if index != len(validated_topic) - 1:
|
2020-06-23 00:49:01 +00:00
|
|
|
# If there are multiple wildcards, this will also trigger
|
|
|
|
raise vol.Invalid(
|
2022-12-22 12:35:47 +00:00
|
|
|
"Multi-level wildcard must be the last character in the topic filter."
|
2020-06-23 00:49:01 +00:00
|
|
|
)
|
2022-10-26 11:52:34 +00:00
|
|
|
if len(validated_topic) > 1 and validated_topic[index - 1] != "/":
|
2020-06-23 00:49:01 +00:00
|
|
|
raise vol.Invalid(
|
|
|
|
"Multi-level wildcard must be after a topic level separator."
|
|
|
|
)
|
|
|
|
|
2022-10-26 11:52:34 +00:00
|
|
|
return validated_topic
|
2020-06-23 00:49:01 +00:00
|
|
|
|
|
|
|
|
2021-02-08 09:50:38 +00:00
|
|
|
def valid_subscribe_topic_template(value: Any) -> template.Template:
|
|
|
|
"""Validate either a jinja2 template or a valid MQTT subscription topic."""
|
2022-11-24 07:25:44 +00:00
|
|
|
tpl = cv.template(value)
|
2021-02-08 09:50:38 +00:00
|
|
|
|
|
|
|
if tpl.is_static:
|
|
|
|
valid_subscribe_topic(value)
|
|
|
|
|
|
|
|
return tpl
|
|
|
|
|
|
|
|
|
2024-05-22 03:11:05 +00:00
|
|
|
@lru_cache
|
2022-10-26 11:52:34 +00:00
|
|
|
def valid_publish_topic(topic: Any) -> str:
|
2020-06-23 00:49:01 +00:00
|
|
|
"""Validate that we can publish using this MQTT topic."""
|
2022-10-26 11:52:34 +00:00
|
|
|
validated_topic = valid_topic(topic)
|
|
|
|
if "+" in validated_topic or "#" in validated_topic:
|
2023-02-03 10:37:16 +00:00
|
|
|
raise vol.Invalid("Wildcards cannot be used in topic names")
|
2022-10-26 11:52:34 +00:00
|
|
|
return validated_topic
|
2020-06-23 00:49:01 +00:00
|
|
|
|
|
|
|
|
2022-11-10 14:24:56 +00:00
|
|
|
def valid_qos_schema(qos: Any) -> int:
|
|
|
|
"""Validate that QOS value is valid."""
|
2022-11-24 07:25:44 +00:00
|
|
|
validated_qos: int = _VALID_QOS_SCHEMA(qos)
|
|
|
|
return validated_qos
|
2020-06-23 00:49:01 +00:00
|
|
|
|
2022-11-10 14:24:56 +00:00
|
|
|
|
|
|
|
_MQTT_WILL_BIRTH_SCHEMA = vol.Schema(
|
2020-06-23 00:49:01 +00:00
|
|
|
{
|
|
|
|
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
2022-11-10 14:24:56 +00:00
|
|
|
vol.Required(ATTR_PAYLOAD): cv.string,
|
|
|
|
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
2020-06-23 00:49:01 +00:00
|
|
|
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
|
|
|
},
|
|
|
|
required=True,
|
|
|
|
)
|
2022-09-20 17:40:06 +00:00
|
|
|
|
|
|
|
|
2022-11-10 14:24:56 +00:00
|
|
|
def valid_birth_will(config: ConfigType) -> ConfigType:
|
|
|
|
"""Validate a birth or will configuration and required topic/payload."""
|
|
|
|
if config:
|
|
|
|
config = _MQTT_WILL_BIRTH_SCHEMA(config)
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
2022-10-24 07:58:23 +00:00
|
|
|
async def async_create_certificate_temp_files(
|
|
|
|
hass: HomeAssistant, config: ConfigType
|
|
|
|
) -> None:
|
|
|
|
"""Create certificate temporary files for the MQTT client."""
|
|
|
|
|
|
|
|
def _create_temp_file(temp_file: Path, data: str | None) -> None:
|
|
|
|
if data is None or data == "auto":
|
|
|
|
if temp_file.exists():
|
|
|
|
os.remove(Path(temp_file))
|
|
|
|
return
|
|
|
|
temp_file.write_text(data)
|
|
|
|
|
|
|
|
def _create_temp_dir_and_files() -> None:
|
|
|
|
"""Create temporary directory."""
|
|
|
|
temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
|
|
|
|
|
|
|
|
if (
|
|
|
|
config.get(CONF_CERTIFICATE)
|
|
|
|
or config.get(CONF_CLIENT_CERT)
|
|
|
|
or config.get(CONF_CLIENT_KEY)
|
|
|
|
) and not temp_dir.exists():
|
|
|
|
temp_dir.mkdir(0o700)
|
|
|
|
|
|
|
|
_create_temp_file(temp_dir / CONF_CERTIFICATE, config.get(CONF_CERTIFICATE))
|
|
|
|
_create_temp_file(temp_dir / CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
|
|
|
|
_create_temp_file(temp_dir / CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY))
|
|
|
|
|
|
|
|
await hass.async_add_executor_job(_create_temp_dir_and_files)
|
|
|
|
|
|
|
|
|
2024-05-24 11:11:52 +00:00
|
|
|
def check_state_too_long(
|
|
|
|
logger: logging.Logger, proposed_state: str, entity_id: str, msg: ReceiveMessage
|
|
|
|
) -> bool:
|
|
|
|
"""Check if the processed state is too long and log warning."""
|
|
|
|
if (state_length := len(proposed_state)) > MAX_LENGTH_STATE_STATE:
|
|
|
|
logger.warning(
|
|
|
|
"Cannot update state for entity %s after processing "
|
|
|
|
"payload on topic %s. The requested state (%s) exceeds "
|
|
|
|
"the maximum allowed length (%s). Fall back to "
|
|
|
|
"%s, failed state: %s",
|
|
|
|
entity_id,
|
|
|
|
msg.topic,
|
|
|
|
state_length,
|
|
|
|
MAX_LENGTH_STATE_STATE,
|
|
|
|
STATE_UNKNOWN,
|
|
|
|
proposed_state[:8192],
|
|
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2022-12-09 14:27:46 +00:00
|
|
|
def get_file_path(option: str, default: str | None = None) -> str | None:
|
2022-10-24 07:58:23 +00:00
|
|
|
"""Get file path of a certificate file."""
|
|
|
|
temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
|
|
|
|
if not temp_dir.exists():
|
|
|
|
return default
|
|
|
|
|
|
|
|
file_path: Path = temp_dir / option
|
|
|
|
if not file_path.exists():
|
|
|
|
return default
|
|
|
|
|
2022-12-09 14:27:46 +00:00
|
|
|
return str(temp_dir / option)
|
2022-10-24 07:58:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
|
|
|
|
"""Convert certificate file or setting to config entry setting."""
|
|
|
|
if file_name_or_auto == "auto":
|
|
|
|
return "auto"
|
|
|
|
try:
|
2022-11-14 15:22:23 +00:00
|
|
|
with open(file_name_or_auto, encoding=DEFAULT_ENCODING) as certificate_file:
|
|
|
|
return certificate_file.read()
|
2022-10-24 07:58:23 +00:00
|
|
|
except OSError:
|
|
|
|
return None
|