This commit is contained in:
Franck Nijhof 2024-04-05 16:18:02 +02:00 committed by GitHub
commit b1fb77cb4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 433 additions and 112 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"])]
), ),
} }
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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",
}

View File

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

View File

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