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",
"iot_class": "cloud_push",
"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 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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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):
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:
"""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,