mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +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",
|
||||
"iot_class": "cloud_push",
|
||||
"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 functools import partial
|
||||
import logging
|
||||
from time import monotonic
|
||||
|
||||
from aiohttp import ClientError
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||
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)
|
||||
# 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
|
||||
@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
self.pubnub = pubnub
|
||||
self._update_debounce: dict[str, Debouncer] = {}
|
||||
self._update_debounce_jobs: dict[str, HassJob] = {}
|
||||
self._start_time: float | None = None
|
||||
|
||||
@callback
|
||||
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
|
||||
@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Token refresh check and catch up the activity stream."""
|
||||
self._start_time = monotonic()
|
||||
update_debounce = self._update_debounce
|
||||
update_debounce_jobs = self._update_debounce_jobs
|
||||
for house_id in self._house_ids:
|
||||
@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
debouncer = self._update_debounce[house_id]
|
||||
debouncer.async_schedule_call()
|
||||
|
||||
# Schedule two updates past the debounce time
|
||||
# to ensure we catch the case where the activity
|
||||
# api does not update right away and we need to poll
|
||||
# it again. Sometimes the lock operator or a doorbell
|
||||
# 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]
|
||||
for step in (1, 2):
|
||||
future_updates.append(
|
||||
|
@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
|
||||
# Limit battery, online, and hardware updates to hourly
|
||||
# in order to reduce the number of api requests and
|
||||
# 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
|
||||
# doorbell motion and rings are included here
|
||||
|
@ -49,9 +49,17 @@ class AugustSubscriberMixin:
|
||||
"""Call the refresh method."""
|
||||
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
|
||||
def _async_setup_listeners(self) -> None:
|
||||
"""Create interval and stop listeners."""
|
||||
self._async_cancel_update_interval()
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
self._async_scheduled_refresh,
|
||||
@ -59,17 +67,12 @@ class AugustSubscriberMixin:
|
||||
name="august refresh",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_cancel_update_interval(_: Event) -> None:
|
||||
self._stop_interval = None
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
if not self._stop_interval:
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
self._async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_unsubscribe_device_id(
|
||||
@ -82,13 +85,7 @@ class AugustSubscriberMixin:
|
||||
|
||||
if self._subscriptions:
|
||||
return
|
||||
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
if self._stop_interval:
|
||||
self._stop_interval()
|
||||
self._stop_interval = None
|
||||
self._async_cancel_update_interval()
|
||||
|
||||
@callback
|
||||
def async_signal_device_id_update(self, device_id: str) -> None:
|
||||
|
@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
mjpeg_url=self.mjpeg_source,
|
||||
still_image_url=self.image_source,
|
||||
authentication=HTTP_DIGEST_AUTHENTICATION,
|
||||
verify_ssl=False,
|
||||
unique_id=f"{hub.unique_id}-camera",
|
||||
)
|
||||
|
||||
@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
|
||||
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)
|
||||
self._still_image_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/jpg/image.cgi{image_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}"
|
||||
)
|
||||
|
||||
mjpeg_options = self.generate_options()
|
||||
self._mjpeg_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/mjpg/video.cgi{mjpeg_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}"
|
||||
)
|
||||
|
||||
stream_options = self.generate_options(add_video_codec_h264=True)
|
||||
@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
self.hub.additional_diagnostics["camera_sources"] = {
|
||||
"Image": self._still_image_url,
|
||||
"MJPEG": self._mjpeg_url,
|
||||
"Stream": (
|
||||
f"rtsp://user:pass@{self.hub.config.host}/axis-media"
|
||||
f"/media.amp{stream_options}"
|
||||
),
|
||||
"Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"),
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
self, entry_data: Mapping[str, Any], keep_password: bool
|
||||
) -> ConfigFlowResult:
|
||||
"""Re-run configuration step."""
|
||||
protocol = entry_data.get(CONF_PROTOCOL, "http")
|
||||
password = entry_data[CONF_PASSWORD] if keep_password else ""
|
||||
self.discovery_schema = {
|
||||
vol.Required(
|
||||
CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http")
|
||||
): str,
|
||||
vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
|
||||
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
|
||||
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD,
|
||||
default=entry_data[CONF_PASSWORD] if keep_password else "",
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD, default=password): str,
|
||||
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
@ -31,6 +32,7 @@ class AxisConfig:
|
||||
|
||||
entry: ConfigEntry
|
||||
|
||||
protocol: str
|
||||
host: str
|
||||
port: int
|
||||
username: str
|
||||
@ -54,6 +56,7 @@ class AxisConfig:
|
||||
options = config_entry.options
|
||||
return cls(
|
||||
entry=config_entry,
|
||||
protocol=config.get(CONF_PROTOCOL, "http"),
|
||||
host=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
|
@ -11,7 +11,11 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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:
|
||||
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(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
@ -51,28 +62,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
},
|
||||
)
|
||||
|
||||
translation_key = "deprecated_yaml"
|
||||
if (
|
||||
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,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.9.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
},
|
||||
)
|
||||
return True
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="directory_does_not_exist",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
"url": "/config/integrations/dashboard/add?domain=downloader",
|
||||
},
|
||||
)
|
||||
else:
|
||||
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:
|
||||
@ -83,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not os.path.isabs(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(
|
||||
"Download path %s does not exist. File Downloader not active", download_path
|
||||
)
|
||||
|
@ -46,12 +46,16 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""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:
|
||||
"""Validate the user input if the directory exists."""
|
||||
|
@ -37,13 +37,9 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"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": {
|
||||
"directory_does_not_exist": {
|
||||
"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",
|
||||
"integration_type": "system",
|
||||
"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,
|
||||
}
|
||||
|
||||
if config is None:
|
||||
if config is None or "views" not in config:
|
||||
return data
|
||||
|
||||
for idx, view in enumerate(config["views"]):
|
||||
|
@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity):
|
||||
else:
|
||||
brightness = self._prev_brightness
|
||||
self._prev_brightness = brightness
|
||||
args = {"new_level": brightness}
|
||||
args = {"new_level": to_lutron_level(brightness)}
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
|
||||
self._lutron_device.set_level(**args)
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
|
||||
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.const import Platform
|
||||
@ -92,7 +92,7 @@ def create_devices(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=get_system_name(system),
|
||||
manufacturer=get_manufacturer(device),
|
||||
model=device.productName,
|
||||
model=get_model(device),
|
||||
sw_version=device.firmwareCurrent,
|
||||
serial_number=device.product_serial_number,
|
||||
)
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/myuplink",
|
||||
"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_USER_UUID, client.user_uuid),
|
||||
):
|
||||
if entry.data[key] == value:
|
||||
if entry.data.get(key) == value:
|
||||
continue
|
||||
entry_updates["data"][key] = value
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.2"]
|
||||
"requirements": ["opower==0.4.3"]
|
||||
}
|
||||
|
@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS
|
||||
|
||||
@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS_SHORT_TERM
|
||||
|
||||
@ -760,7 +762,10 @@ class StatisticsMeta(Base):
|
||||
class RecorderRuns(Base):
|
||||
"""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
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@ -789,6 +794,7 @@ class MigrationChanges(Base):
|
||||
"""Representation of migration changes."""
|
||||
|
||||
__tablename__ = TABLE_MIGRATION_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||
version: Mapped[int] = mapped_column(SmallInteger)
|
||||
@ -798,6 +804,8 @@ class SchemaChanges(Base):
|
||||
"""Representation of schema version changes."""
|
||||
|
||||
__tablename__ = TABLE_SCHEMA_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
schema_version: Mapped[int | None] = mapped_column(Integer)
|
||||
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@ -816,6 +824,8 @@ class StatisticsRuns(Base):
|
||||
"""Representation of statistics run."""
|
||||
|
||||
__tablename__ = TABLE_STATISTICS_RUNS
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
|
||||
|
||||
|
@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/romy",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["romy==0.0.7"],
|
||||
"requirements": ["romy==0.0.10"],
|
||||
"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_NAME, default="Rova"): cv.string,
|
||||
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",
|
||||
self._baseoid,
|
||||
type(value),
|
||||
bytes(value),
|
||||
value,
|
||||
)
|
||||
if isinstance(value, NoSuchObject):
|
||||
_LOGGER.error(
|
||||
|
@ -10,6 +10,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["systembridgeconnector"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["systembridgeconnector==4.0.3"],
|
||||
"requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"],
|
||||
"zeroconf": ["_system-bridge._tcp.local."]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"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"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 4
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
@ -401,6 +401,7 @@ class HomeAssistant:
|
||||
self.services = ServiceRegistry(self)
|
||||
self.states = StateMachine(self.bus, self.loop)
|
||||
self.config = Config(self, config_dir)
|
||||
self.config.async_initialize()
|
||||
self.components = loader.Components(self)
|
||||
self.helpers = loader.Helpers(self)
|
||||
self.state: CoreState = CoreState.not_running
|
||||
@ -2589,12 +2590,12 @@ class ServiceRegistry:
|
||||
class Config:
|
||||
"""Configuration settings for Home Assistant."""
|
||||
|
||||
_store: Config._ConfigStore
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
||||
"""Initialize a new config object."""
|
||||
self.hass = hass
|
||||
|
||||
self._store = self._ConfigStore(self.hass, config_dir)
|
||||
|
||||
self.latitude: float = 0
|
||||
self.longitude: float = 0
|
||||
|
||||
@ -2645,6 +2646,13 @@ class Config:
|
||||
# If Home Assistant is running in safe mode
|
||||
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:
|
||||
"""Calculate distance from Home Assistant.
|
||||
|
||||
@ -2850,7 +2858,6 @@ class Config:
|
||||
"country": self.country,
|
||||
"language": self.language,
|
||||
}
|
||||
|
||||
await self._store.async_save(data)
|
||||
|
||||
# Circular dependency prevents us from generating the class at top level
|
||||
@ -2860,7 +2867,7 @@ class Config:
|
||||
class _ConfigStore(Store[dict[str, Any]]):
|
||||
"""Class to help storing Config data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize storage class."""
|
||||
super().__init__(
|
||||
hass,
|
||||
@ -2869,7 +2876,6 @@ class Config:
|
||||
private=True,
|
||||
atomic_writes=True,
|
||||
minor_version=CORE_STORAGE_MINOR_VERSION,
|
||||
config_dir=config_dir,
|
||||
)
|
||||
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."""
|
||||
if not (actions := ACTIONS_SET.intersection(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()]
|
||||
|
||||
|
||||
|
@ -95,9 +95,7 @@ async def async_migrator(
|
||||
return config
|
||||
|
||||
|
||||
def get_internal_store_manager(
|
||||
hass: HomeAssistant, config_dir: str | None = None
|
||||
) -> _StoreManager:
|
||||
def get_internal_store_manager(hass: HomeAssistant) -> _StoreManager:
|
||||
"""Get the store manager.
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
return hass.data[STORAGE_MANAGER]
|
||||
|
||||
@ -116,13 +114,13 @@ class _StoreManager:
|
||||
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."""
|
||||
self._hass = hass
|
||||
self._invalidated: set[str] = set()
|
||||
self._files: set[str] | None = None
|
||||
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
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
@ -251,7 +249,6 @@ class Store(Generic[_T]):
|
||||
encoder: type[JSONEncoder] | None = None,
|
||||
minor_version: int = 1,
|
||||
read_only: bool = False,
|
||||
config_dir: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize storage class."""
|
||||
self.version = version
|
||||
@ -268,7 +265,7 @@ class Store(Generic[_T]):
|
||||
self._atomic_writes = atomic_writes
|
||||
self._read_only = read_only
|
||||
self._next_write_time = 0.0
|
||||
self._manager = get_internal_store_manager(hass, config_dir)
|
||||
self._manager = get_internal_store_manager(hass)
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
|
@ -30,7 +30,7 @@ habluetooth==2.4.2
|
||||
hass-nabucasa==0.79.0
|
||||
hassil==1.6.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240403.1
|
||||
home-assistant-frontend==20240404.1
|
||||
home-assistant-intents==2024.4.3
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.4.0"
|
||||
version = "2024.4.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -513,8 +513,6 @@ filterwarnings = [
|
||||
"ignore:invalid escape sequence:SyntaxWarning:.*stringcase",
|
||||
# https://github.com/pyudev/pyudev/pull/466 - >=0.24.0
|
||||
"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
|
||||
"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
|
||||
|
@ -185,7 +185,7 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.4.6
|
||||
aioairzone-cloud==0.4.7
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.7.6
|
||||
@ -1077,7 +1077,7 @@ hole==0.8.0
|
||||
holidays==0.46
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240403.1
|
||||
home-assistant-frontend==20240404.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.4.3
|
||||
@ -1349,7 +1349,7 @@ mutesync==0.0.1
|
||||
mypermobil==0.1.8
|
||||
|
||||
# homeassistant.components.myuplink
|
||||
myuplink==0.5.0
|
||||
myuplink==0.6.0
|
||||
|
||||
# homeassistant.components.nad
|
||||
nad-receiver==0.3.0
|
||||
@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.4.2
|
||||
opower==0.4.3
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1
|
||||
rokuecp==0.19.2
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.7
|
||||
romy==0.0.10
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.8.1
|
||||
@ -2654,6 +2654,9 @@ synology-srm==0.2.0
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgeconnector==4.0.3
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgemodels==4.0.4
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.6.0
|
||||
|
||||
@ -2838,7 +2841,7 @@ watchdog==2.3.1
|
||||
waterfurnace==1.1.0
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.2.17
|
||||
weatherflow4py==0.2.20
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.2
|
||||
|
@ -164,7 +164,7 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.4.6
|
||||
aioairzone-cloud==0.4.7
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.7.6
|
||||
@ -876,7 +876,7 @@ hole==0.8.0
|
||||
holidays==0.46
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240403.1
|
||||
home-assistant-frontend==20240404.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.4.3
|
||||
@ -1088,7 +1088,7 @@ mutesync==0.0.1
|
||||
mypermobil==0.1.8
|
||||
|
||||
# homeassistant.components.myuplink
|
||||
myuplink==0.5.0
|
||||
myuplink==0.6.0
|
||||
|
||||
# homeassistant.components.keenetic_ndms2
|
||||
ndms2-client==0.1.2
|
||||
@ -1176,7 +1176,7 @@ openerz-api==0.3.0
|
||||
openhomedevice==2.2.0
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.4.2
|
||||
opower==0.4.3
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9
|
||||
rokuecp==0.19.2
|
||||
|
||||
# homeassistant.components.romy
|
||||
romy==0.0.7
|
||||
romy==0.0.10
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.8.1
|
||||
@ -2049,6 +2049,9 @@ switchbot-api==2.0.0
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgeconnector==4.0.3
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgemodels==4.0.4
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.6.0
|
||||
|
||||
@ -2185,7 +2188,7 @@ wallbox==0.6.0
|
||||
watchdog==2.3.1
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==0.2.17
|
||||
weatherflow4py==0.2.20
|
||||
|
||||
# homeassistant.components.webmin
|
||||
webmin-xmlrpc==0.0.2
|
||||
|
@ -4,9 +4,11 @@ import datetime
|
||||
from unittest.mock import Mock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from yalexs.pubnub_async import AugustPubNub
|
||||
|
||||
from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME
|
||||
from homeassistant.components.lock import (
|
||||
DOMAIN as LOCK_DOMAIN,
|
||||
STATE_JAMMED,
|
||||
@ -155,7 +157,9 @@ async def test_one_lock_operation(
|
||||
|
||||
|
||||
async def test_one_lock_operation_pubnub_connected(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test lock and unlock operations are async when pubnub is connected."""
|
||||
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
|
||||
@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected(
|
||||
== 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:
|
||||
"""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["data"] == {}
|
||||
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,
|
||||
SQLITE_URL_PREFIX,
|
||||
Recorder,
|
||||
db_schema,
|
||||
get_instance,
|
||||
migration,
|
||||
pool,
|
||||
@ -2598,3 +2599,9 @@ async def test_commit_before_commits_pending_writes(
|
||||
|
||||
await verify_states_in_queue_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):
|
||||
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:
|
||||
"""Test that we can handle additional data in core configuration."""
|
||||
config = ha.Config(hass, "/test/ha-config")
|
||||
config.async_initialize()
|
||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": {"location_name": "Test Name", "additional_valid_key": "value"},
|
||||
@ -2301,6 +2302,7 @@ async def test_incorrect_internal_external_url(
|
||||
) -> None:
|
||||
"""Test that we warn when detecting invalid internal/external url."""
|
||||
config = ha.Config(hass, "/test/ha-config")
|
||||
config.async_initialize()
|
||||
|
||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
@ -2314,6 +2316,7 @@ async def test_incorrect_internal_external_url(
|
||||
assert "Invalid internal_url set" not in caplog.text
|
||||
|
||||
config = ha.Config(hass, "/test/ha-config")
|
||||
config.async_initialize()
|
||||
|
||||
hass_storage[ha.CORE_STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
|
Loading…
x
Reference in New Issue
Block a user