mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 22:57:17 +00:00
2024.4.1 (#114934)
This commit is contained in:
commit
b1fb77cb4d
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.4.6"]
|
"requirements": ["aioairzone-cloud==0.4.7"]
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from time import monotonic
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from yalexs.activity import Activity, ActivityType
|
from yalexs.activity import Activity, ActivityType
|
||||||
@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||||
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
|
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
|
||||||
|
|
||||||
|
INITIAL_LOCK_RESYNC_TIME = 60
|
||||||
|
|
||||||
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
|
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
|
||||||
# we want to debounce the updates so we don't hammer the activity api too much.
|
# we want to debounce the updates so we don't hammer the activity api too much.
|
||||||
ACTIVITY_DEBOUNCE_COOLDOWN = 3
|
ACTIVITY_DEBOUNCE_COOLDOWN = 4
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
|
|||||||
self.pubnub = pubnub
|
self.pubnub = pubnub
|
||||||
self._update_debounce: dict[str, Debouncer] = {}
|
self._update_debounce: dict[str, Debouncer] = {}
|
||||||
self._update_debounce_jobs: dict[str, HassJob] = {}
|
self._update_debounce_jobs: dict[str, HassJob] = {}
|
||||||
|
self._start_time: float | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
|
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
|
||||||
@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
|
|||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Token refresh check and catch up the activity stream."""
|
"""Token refresh check and catch up the activity stream."""
|
||||||
|
self._start_time = monotonic()
|
||||||
update_debounce = self._update_debounce
|
update_debounce = self._update_debounce
|
||||||
update_debounce_jobs = self._update_debounce_jobs
|
update_debounce_jobs = self._update_debounce_jobs
|
||||||
for house_id in self._house_ids:
|
for house_id in self._house_ids:
|
||||||
@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
|
|||||||
|
|
||||||
debouncer = self._update_debounce[house_id]
|
debouncer = self._update_debounce[house_id]
|
||||||
debouncer.async_schedule_call()
|
debouncer.async_schedule_call()
|
||||||
|
|
||||||
# Schedule two updates past the debounce time
|
# Schedule two updates past the debounce time
|
||||||
# to ensure we catch the case where the activity
|
# to ensure we catch the case where the activity
|
||||||
# api does not update right away and we need to poll
|
# api does not update right away and we need to poll
|
||||||
# it again. Sometimes the lock operator or a doorbell
|
# it again. Sometimes the lock operator or a doorbell
|
||||||
# will not show up in the activity stream right away.
|
# will not show up in the activity stream right away.
|
||||||
|
# Only do additional polls if we are past
|
||||||
|
# the initial lock resync time to avoid a storm
|
||||||
|
# of activity at setup.
|
||||||
|
if (
|
||||||
|
not self._start_time
|
||||||
|
or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME
|
||||||
|
):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Skipping additional updates due to ongoing initial lock resync time"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("Scheduling additional updates for house id %s", house_id)
|
||||||
job = self._update_debounce_jobs[house_id]
|
job = self._update_debounce_jobs[house_id]
|
||||||
for step in (1, 2):
|
for step in (1, 2):
|
||||||
future_updates.append(
|
future_updates.append(
|
||||||
|
@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
|
|||||||
# Limit battery, online, and hardware updates to hourly
|
# Limit battery, online, and hardware updates to hourly
|
||||||
# in order to reduce the number of api requests and
|
# in order to reduce the number of api requests and
|
||||||
# avoid hitting rate limits
|
# avoid hitting rate limits
|
||||||
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
|
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24)
|
||||||
|
|
||||||
# Activity needs to be checked more frequently as the
|
# Activity needs to be checked more frequently as the
|
||||||
# doorbell motion and rings are included here
|
# doorbell motion and rings are included here
|
||||||
|
@ -49,9 +49,17 @@ class AugustSubscriberMixin:
|
|||||||
"""Call the refresh method."""
|
"""Call the refresh method."""
|
||||||
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
|
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
|
||||||
|
"""Cancel the scheduled update."""
|
||||||
|
if self._unsub_interval:
|
||||||
|
self._unsub_interval()
|
||||||
|
self._unsub_interval = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_setup_listeners(self) -> None:
|
def _async_setup_listeners(self) -> None:
|
||||||
"""Create interval and stop listeners."""
|
"""Create interval and stop listeners."""
|
||||||
|
self._async_cancel_update_interval()
|
||||||
self._unsub_interval = async_track_time_interval(
|
self._unsub_interval = async_track_time_interval(
|
||||||
self._hass,
|
self._hass,
|
||||||
self._async_scheduled_refresh,
|
self._async_scheduled_refresh,
|
||||||
@ -59,17 +67,12 @@ class AugustSubscriberMixin:
|
|||||||
name="august refresh",
|
name="august refresh",
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
if not self._stop_interval:
|
||||||
def _async_cancel_update_interval(_: Event) -> None:
|
self._stop_interval = self._hass.bus.async_listen(
|
||||||
self._stop_interval = None
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
if self._unsub_interval:
|
self._async_cancel_update_interval,
|
||||||
self._unsub_interval()
|
run_immediately=True,
|
||||||
|
)
|
||||||
self._stop_interval = self._hass.bus.async_listen(
|
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
_async_cancel_update_interval,
|
|
||||||
run_immediately=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_unsubscribe_device_id(
|
def async_unsubscribe_device_id(
|
||||||
@ -82,13 +85,7 @@ class AugustSubscriberMixin:
|
|||||||
|
|
||||||
if self._subscriptions:
|
if self._subscriptions:
|
||||||
return
|
return
|
||||||
|
self._async_cancel_update_interval()
|
||||||
if self._unsub_interval:
|
|
||||||
self._unsub_interval()
|
|
||||||
self._unsub_interval = None
|
|
||||||
if self._stop_interval:
|
|
||||||
self._stop_interval()
|
|
||||||
self._stop_interval = None
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_signal_device_id_update(self, device_id: str) -> None:
|
def async_signal_device_id_update(self, device_id: str) -> None:
|
||||||
|
@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
|||||||
mjpeg_url=self.mjpeg_source,
|
mjpeg_url=self.mjpeg_source,
|
||||||
still_image_url=self.image_source,
|
still_image_url=self.image_source,
|
||||||
authentication=HTTP_DIGEST_AUTHENTICATION,
|
authentication=HTTP_DIGEST_AUTHENTICATION,
|
||||||
|
verify_ssl=False,
|
||||||
unique_id=f"{hub.unique_id}-camera",
|
unique_id=f"{hub.unique_id}-camera",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
|||||||
|
|
||||||
Additionally used when device change IP address.
|
Additionally used when device change IP address.
|
||||||
"""
|
"""
|
||||||
|
proto = self.hub.config.protocol
|
||||||
|
host = self.hub.config.host
|
||||||
|
port = self.hub.config.port
|
||||||
|
|
||||||
image_options = self.generate_options(skip_stream_profile=True)
|
image_options = self.generate_options(skip_stream_profile=True)
|
||||||
self._still_image_url = (
|
self._still_image_url = (
|
||||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}"
|
||||||
f"/jpg/image.cgi{image_options}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mjpeg_options = self.generate_options()
|
mjpeg_options = self.generate_options()
|
||||||
self._mjpeg_url = (
|
self._mjpeg_url = (
|
||||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}"
|
||||||
f"/mjpg/video.cgi{mjpeg_options}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stream_options = self.generate_options(add_video_codec_h264=True)
|
stream_options = self.generate_options(add_video_codec_h264=True)
|
||||||
@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
|||||||
self.hub.additional_diagnostics["camera_sources"] = {
|
self.hub.additional_diagnostics["camera_sources"] = {
|
||||||
"Image": self._still_image_url,
|
"Image": self._still_image_url,
|
||||||
"MJPEG": self._mjpeg_url,
|
"MJPEG": self._mjpeg_url,
|
||||||
"Stream": (
|
"Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"),
|
||||||
f"rtsp://user:pass@{self.hub.config.host}/axis-media"
|
|
||||||
f"/media.amp{stream_options}"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
|||||||
self, entry_data: Mapping[str, Any], keep_password: bool
|
self, entry_data: Mapping[str, Any], keep_password: bool
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Re-run configuration step."""
|
"""Re-run configuration step."""
|
||||||
|
protocol = entry_data.get(CONF_PROTOCOL, "http")
|
||||||
|
password = entry_data[CONF_PASSWORD] if keep_password else ""
|
||||||
self.discovery_schema = {
|
self.discovery_schema = {
|
||||||
vol.Required(
|
vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
|
||||||
CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http")
|
|
||||||
): str,
|
|
||||||
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
|
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
|
||||||
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
||||||
vol.Required(
|
vol.Required(CONF_PASSWORD, default=password): str,
|
||||||
CONF_PASSWORD,
|
|
||||||
default=entry_data[CONF_PASSWORD] if keep_password else "",
|
|
||||||
): str,
|
|
||||||
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
|
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_PROTOCOL,
|
||||||
CONF_TRIGGER_TIME,
|
CONF_TRIGGER_TIME,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
@ -31,6 +32,7 @@ class AxisConfig:
|
|||||||
|
|
||||||
entry: ConfigEntry
|
entry: ConfigEntry
|
||||||
|
|
||||||
|
protocol: str
|
||||||
host: str
|
host: str
|
||||||
port: int
|
port: int
|
||||||
username: str
|
username: str
|
||||||
@ -54,6 +56,7 @@ class AxisConfig:
|
|||||||
options = config_entry.options
|
options = config_entry.options
|
||||||
return cls(
|
return cls(
|
||||||
entry=config_entry,
|
entry=config_entry,
|
||||||
|
protocol=config.get(CONF_PROTOCOL, "http"),
|
||||||
host=config[CONF_HOST],
|
host=config[CONF_HOST],
|
||||||
username=config[CONF_USERNAME],
|
username=config[CONF_USERNAME],
|
||||||
password=config[CONF_PASSWORD],
|
password=config[CONF_PASSWORD],
|
||||||
|
@ -11,7 +11,11 @@ import requests
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import (
|
||||||
|
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
)
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
@ -43,6 +47,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
if DOMAIN not in config:
|
if DOMAIN not in config:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
hass.async_create_task(_async_import_config(hass, config))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None:
|
||||||
|
"""Import the Downloader component from the YAML file."""
|
||||||
|
|
||||||
import_result = await hass.config_entries.flow.async_init(
|
import_result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": SOURCE_IMPORT},
|
context={"source": SOURCE_IMPORT},
|
||||||
@ -51,28 +62,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
translation_key = "deprecated_yaml"
|
|
||||||
if (
|
if (
|
||||||
import_result["type"] == FlowResultType.ABORT
|
import_result["type"] == FlowResultType.ABORT
|
||||||
and import_result["reason"] == "import_failed"
|
and import_result["reason"] != "single_instance_allowed"
|
||||||
):
|
):
|
||||||
translation_key = "import_failed"
|
async_create_issue(
|
||||||
|
hass,
|
||||||
async_create_issue(
|
DOMAIN,
|
||||||
hass,
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
DOMAIN,
|
breaks_in_ha_version="2024.10.0",
|
||||||
f"deprecated_yaml_{DOMAIN}",
|
is_fixable=False,
|
||||||
breaks_in_ha_version="2024.9.0",
|
issue_domain=DOMAIN,
|
||||||
is_fixable=False,
|
severity=IssueSeverity.WARNING,
|
||||||
issue_domain=DOMAIN,
|
translation_key="directory_does_not_exist",
|
||||||
severity=IssueSeverity.WARNING,
|
translation_placeholders={
|
||||||
translation_key=translation_key,
|
"domain": DOMAIN,
|
||||||
translation_placeholders={
|
"integration_title": "Downloader",
|
||||||
"domain": DOMAIN,
|
"url": "/config/integrations/dashboard/add?domain=downloader",
|
||||||
"integration_title": "Downloader",
|
},
|
||||||
},
|
)
|
||||||
)
|
else:
|
||||||
return True
|
async_create_issue(
|
||||||
|
hass,
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
|
breaks_in_ha_version="2024.10.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": "Downloader",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
@ -83,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
if not os.path.isabs(download_path):
|
if not os.path.isabs(download_path):
|
||||||
download_path = hass.config.path(download_path)
|
download_path = hass.config.path(download_path)
|
||||||
|
|
||||||
if not os.path.isdir(download_path):
|
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Download path %s does not exist. File Downloader not active", download_path
|
"Download path %s does not exist. File Downloader not active", download_path
|
||||||
)
|
)
|
||||||
|
@ -46,12 +46,16 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(
|
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle a flow initiated by configuration file."""
|
"""Handle a flow initiated by configuration file."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
return await self.async_step_user(user_input)
|
try:
|
||||||
|
await self._validate_input(user_input)
|
||||||
|
except DirectoryDoesNotExist:
|
||||||
|
return self.async_abort(reason="directory_does_not_exist")
|
||||||
|
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||||
|
|
||||||
async def _validate_input(self, user_input: dict[str, Any]) -> None:
|
async def _validate_input(self, user_input: dict[str, Any]) -> None:
|
||||||
"""Validate the user input if the directory exists."""
|
"""Validate the user input if the directory exists."""
|
||||||
|
@ -37,13 +37,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"deprecated_yaml": {
|
"directory_does_not_exist": {
|
||||||
"title": "The {integration_title} YAML configuration is being removed",
|
|
||||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
|
||||||
},
|
|
||||||
"import_failed": {
|
|
||||||
"title": "The {integration_title} failed to import",
|
"title": "The {integration_title} failed to import",
|
||||||
"description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details."
|
"description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20240403.1"]
|
"requirements": ["home-assistant-frontend==20240404.1"]
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path):
|
|||||||
"views": views,
|
"views": views,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config is None:
|
if config is None or "views" not in config:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
for idx, view in enumerate(config["views"]):
|
for idx, view in enumerate(config["views"]):
|
||||||
|
@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity):
|
|||||||
else:
|
else:
|
||||||
brightness = self._prev_brightness
|
brightness = self._prev_brightness
|
||||||
self._prev_brightness = brightness
|
self._prev_brightness = brightness
|
||||||
args = {"new_level": brightness}
|
args = {"new_level": to_lutron_level(brightness)}
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
|
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
|
||||||
self._lutron_device.set_level(**args)
|
self._lutron_device.set_level(**args)
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from aiohttp import ClientError, ClientResponseError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
from myuplink import MyUplinkAPI, get_manufacturer, get_system_name
|
from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
@ -92,7 +92,7 @@ def create_devices(
|
|||||||
identifiers={(DOMAIN, device_id)},
|
identifiers={(DOMAIN, device_id)},
|
||||||
name=get_system_name(system),
|
name=get_system_name(system),
|
||||||
manufacturer=get_manufacturer(device),
|
manufacturer=get_manufacturer(device),
|
||||||
model=device.productName,
|
model=get_model(device),
|
||||||
sw_version=device.firmwareCurrent,
|
sw_version=device.firmwareCurrent,
|
||||||
serial_number=device.product_serial_number,
|
serial_number=device.product_serial_number,
|
||||||
)
|
)
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["application_credentials"],
|
"dependencies": ["application_credentials"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/myuplink",
|
"documentation": "https://www.home-assistant.io/integrations/myuplink",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["myuplink==0.5.0"]
|
"requirements": ["myuplink==0.6.0"]
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
(CONF_REFRESH_TOKEN, client.refresh_token),
|
(CONF_REFRESH_TOKEN, client.refresh_token),
|
||||||
(CONF_USER_UUID, client.user_uuid),
|
(CONF_USER_UUID, client.user_uuid),
|
||||||
):
|
):
|
||||||
if entry.data[key] == value:
|
if entry.data.get(key) == value:
|
||||||
continue
|
continue
|
||||||
entry_updates["data"][key] = value
|
entry_updates["data"][key] = value
|
||||||
|
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["opower"],
|
"loggers": ["opower"],
|
||||||
"requirements": ["opower==0.4.2"]
|
"requirements": ["opower==0.4.3"]
|
||||||
}
|
}
|
||||||
|
@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase):
|
|||||||
"start_ts",
|
"start_ts",
|
||||||
unique=True,
|
unique=True,
|
||||||
),
|
),
|
||||||
|
_DEFAULT_TABLE_ARGS,
|
||||||
)
|
)
|
||||||
__tablename__ = TABLE_STATISTICS
|
__tablename__ = TABLE_STATISTICS
|
||||||
|
|
||||||
@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase):
|
|||||||
"start_ts",
|
"start_ts",
|
||||||
unique=True,
|
unique=True,
|
||||||
),
|
),
|
||||||
|
_DEFAULT_TABLE_ARGS,
|
||||||
)
|
)
|
||||||
__tablename__ = TABLE_STATISTICS_SHORT_TERM
|
__tablename__ = TABLE_STATISTICS_SHORT_TERM
|
||||||
|
|
||||||
@ -760,7 +762,10 @@ class StatisticsMeta(Base):
|
|||||||
class RecorderRuns(Base):
|
class RecorderRuns(Base):
|
||||||
"""Representation of recorder run."""
|
"""Representation of recorder run."""
|
||||||
|
|
||||||
__table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
|
__table_args__ = (
|
||||||
|
Index("ix_recorder_runs_start_end", "start", "end"),
|
||||||
|
_DEFAULT_TABLE_ARGS,
|
||||||
|
)
|
||||||
__tablename__ = TABLE_RECORDER_RUNS
|
__tablename__ = TABLE_RECORDER_RUNS
|
||||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||||
@ -789,6 +794,7 @@ class MigrationChanges(Base):
|
|||||||
"""Representation of migration changes."""
|
"""Representation of migration changes."""
|
||||||
|
|
||||||
__tablename__ = TABLE_MIGRATION_CHANGES
|
__tablename__ = TABLE_MIGRATION_CHANGES
|
||||||
|
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||||
|
|
||||||
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||||
version: Mapped[int] = mapped_column(SmallInteger)
|
version: Mapped[int] = mapped_column(SmallInteger)
|
||||||
@ -798,6 +804,8 @@ class SchemaChanges(Base):
|
|||||||
"""Representation of schema version changes."""
|
"""Representation of schema version changes."""
|
||||||
|
|
||||||
__tablename__ = TABLE_SCHEMA_CHANGES
|
__tablename__ = TABLE_SCHEMA_CHANGES
|
||||||
|
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||||
|
|
||||||
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||||
schema_version: Mapped[int | None] = mapped_column(Integer)
|
schema_version: Mapped[int | None] = mapped_column(Integer)
|
||||||
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||||
@ -816,6 +824,8 @@ class StatisticsRuns(Base):
|
|||||||
"""Representation of statistics run."""
|
"""Representation of statistics run."""
|
||||||
|
|
||||||
__tablename__ = TABLE_STATISTICS_RUNS
|
__tablename__ = TABLE_STATISTICS_RUNS
|
||||||
|
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||||
|
|
||||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
|
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
|
||||||
|
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/romy",
|
"documentation": "https://www.home-assistant.io/integrations/romy",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["romy==0.0.7"],
|
"requirements": ["romy==0.0.10"],
|
||||||
"zeroconf": ["_aicu-http._tcp.local."]
|
"zeroconf": ["_aicu-http._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string,
|
vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string,
|
||||||
vol.Optional(CONF_NAME, default="Rova"): cv.string,
|
vol.Optional(CONF_NAME, default="Rova"): cv.string,
|
||||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All(
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All(
|
||||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -270,7 +270,7 @@ class SnmpData:
|
|||||||
"SNMP OID %s received type=%s and data %s",
|
"SNMP OID %s received type=%s and data %s",
|
||||||
self._baseoid,
|
self._baseoid,
|
||||||
type(value),
|
type(value),
|
||||||
bytes(value),
|
value,
|
||||||
)
|
)
|
||||||
if isinstance(value, NoSuchObject):
|
if isinstance(value, NoSuchObject):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["systembridgeconnector"],
|
"loggers": ["systembridgeconnector"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["systembridgeconnector==4.0.3"],
|
"requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"],
|
||||||
"zeroconf": ["_system-bridge._tcp.local."]
|
"zeroconf": ["_system-bridge._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["weatherflow4py==0.2.17"]
|
"requirements": ["weatherflow4py==0.2.20"]
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ from .util.signal_type import SignalType
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 4
|
MINOR_VERSION: Final = 4
|
||||||
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, 12, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||||
|
@ -401,6 +401,7 @@ class HomeAssistant:
|
|||||||
self.services = ServiceRegistry(self)
|
self.services = ServiceRegistry(self)
|
||||||
self.states = StateMachine(self.bus, self.loop)
|
self.states = StateMachine(self.bus, self.loop)
|
||||||
self.config = Config(self, config_dir)
|
self.config = Config(self, config_dir)
|
||||||
|
self.config.async_initialize()
|
||||||
self.components = loader.Components(self)
|
self.components = loader.Components(self)
|
||||||
self.helpers = loader.Helpers(self)
|
self.helpers = loader.Helpers(self)
|
||||||
self.state: CoreState = CoreState.not_running
|
self.state: CoreState = CoreState.not_running
|
||||||
@ -2589,12 +2590,12 @@ class ServiceRegistry:
|
|||||||
class Config:
|
class Config:
|
||||||
"""Configuration settings for Home Assistant."""
|
"""Configuration settings for Home Assistant."""
|
||||||
|
|
||||||
|
_store: Config._ConfigStore
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
||||||
"""Initialize a new config object."""
|
"""Initialize a new config object."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
|
||||||
self._store = self._ConfigStore(self.hass, config_dir)
|
|
||||||
|
|
||||||
self.latitude: float = 0
|
self.latitude: float = 0
|
||||||
self.longitude: float = 0
|
self.longitude: float = 0
|
||||||
|
|
||||||
@ -2645,6 +2646,13 @@ class Config:
|
|||||||
# If Home Assistant is running in safe mode
|
# If Home Assistant is running in safe mode
|
||||||
self.safe_mode: bool = False
|
self.safe_mode: bool = False
|
||||||
|
|
||||||
|
def async_initialize(self) -> None:
|
||||||
|
"""Finish initializing a config object.
|
||||||
|
|
||||||
|
This must be called before the config object is used.
|
||||||
|
"""
|
||||||
|
self._store = self._ConfigStore(self.hass)
|
||||||
|
|
||||||
def distance(self, lat: float, lon: float) -> float | None:
|
def distance(self, lat: float, lon: float) -> float | None:
|
||||||
"""Calculate distance from Home Assistant.
|
"""Calculate distance from Home Assistant.
|
||||||
|
|
||||||
@ -2850,7 +2858,6 @@ class Config:
|
|||||||
"country": self.country,
|
"country": self.country,
|
||||||
"language": self.language,
|
"language": self.language,
|
||||||
}
|
}
|
||||||
|
|
||||||
await self._store.async_save(data)
|
await self._store.async_save(data)
|
||||||
|
|
||||||
# Circular dependency prevents us from generating the class at top level
|
# Circular dependency prevents us from generating the class at top level
|
||||||
@ -2860,7 +2867,7 @@ class Config:
|
|||||||
class _ConfigStore(Store[dict[str, Any]]):
|
class _ConfigStore(Store[dict[str, Any]]):
|
||||||
"""Class to help storing Config data."""
|
"""Class to help storing Config data."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize storage class."""
|
"""Initialize storage class."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@ -2869,7 +2876,6 @@ class Config:
|
|||||||
private=True,
|
private=True,
|
||||||
atomic_writes=True,
|
atomic_writes=True,
|
||||||
minor_version=CORE_STORAGE_MINOR_VERSION,
|
minor_version=CORE_STORAGE_MINOR_VERSION,
|
||||||
config_dir=config_dir,
|
|
||||||
)
|
)
|
||||||
self._original_unit_system: str | None = None # from old store 1.1
|
self._original_unit_system: str | None = None # from old store 1.1
|
||||||
|
|
||||||
|
@ -1855,6 +1855,12 @@ def determine_script_action(action: dict[str, Any]) -> str:
|
|||||||
"""Determine action type."""
|
"""Determine action type."""
|
||||||
if not (actions := ACTIONS_SET.intersection(action)):
|
if not (actions := ACTIONS_SET.intersection(action)):
|
||||||
raise ValueError("Unable to determine action")
|
raise ValueError("Unable to determine action")
|
||||||
|
if len(actions) > 1:
|
||||||
|
# Ambiguous action, select the first one in the
|
||||||
|
# order of the ACTIONS_MAP
|
||||||
|
for action_key, _script_action in ACTIONS_MAP.items():
|
||||||
|
if action_key in actions:
|
||||||
|
return _script_action
|
||||||
return ACTIONS_MAP[actions.pop()]
|
return ACTIONS_MAP[actions.pop()]
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,9 +95,7 @@ async def async_migrator(
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_internal_store_manager(
|
def get_internal_store_manager(hass: HomeAssistant) -> _StoreManager:
|
||||||
hass: HomeAssistant, config_dir: str | None = None
|
|
||||||
) -> _StoreManager:
|
|
||||||
"""Get the store manager.
|
"""Get the store manager.
|
||||||
|
|
||||||
This function is not part of the API and should only be
|
This function is not part of the API and should only be
|
||||||
@ -105,7 +103,7 @@ def get_internal_store_manager(
|
|||||||
guaranteed to be stable.
|
guaranteed to be stable.
|
||||||
"""
|
"""
|
||||||
if STORAGE_MANAGER not in hass.data:
|
if STORAGE_MANAGER not in hass.data:
|
||||||
manager = _StoreManager(hass, config_dir or hass.config.config_dir)
|
manager = _StoreManager(hass)
|
||||||
hass.data[STORAGE_MANAGER] = manager
|
hass.data[STORAGE_MANAGER] = manager
|
||||||
return hass.data[STORAGE_MANAGER]
|
return hass.data[STORAGE_MANAGER]
|
||||||
|
|
||||||
@ -116,13 +114,13 @@ class _StoreManager:
|
|||||||
The store manager is used to cache and manage storage files.
|
The store manager is used to cache and manage storage files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize storage manager class."""
|
"""Initialize storage manager class."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._invalidated: set[str] = set()
|
self._invalidated: set[str] = set()
|
||||||
self._files: set[str] | None = None
|
self._files: set[str] | None = None
|
||||||
self._data_preload: dict[str, json_util.JsonValueType] = {}
|
self._data_preload: dict[str, json_util.JsonValueType] = {}
|
||||||
self._storage_path: Path = Path(config_dir).joinpath(STORAGE_DIR)
|
self._storage_path: Path = Path(hass.config.config_dir).joinpath(STORAGE_DIR)
|
||||||
self._cancel_cleanup: asyncio.TimerHandle | None = None
|
self._cancel_cleanup: asyncio.TimerHandle | None = None
|
||||||
|
|
||||||
async def async_initialize(self) -> None:
|
async def async_initialize(self) -> None:
|
||||||
@ -251,7 +249,6 @@ class Store(Generic[_T]):
|
|||||||
encoder: type[JSONEncoder] | None = None,
|
encoder: type[JSONEncoder] | None = None,
|
||||||
minor_version: int = 1,
|
minor_version: int = 1,
|
||||||
read_only: bool = False,
|
read_only: bool = False,
|
||||||
config_dir: str | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize storage class."""
|
"""Initialize storage class."""
|
||||||
self.version = version
|
self.version = version
|
||||||
@ -268,7 +265,7 @@ class Store(Generic[_T]):
|
|||||||
self._atomic_writes = atomic_writes
|
self._atomic_writes = atomic_writes
|
||||||
self._read_only = read_only
|
self._read_only = read_only
|
||||||
self._next_write_time = 0.0
|
self._next_write_time = 0.0
|
||||||
self._manager = get_internal_store_manager(hass, config_dir)
|
self._manager = get_internal_store_manager(hass)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def path(self):
|
def path(self):
|
||||||
|
@ -30,7 +30,7 @@ habluetooth==2.4.2
|
|||||||
hass-nabucasa==0.79.0
|
hass-nabucasa==0.79.0
|
||||||
hassil==1.6.1
|
hassil==1.6.1
|
||||||
home-assistant-bluetooth==1.12.0
|
home-assistant-bluetooth==1.12.0
|
||||||
home-assistant-frontend==20240403.1
|
home-assistant-frontend==20240404.1
|
||||||
home-assistant-intents==2024.4.3
|
home-assistant-intents==2024.4.3
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.4.0"
|
version = "2024.4.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
@ -513,8 +513,6 @@ filterwarnings = [
|
|||||||
"ignore:invalid escape sequence:SyntaxWarning:.*stringcase",
|
"ignore:invalid escape sequence:SyntaxWarning:.*stringcase",
|
||||||
# https://github.com/pyudev/pyudev/pull/466 - >=0.24.0
|
# https://github.com/pyudev/pyudev/pull/466 - >=0.24.0
|
||||||
"ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor",
|
"ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor",
|
||||||
# https://github.com/xeniter/romy/pull/1 - >=0.0.8
|
|
||||||
"ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils",
|
|
||||||
# https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3
|
# https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3
|
||||||
"ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas",
|
"ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas",
|
||||||
# https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1
|
# https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1
|
||||||
|
@ -185,7 +185,7 @@ aio-georss-gdacs==0.9
|
|||||||
aioairq==0.3.2
|
aioairq==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.airzone_cloud
|
# homeassistant.components.airzone_cloud
|
||||||
aioairzone-cloud==0.4.6
|
aioairzone-cloud==0.4.7
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.7.6
|
aioairzone==0.7.6
|
||||||
@ -1077,7 +1077,7 @@ hole==0.8.0
|
|||||||
holidays==0.46
|
holidays==0.46
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240403.1
|
home-assistant-frontend==20240404.1
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.4.3
|
home-assistant-intents==2024.4.3
|
||||||
@ -1349,7 +1349,7 @@ mutesync==0.0.1
|
|||||||
mypermobil==0.1.8
|
mypermobil==0.1.8
|
||||||
|
|
||||||
# homeassistant.components.myuplink
|
# homeassistant.components.myuplink
|
||||||
myuplink==0.5.0
|
myuplink==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.nad
|
# homeassistant.components.nad
|
||||||
nad-receiver==0.3.0
|
nad-receiver==0.3.0
|
||||||
@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17
|
|||||||
openwrt-ubus-rpc==0.0.2
|
openwrt-ubus-rpc==0.0.2
|
||||||
|
|
||||||
# homeassistant.components.opower
|
# homeassistant.components.opower
|
||||||
opower==0.4.2
|
opower==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.oralb
|
# homeassistant.components.oralb
|
||||||
oralb-ble==0.17.6
|
oralb-ble==0.17.6
|
||||||
@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1
|
|||||||
rokuecp==0.19.2
|
rokuecp==0.19.2
|
||||||
|
|
||||||
# homeassistant.components.romy
|
# homeassistant.components.romy
|
||||||
romy==0.0.7
|
romy==0.0.10
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.8.1
|
roombapy==1.8.1
|
||||||
@ -2654,6 +2654,9 @@ synology-srm==0.2.0
|
|||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==4.0.3
|
systembridgeconnector==4.0.3
|
||||||
|
|
||||||
|
# homeassistant.components.system_bridge
|
||||||
|
systembridgemodels==4.0.4
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.6.0
|
tailscale==0.6.0
|
||||||
|
|
||||||
@ -2838,7 +2841,7 @@ watchdog==2.3.1
|
|||||||
waterfurnace==1.1.0
|
waterfurnace==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.weatherflow_cloud
|
# homeassistant.components.weatherflow_cloud
|
||||||
weatherflow4py==0.2.17
|
weatherflow4py==0.2.20
|
||||||
|
|
||||||
# homeassistant.components.webmin
|
# homeassistant.components.webmin
|
||||||
webmin-xmlrpc==0.0.2
|
webmin-xmlrpc==0.0.2
|
||||||
|
@ -164,7 +164,7 @@ aio-georss-gdacs==0.9
|
|||||||
aioairq==0.3.2
|
aioairq==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.airzone_cloud
|
# homeassistant.components.airzone_cloud
|
||||||
aioairzone-cloud==0.4.6
|
aioairzone-cloud==0.4.7
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.7.6
|
aioairzone==0.7.6
|
||||||
@ -876,7 +876,7 @@ hole==0.8.0
|
|||||||
holidays==0.46
|
holidays==0.46
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240403.1
|
home-assistant-frontend==20240404.1
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.4.3
|
home-assistant-intents==2024.4.3
|
||||||
@ -1088,7 +1088,7 @@ mutesync==0.0.1
|
|||||||
mypermobil==0.1.8
|
mypermobil==0.1.8
|
||||||
|
|
||||||
# homeassistant.components.myuplink
|
# homeassistant.components.myuplink
|
||||||
myuplink==0.5.0
|
myuplink==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.keenetic_ndms2
|
# homeassistant.components.keenetic_ndms2
|
||||||
ndms2-client==0.1.2
|
ndms2-client==0.1.2
|
||||||
@ -1176,7 +1176,7 @@ openerz-api==0.3.0
|
|||||||
openhomedevice==2.2.0
|
openhomedevice==2.2.0
|
||||||
|
|
||||||
# homeassistant.components.opower
|
# homeassistant.components.opower
|
||||||
opower==0.4.2
|
opower==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.oralb
|
# homeassistant.components.oralb
|
||||||
oralb-ble==0.17.6
|
oralb-ble==0.17.6
|
||||||
@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9
|
|||||||
rokuecp==0.19.2
|
rokuecp==0.19.2
|
||||||
|
|
||||||
# homeassistant.components.romy
|
# homeassistant.components.romy
|
||||||
romy==0.0.7
|
romy==0.0.10
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.8.1
|
roombapy==1.8.1
|
||||||
@ -2049,6 +2049,9 @@ switchbot-api==2.0.0
|
|||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==4.0.3
|
systembridgeconnector==4.0.3
|
||||||
|
|
||||||
|
# homeassistant.components.system_bridge
|
||||||
|
systembridgemodels==4.0.4
|
||||||
|
|
||||||
# homeassistant.components.tailscale
|
# homeassistant.components.tailscale
|
||||||
tailscale==0.6.0
|
tailscale==0.6.0
|
||||||
|
|
||||||
@ -2185,7 +2188,7 @@ wallbox==0.6.0
|
|||||||
watchdog==2.3.1
|
watchdog==2.3.1
|
||||||
|
|
||||||
# homeassistant.components.weatherflow_cloud
|
# homeassistant.components.weatherflow_cloud
|
||||||
weatherflow4py==0.2.17
|
weatherflow4py==0.2.20
|
||||||
|
|
||||||
# homeassistant.components.webmin
|
# homeassistant.components.webmin
|
||||||
webmin-xmlrpc==0.0.2
|
webmin-xmlrpc==0.0.2
|
||||||
|
@ -4,9 +4,11 @@ import datetime
|
|||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
from yalexs.pubnub_async import AugustPubNub
|
from yalexs.pubnub_async import AugustPubNub
|
||||||
|
|
||||||
|
from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME
|
||||||
from homeassistant.components.lock import (
|
from homeassistant.components.lock import (
|
||||||
DOMAIN as LOCK_DOMAIN,
|
DOMAIN as LOCK_DOMAIN,
|
||||||
STATE_JAMMED,
|
STATE_JAMMED,
|
||||||
@ -155,7 +157,9 @@ async def test_one_lock_operation(
|
|||||||
|
|
||||||
|
|
||||||
async def test_one_lock_operation_pubnub_connected(
|
async def test_one_lock_operation_pubnub_connected(
|
||||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test lock and unlock operations are async when pubnub is connected."""
|
"""Test lock and unlock operations are async when pubnub is connected."""
|
||||||
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
|
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
|
||||||
@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected(
|
|||||||
== STATE_UNKNOWN
|
== STATE_UNKNOWN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
freezer.tick(INITIAL_LOCK_RESYNC_TIME)
|
||||||
|
|
||||||
|
pubnub.message(
|
||||||
|
pubnub,
|
||||||
|
Mock(
|
||||||
|
channel=lock_one.pubsub_channel,
|
||||||
|
timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000,
|
||||||
|
message={
|
||||||
|
"status": "kAugLockState_Unlocked",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
|
||||||
|
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
|
||||||
|
|
||||||
|
|
||||||
async def test_lock_jammed(hass: HomeAssistant) -> None:
|
async def test_lock_jammed(hass: HomeAssistant) -> None:
|
||||||
"""Test lock gets jammed on unlock."""
|
"""Test lock gets jammed on unlock."""
|
||||||
|
@ -99,3 +99,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None:
|
|||||||
assert result["title"] == "Downloader"
|
assert result["title"] == "Downloader"
|
||||||
assert result["data"] == {}
|
assert result["data"] == {}
|
||||||
assert result["options"] == {}
|
assert result["options"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None:
|
||||||
|
"""Test import flow."""
|
||||||
|
with patch("os.path.isdir", return_value=False):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_DOWNLOAD_DIR: "download_dir",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "directory_does_not_exist"
|
||||||
|
111
tests/components/downloader/test_init.py
Normal file
111
tests/components/downloader/test_init.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"""Tests for the downloader component init."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.downloader import (
|
||||||
|
CONF_DOWNLOAD_DIR,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_DOWNLOAD_FILE,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_initialization(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the initialization of the downloader component."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_DOWNLOAD_DIR: "/test_dir",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
with patch("os.path.isdir", return_value=True):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE)
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None:
|
||||||
|
"""Test the import of the downloader component."""
|
||||||
|
with patch("os.path.isdir", return_value=True):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_DOWNLOAD_DIR: "/test_dir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"}
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE)
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
issue = issue_registry.async_get_issue(
|
||||||
|
issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN
|
||||||
|
)
|
||||||
|
assert issue
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_directory_missing(
|
||||||
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test the import of the downloader component."""
|
||||||
|
with patch("os.path.isdir", return_value=False):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_DOWNLOAD_DIR: "/test_dir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
issue = issue_registry.async_get_issue(
|
||||||
|
issue_id="deprecated_yaml_downloader", domain=DOMAIN
|
||||||
|
)
|
||||||
|
assert issue
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_already_exists(
|
||||||
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test the import of the downloader component."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_DOWNLOAD_DIR: "/test_dir",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
with patch("os.path.isdir", return_value=True):
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_DOWNLOAD_DIR: "/test_dir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(issue_registry.issues) == 1
|
||||||
|
issue = issue_registry.async_get_issue(
|
||||||
|
issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN
|
||||||
|
)
|
||||||
|
assert issue
|
@ -27,6 +27,7 @@ from homeassistant.components.recorder import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SQLITE_URL_PREFIX,
|
SQLITE_URL_PREFIX,
|
||||||
Recorder,
|
Recorder,
|
||||||
|
db_schema,
|
||||||
get_instance,
|
get_instance,
|
||||||
migration,
|
migration,
|
||||||
pool,
|
pool,
|
||||||
@ -2598,3 +2599,9 @@ async def test_commit_before_commits_pending_writes(
|
|||||||
|
|
||||||
await verify_states_in_queue_future
|
await verify_states_in_queue_future
|
||||||
await verify_session_commit_future
|
await verify_session_commit_future
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_tables_use_default_table_args(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that all tables use the default table args."""
|
||||||
|
for table in db_schema.Base.metadata.tables.values():
|
||||||
|
assert table.kwargs.items() >= db_schema._DEFAULT_TABLE_ARGS.items()
|
||||||
|
79
tests/components/snmp/test_negative_sensor.py
Normal file
79
tests/components/snmp/test_negative_sensor.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""SNMP sensor tests."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pysnmp.hlapi import Integer32
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def hlapi_mock():
|
||||||
|
"""Mock out 3rd party API."""
|
||||||
|
mock_data = Integer32(-13)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.snmp.sensor.getCmd",
|
||||||
|
return_value=(None, None, None, [[mock_data]]),
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
async def test_basic_config(hass: HomeAssistant) -> None:
|
||||||
|
"""Test basic entity configuration."""
|
||||||
|
|
||||||
|
config = {
|
||||||
|
SENSOR_DOMAIN: {
|
||||||
|
"platform": "snmp",
|
||||||
|
"host": "192.168.1.32",
|
||||||
|
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.snmp")
|
||||||
|
assert state.state == "-13"
|
||||||
|
assert state.attributes == {"friendly_name": "SNMP"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_config(hass: HomeAssistant) -> None:
|
||||||
|
"""Test entity configuration."""
|
||||||
|
|
||||||
|
config = {
|
||||||
|
SENSOR_DOMAIN: {
|
||||||
|
# SNMP configuration
|
||||||
|
"platform": "snmp",
|
||||||
|
"host": "192.168.1.32",
|
||||||
|
"baseoid": "1.3.6.1.4.1.2021.10.1.3.1",
|
||||||
|
# Entity configuration
|
||||||
|
"icon": "{{'mdi:one_two_three'}}",
|
||||||
|
"picture": "{{'blabla.png'}}",
|
||||||
|
"device_class": "temperature",
|
||||||
|
"name": "{{'SNMP' + ' ' + 'Sensor'}}",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unique_id": "very_unique",
|
||||||
|
"unit_of_measurement": "°C",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, SENSOR_DOMAIN, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique"
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.snmp_sensor")
|
||||||
|
assert state.state == "-13"
|
||||||
|
assert state.attributes == {
|
||||||
|
"device_class": "temperature",
|
||||||
|
"entity_picture": "blabla.png",
|
||||||
|
"friendly_name": "SNMP Sensor",
|
||||||
|
"icon": "mdi:one_two_three",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "°C",
|
||||||
|
}
|
@ -1672,3 +1672,25 @@ def test_color_hex() -> None:
|
|||||||
|
|
||||||
with pytest.raises(vol.Invalid, match=msg):
|
with pytest.raises(vol.Invalid, match=msg):
|
||||||
cv.color_hex(123456)
|
cv.color_hex(123456)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_script_action_ambiguous():
|
||||||
|
"""Test determine script action with ambiguous actions."""
|
||||||
|
assert (
|
||||||
|
cv.determine_script_action(
|
||||||
|
{
|
||||||
|
"type": "is_power",
|
||||||
|
"condition": "device",
|
||||||
|
"device_id": "9c2bda81bc7997c981f811c32cafdb22",
|
||||||
|
"entity_id": "2ee287ec70dd0c6db187b539bee429b7",
|
||||||
|
"domain": "sensor",
|
||||||
|
"below": "15",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "condition"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_script_action_non_ambiguous():
|
||||||
|
"""Test determine script action with a non ambiguous action."""
|
||||||
|
assert cv.determine_script_action({"delay": "00:00:05"}) == "delay"
|
||||||
|
@ -2288,6 +2288,7 @@ async def test_additional_data_in_core_config(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we can handle additional data in core configuration."""
|
"""Test that we can handle additional data in core configuration."""
|
||||||
config = ha.Config(hass, "/test/ha-config")
|
config = ha.Config(hass, "/test/ha-config")
|
||||||
|
config.async_initialize()
|
||||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"data": {"location_name": "Test Name", "additional_valid_key": "value"},
|
"data": {"location_name": "Test Name", "additional_valid_key": "value"},
|
||||||
@ -2301,6 +2302,7 @@ async def test_incorrect_internal_external_url(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test that we warn when detecting invalid internal/external url."""
|
"""Test that we warn when detecting invalid internal/external url."""
|
||||||
config = ha.Config(hass, "/test/ha-config")
|
config = ha.Config(hass, "/test/ha-config")
|
||||||
|
config.async_initialize()
|
||||||
|
|
||||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@ -2314,6 +2316,7 @@ async def test_incorrect_internal_external_url(
|
|||||||
assert "Invalid internal_url set" not in caplog.text
|
assert "Invalid internal_url set" not in caplog.text
|
||||||
|
|
||||||
config = ha.Config(hass, "/test/ha-config")
|
config = ha.Config(hass, "/test/ha-config")
|
||||||
|
config.async_initialize()
|
||||||
|
|
||||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user