This commit is contained in:
Franck Nijhof 2024-04-08 14:17:03 +02:00 committed by GitHub
commit 04072cb3c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 511 additions and 121 deletions

View File

@ -1249,6 +1249,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123
/tests/components/snapcast/ @luar123
/homeassistant/components/snmp/ @nmaggioni
/tests/components/snmp/ @nmaggioni
/homeassistant/components/snooz/ @AustinBrunkhorst
/tests/components/snooz/ @AustinBrunkhorst
/homeassistant/components/solaredge/ @frenck

View File

@ -26,7 +26,7 @@
"iot_class": "local_push",
"loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==60"],
"requirements": ["axis==61"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"quality_scale": "platinum",
"requirements": ["brother==4.0.2"],
"requirements": ["brother==4.1.0"],
"zeroconf": [
{
"type": "_printer._tcp.local.",

View File

@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["hass_nabucasa"],
"requirements": ["hass-nabucasa==0.79.0"]
"requirements": ["hass-nabucasa==0.78.0"]
}

View File

@ -121,6 +121,7 @@ async def async_setup_entry(
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
)
for device in controller.fibaro_devices[platform]

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/fyta",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["fyta_cli==0.3.3"]
"requirements": ["fyta_cli==0.3.5"]
}

View File

@ -46,35 +46,35 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
translation_key="plant_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
value_fn=PLANT_STATUS.get,
),
FytaSensorEntityDescription(
key="temperature_status",
translation_key="temperature_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
value_fn=PLANT_STATUS.get,
),
FytaSensorEntityDescription(
key="light_status",
translation_key="light_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
value_fn=PLANT_STATUS.get,
),
FytaSensorEntityDescription(
key="moisture_status",
translation_key="moisture_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
value_fn=PLANT_STATUS.get,
),
FytaSensorEntityDescription(
key="salinity_status",
translation_key="salinity_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
value_fn=PLANT_STATUS.get,
),
FytaSensorEntityDescription(
key="temperature",

View File

@ -113,7 +113,11 @@ class HMThermostat(HMDevice, ClimateEntity):
@property
def preset_modes(self):
"""Return a list of available preset modes."""
return [HM_PRESET_MAP[mode] for mode in self._hmdevice.ACTIONNODE]
return [
HM_PRESET_MAP[mode]
for mode in self._hmdevice.ACTIONNODE
if mode in HM_PRESET_MAP
]
@property
def current_humidity(self):

View File

@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.12.2",
"xknxproject==3.7.0",
"xknxproject==3.7.1",
"knx-frontend==2024.1.20.105944"
],
"single_config_entry": true

View File

@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.4.9"]
"requirements": ["pylitterbot==2023.4.11"]
}

View File

@ -35,6 +35,7 @@ LITTER_BOX_STATUS_STATE_MAP = {
LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING,
LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING,
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
LitterBoxStatus.CAT_DETECTED: STATE_DOCKED,
LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED,
LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED,

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nobo_hub",
"integration_type": "hub",
"iot_class": "local_push",
"requirements": ["pynobo==1.8.0"]
"requirements": ["pynobo==1.8.1"]
}

View File

@ -258,7 +258,7 @@ class PrometheusMetrics:
self, entity_id: str, friendly_name: str | None = None
) -> None:
"""Remove labelsets matching the given entity id from all metrics."""
for metric in self._metrics.values():
for metric in list(self._metrics.values()):
for sample in cast(list[prometheus_client.Metric], metric.collect())[
0
].samples:

View File

@ -45,7 +45,7 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN):
except OSError:
errors["base"] = "cannot_connect"
else:
await client.stop()
client.stop()
return self.async_create_entry(title=DEFAULT_TITLE, data=user_input)
return self.async_show_form(
step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors

View File

@ -5,8 +5,19 @@ from __future__ import annotations
import binascii
import logging
from pysnmp.entity import config as cfg
from pysnmp.entity.rfc3413.oneliner import cmdgen
from pysnmp.error import PySnmpError
from pysnmp.hlapi.asyncio import (
CommunityData,
ContextData,
ObjectIdentity,
ObjectType,
SnmpEngine,
Udp6TransportTarget,
UdpTransportTarget,
UsmUserData,
bulkWalkCmd,
isEndOfMib,
)
import voluptuous as vol
from homeassistant.components.device_tracker import (
@ -24,7 +35,13 @@ from .const import (
CONF_BASEOID,
CONF_COMMUNITY,
CONF_PRIV_KEY,
DEFAULT_AUTH_PROTOCOL,
DEFAULT_COMMUNITY,
DEFAULT_PORT,
DEFAULT_PRIV_PROTOCOL,
DEFAULT_TIMEOUT,
DEFAULT_VERSION,
SNMP_VERSIONS,
)
_LOGGER = logging.getLogger(__name__)
@ -40,9 +57,12 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
)
def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None:
async def async_get_scanner(
hass: HomeAssistant, config: ConfigType
) -> SnmpScanner | None:
"""Validate the configuration and return an SNMP scanner."""
scanner = SnmpScanner(config[DOMAIN])
await scanner.async_init()
return scanner if scanner.success_init else None
@ -51,39 +71,75 @@ class SnmpScanner(DeviceScanner):
"""Queries any SNMP capable Access Point for connected devices."""
def __init__(self, config):
"""Initialize the scanner."""
"""Initialize the scanner and test the target device."""
host = config[CONF_HOST]
community = config[CONF_COMMUNITY]
baseoid = config[CONF_BASEOID]
authkey = config.get(CONF_AUTH_KEY)
authproto = DEFAULT_AUTH_PROTOCOL
privkey = config.get(CONF_PRIV_KEY)
privproto = DEFAULT_PRIV_PROTOCOL
self.snmp = cmdgen.CommandGenerator()
try:
# Try IPv4 first.
target = UdpTransportTarget((host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT)
except PySnmpError:
# Then try IPv6.
try:
target = Udp6TransportTarget(
(host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT
)
except PySnmpError as err:
_LOGGER.error("Invalid SNMP host: %s", err)
return
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
if CONF_AUTH_KEY not in config or CONF_PRIV_KEY not in config:
self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY])
if authkey is not None or privkey is not None:
if not authkey:
authproto = "none"
if not privkey:
privproto = "none"
request_args = [
SnmpEngine(),
UsmUserData(
community,
authKey=authkey or None,
privKey=privkey or None,
authProtocol=authproto,
privProtocol=privproto,
),
target,
ContextData(),
]
else:
self.auth = cmdgen.UsmUserData(
config[CONF_COMMUNITY],
config[CONF_AUTH_KEY],
config[CONF_PRIV_KEY],
authProtocol=cfg.usmHMACSHAAuthProtocol,
privProtocol=cfg.usmAesCfb128Protocol,
)
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
self.last_results = []
request_args = [
SnmpEngine(),
CommunityData(community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION]),
target,
ContextData(),
]
# Test the router is accessible
data = self.get_snmp_data()
self.request_args = request_args
self.baseoid = baseoid
self.last_results = []
self.success_init = False
async def async_init(self):
"""Make a one-off read to check if the target device is reachable and readable."""
data = await self.async_get_snmp_data()
self.success_init = data is not None
def scan_devices(self):
async def async_scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
await self._async_update_info()
return [client["mac"] for client in self.last_results if client.get("mac")]
def get_device_name(self, device):
async def async_get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
# We have no names
return None
def _update_info(self):
async def _async_update_info(self):
"""Ensure the information from the device is up to date.
Return boolean if scanning successful.
@ -91,38 +147,42 @@ class SnmpScanner(DeviceScanner):
if not self.success_init:
return False
if not (data := self.get_snmp_data()):
if not (data := await self.async_get_snmp_data()):
return False
self.last_results = data
return True
def get_snmp_data(self):
async def async_get_snmp_data(self):
"""Fetch MAC addresses from access point via SNMP."""
devices = []
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
self.auth, self.host, self.baseoid
walker = bulkWalkCmd(
*self.request_args,
0,
50,
ObjectType(ObjectIdentity(self.baseoid)),
lexicographicMode=False,
)
async for errindication, errstatus, errindex, res in walker:
if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication)
return
if errstatus:
_LOGGER.error(
"SNMP error: %s at %s",
errstatus.prettyPrint(),
errindex and res[int(errindex) - 1][0] or "?",
)
return
if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication)
return
if errstatus:
_LOGGER.error(
"SNMP error: %s at %s",
errstatus.prettyPrint(),
errindex and restable[int(errindex) - 1][0] or "?",
)
return
for resrow in restable:
for _, val in resrow:
try:
mac = binascii.hexlify(val.asOctets()).decode("utf-8")
except AttributeError:
continue
_LOGGER.debug("Found MAC address: %s", mac)
mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)])
devices.append({"mac": mac})
for _oid, value in res:
if not isEndOfMib(res):
try:
mac = binascii.hexlify(value.asOctets()).decode("utf-8")
except AttributeError:
continue
_LOGGER.debug("Found MAC address: %s", mac)
mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)])
devices.append({"mac": mac})
return devices

View File

@ -1,7 +1,7 @@
{
"domain": "snmp",
"name": "SNMP",
"codeowners": [],
"codeowners": ["@nmaggioni"],
"documentation": "https://www.home-assistant.io/integrations/snmp",
"iot_class": "local_polling",
"loggers": ["pyasn1", "pysmi", "pysnmp"],

View File

@ -116,7 +116,7 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.security)
return bool(self._api.security) and super().available
@property
def extra_state_attributes(self) -> dict[str, str]:

View File

@ -108,7 +108,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
@property
def available(self) -> bool:
"""Return the availability of the camera."""
return self.camera_data.is_enabled and self.coordinator.last_update_success
return self.camera_data.is_enabled and super().available
@property
def is_recording(self) -> bool:

View File

@ -286,18 +286,7 @@ class SynoApi:
async def async_update(self) -> None:
"""Update function for updating API information."""
try:
await self._update()
except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
LOGGER.debug(
"Connection error during update of '%s' with exception: %s",
self._entry.unique_id,
err,
)
LOGGER.warning(
"Connection error during update, fallback by reloading the entry"
)
await self._hass.config_entries.async_reload(self._entry.entry_id)
await self._update()
async def _update(self) -> None:
"""Update function for updating API information."""

View File

@ -366,7 +366,7 @@ class SynoDSMUtilSensor(SynoDSMSensor):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.utilisation)
return bool(self._api.utilisation) and super().available
class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor):

View File

@ -98,7 +98,7 @@ class SynoDSMSurveillanceHomeModeToggle(
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.surveillance_station)
return bool(self._api.surveillance_station) and super().available
@property
def device_info(self) -> DeviceInfo:

View File

@ -59,7 +59,7 @@ class SynoDSMUpdateEntity(
@property
def available(self) -> bool:
"""Return True if entity is available."""
return bool(self._api.upgrade)
return bool(self._api.upgrade) and super().available
@property
def installed_version(self) -> str | None:

View File

@ -325,12 +325,12 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._end = start + self._remaining
self.async_write_ha_state()
self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id})
self._listener = async_track_point_in_utc_time(
self.hass, self._async_finished, self._end
)
self.async_write_ha_state()
@callback
def async_change(self, duration: timedelta) -> None:
@ -351,11 +351,11 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._listener()
self._end += duration
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
self.async_write_ha_state()
self.hass.bus.async_fire(EVENT_TIMER_CHANGED, {ATTR_ENTITY_ID: self.entity_id})
self._listener = async_track_point_in_utc_time(
self.hass, self._async_finished, self._end
)
self.async_write_ha_state()
@callback
def async_pause(self) -> None:
@ -368,8 +368,8 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
self._state = STATUS_PAUSED
self._end = None
self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id})
self.async_write_ha_state()
self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id})
@callback
def async_cancel(self) -> None:
@ -381,10 +381,10 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._end = None
self._remaining = None
self._running_duration = self._configured_duration
self.async_write_ha_state()
self.hass.bus.async_fire(
EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id}
)
self.async_write_ha_state()
@callback
def async_finish(self) -> None:
@ -400,11 +400,11 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._end = None
self._remaining = None
self._running_duration = self._configured_duration
self.async_write_ha_state()
self.hass.bus.async_fire(
EVENT_TIMER_FINISHED,
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
)
self.async_write_ha_state()
@callback
def _async_finished(self, time: datetime) -> None:
@ -418,11 +418,11 @@ class Timer(collection.CollectionEntity, RestoreEntity):
self._end = None
self._remaining = None
self._running_duration = self._configured_duration
self.async_write_ha_state()
self.hass.bus.async_fire(
EVENT_TIMER_FINISHED,
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
)
self.async_write_ha_state()
async def async_update_config(self, config: ConfigType) -> None:
"""Handle when the config is updated."""

View File

@ -578,7 +578,13 @@ class UtilityMeterSensor(RestoreSensor):
async def async_reset_meter(self, entity_id):
"""Reset meter."""
if self._tariff is not None and self._tariff_entity != entity_id:
if self._tariff_entity is not None and self._tariff_entity != entity_id:
return
if (
self._tariff_entity is None
and entity_id is not None
and self.entity_id != entity_id
):
return
_LOGGER.debug("Reset utility meter <%s>", self.entity_id)
self._last_reset = dt_util.utcnow()

View File

@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
"requirements": ["velbus-aio==2024.4.0"],
"requirements": ["velbus-aio==2024.4.1"],
"usb": [
{
"vid": "10CF",

View File

@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink",
"iot_class": "cloud_push",
"requirements": ["yolink-api==0.4.1"]
"requirements": ["yolink-api==0.4.2"]
}

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 = "1"
PATCH_VERSION: Final = "2"
__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

@ -631,7 +631,16 @@ class EntityPlatform:
if (
(self.config_entry and self.config_entry.pref_disable_polling)
or self._async_unsub_polling is not None
or not any(entity.should_poll for entity in entities)
or not any(
# Entity may have failed to add or called `add_to_platform_abort`
# so we check if the entity is in self.entities before
# checking `entity.should_poll` since `should_poll` may need to
# check `self.hass` which will be `None` if the entity did not add
entity.entity_id
and entity.entity_id in self.entities
and entity.should_poll
for entity in entities
)
):
return

View File

@ -286,6 +286,9 @@ STATIC_VALIDATION_ACTION_TYPES = (
cv.SCRIPT_ACTION_WAIT_TEMPLATE,
)
REPEAT_WARN_ITERATIONS = 5000
REPEAT_TERMINATE_ITERATIONS = 10000
async def async_validate_actions_config(
hass: HomeAssistant, actions: list[ConfigType]
@ -846,6 +849,7 @@ class _ScriptRun:
# pylint: disable-next=protected-access
script = self._script._get_repeat_script(self._step)
warned_too_many_loops = False
async def async_run_sequence(iteration, extra_msg=""):
self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg)
@ -916,6 +920,36 @@ class _ScriptRun:
_LOGGER.warning("Error in 'while' evaluation:\n%s", ex)
break
if iteration > 1:
if iteration > REPEAT_WARN_ITERATIONS:
if not warned_too_many_loops:
warned_too_many_loops = True
_LOGGER.warning(
"While condition %s in script `%s` looped %s times",
repeat[CONF_WHILE],
self._script.name,
REPEAT_WARN_ITERATIONS,
)
if iteration > REPEAT_TERMINATE_ITERATIONS:
_LOGGER.critical(
"While condition %s in script `%s` "
"terminated because it looped %s times",
repeat[CONF_WHILE],
self._script.name,
REPEAT_TERMINATE_ITERATIONS,
)
raise _AbortScript(
f"While condition {repeat[CONF_WHILE]} "
"terminated because it looped "
f" {REPEAT_TERMINATE_ITERATIONS} times"
)
# If the user creates a script with a tight loop,
# yield to the event loop so the system stays
# responsive while all the cpu time is consumed.
await asyncio.sleep(0)
await async_run_sequence(iteration)
elif CONF_UNTIL in repeat:
@ -934,6 +968,35 @@ class _ScriptRun:
_LOGGER.warning("Error in 'until' evaluation:\n%s", ex)
break
if iteration >= REPEAT_WARN_ITERATIONS:
if not warned_too_many_loops:
warned_too_many_loops = True
_LOGGER.warning(
"Until condition %s in script `%s` looped %s times",
repeat[CONF_UNTIL],
self._script.name,
REPEAT_WARN_ITERATIONS,
)
if iteration >= REPEAT_TERMINATE_ITERATIONS:
_LOGGER.critical(
"Until condition %s in script `%s` "
"terminated because it looped %s times",
repeat[CONF_UNTIL],
self._script.name,
REPEAT_TERMINATE_ITERATIONS,
)
raise _AbortScript(
f"Until condition {repeat[CONF_UNTIL]} "
"terminated because it looped "
f"{REPEAT_TERMINATE_ITERATIONS} times"
)
# If the user creates a script with a tight loop,
# yield to the event loop so the system stays responsive
# while all the cpu time is consumed.
await asyncio.sleep(0)
if saved_repeat_vars:
self._variables["repeat"] = saved_repeat_vars
else:

View File

@ -27,7 +27,7 @@ fnv-hash-fast==0.5.0
ha-av==10.1.1
ha-ffmpeg==3.2.0
habluetooth==2.4.2
hass-nabucasa==0.79.0
hass-nabucasa==0.78.0
hassil==1.6.1
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240404.1

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.4.1"
version = "2024.4.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@ -38,7 +38,7 @@ dependencies = [
"fnv-hash-fast==0.5.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==0.79.0",
"hass-nabucasa==0.78.0",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.27.0",

View File

@ -16,7 +16,7 @@ bcrypt==4.1.2
certifi>=2021.5.30
ciso8601==2.3.1
fnv-hash-fast==0.5.0
hass-nabucasa==0.79.0
hass-nabucasa==0.78.0
httpx==0.27.0
home-assistant-bluetooth==1.12.0
ifaddr==0.2.0

View File

@ -514,7 +514,7 @@ aurorapy==0.2.7
# avion==0.10
# homeassistant.components.axis
axis==60
axis==61
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -609,7 +609,7 @@ bring-api==0.5.7
broadlink==0.18.3
# homeassistant.components.brother
brother==4.0.2
brother==4.1.0
# homeassistant.components.brottsplatskartan
brottsplatskartan==1.0.5
@ -899,7 +899,7 @@ freesms==0.2.0
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.3.3
fyta_cli==0.3.5
# homeassistant.components.google_translate
gTTS==2.2.4
@ -1037,7 +1037,7 @@ habitipy==0.2.0
habluetooth==2.4.2
# homeassistant.components.cloud
hass-nabucasa==0.79.0
hass-nabucasa==0.78.0
# homeassistant.components.splunk
hass-splunk==0.1.1
@ -1943,7 +1943,7 @@ pylibrespot-java==0.1.1
pylitejet==0.6.2
# homeassistant.components.litterrobot
pylitterbot==2023.4.9
pylitterbot==2023.4.11
# homeassistant.components.lutron_caseta
pylutron-caseta==0.20.0
@ -1991,7 +1991,7 @@ pynetgear==0.10.10
pynetio==0.1.9.1
# homeassistant.components.nobo_hub
pynobo==1.8.0
pynobo==1.8.1
# homeassistant.components.nuki
pynuki==1.6.3
@ -2798,7 +2798,7 @@ vallox-websocket-api==5.1.1
vehicle==2.2.1
# homeassistant.components.velbus
velbus-aio==2024.4.0
velbus-aio==2024.4.1
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -2880,7 +2880,7 @@ xiaomi-ble==0.28.0
xknx==2.12.2
# homeassistant.components.knx
xknxproject==3.7.0
xknxproject==3.7.1
# homeassistant.components.bluesound
# homeassistant.components.fritz
@ -2910,7 +2910,7 @@ yeelight==0.7.14
yeelightsunflower==0.0.10
# homeassistant.components.yolink
yolink-api==0.4.1
yolink-api==0.4.2
# homeassistant.components.youless
youless-api==1.0.1

View File

@ -454,7 +454,7 @@ auroranoaa==0.0.3
aurorapy==0.2.7
# homeassistant.components.axis
axis==60
axis==61
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@ -520,7 +520,7 @@ bring-api==0.5.7
broadlink==0.18.3
# homeassistant.components.brother
brother==4.0.2
brother==4.1.0
# homeassistant.components.brottsplatskartan
brottsplatskartan==1.0.5
@ -731,7 +731,7 @@ freebox-api==1.1.0
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.3.3
fyta_cli==0.3.5
# homeassistant.components.google_translate
gTTS==2.2.4
@ -848,7 +848,7 @@ habitipy==0.2.0
habluetooth==2.4.2
# homeassistant.components.cloud
hass-nabucasa==0.79.0
hass-nabucasa==0.78.0
# homeassistant.components.conversation
hassil==1.6.1
@ -1509,7 +1509,7 @@ pylibrespot-java==0.1.1
pylitejet==0.6.2
# homeassistant.components.litterrobot
pylitterbot==2023.4.9
pylitterbot==2023.4.11
# homeassistant.components.lutron_caseta
pylutron-caseta==0.20.0
@ -1545,7 +1545,7 @@ pymysensors==0.24.0
pynetgear==0.10.10
# homeassistant.components.nobo_hub
pynobo==1.8.0
pynobo==1.8.1
# homeassistant.components.nuki
pynuki==1.6.3
@ -2154,7 +2154,7 @@ vallox-websocket-api==5.1.1
vehicle==2.2.1
# homeassistant.components.velbus
velbus-aio==2024.4.0
velbus-aio==2024.4.1
# homeassistant.components.venstar
venstarcolortouch==0.19
@ -2224,7 +2224,7 @@ xiaomi-ble==0.28.0
xknx==2.12.2
# homeassistant.components.knx
xknxproject==3.7.0
xknxproject==3.7.1
# homeassistant.components.bluesound
# homeassistant.components.fritz
@ -2248,7 +2248,7 @@ yalexs==2.0.0
yeelight==0.7.14
# homeassistant.components.yolink
yolink-api==0.4.1
yolink-api==0.4.2
# homeassistant.components.youless
youless-api==1.0.1

View File

@ -39,6 +39,8 @@ def run_download_docker():
CORE_PROJECT_ID,
"--original-filenames=false",
"--replace-breaks=false",
"--filter-data",
"nonfuzzy",
"--export-empty-as",
"skip",
"--format",

View File

@ -33,6 +33,7 @@ ROBOT_4_DATA = {
"wifiRssi": -53.0,
"unitPowerType": "AC",
"catWeight": 12.0,
"displayCode": "DC_MODE_IDLE",
"unitTimezone": "America/New_York",
"unitTime": None,
"cleanCycleWaitTime": 15,
@ -66,7 +67,7 @@ ROBOT_4_DATA = {
"isDFIResetPending": False,
"DFINumberOfCycles": 104,
"DFILevelPercent": 76,
"isDFIFull": True,
"isDFIFull": False,
"DFIFullCounter": 3,
"DFITriggerCount": 42,
"litterLevel": 460,

View File

@ -86,7 +86,7 @@ async def test_litter_robot_sensor(
assert sensor.state == "2022-09-17T12:06:37+00:00"
assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
sensor = hass.states.get("sensor.test_status_code")
assert sensor.state == "dfs"
assert sensor.state == "rdy"
assert sensor.attributes["device_class"] == SensorDeviceClass.ENUM
sensor = hass.states.get("sensor.test_litter_level")
assert sensor.state == "70.0"

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from pylitterbot import Robot
import pytest
from homeassistant.components.litterrobot import DOMAIN
@ -16,6 +17,7 @@ from homeassistant.components.vacuum import (
SERVICE_STOP,
STATE_DOCKED,
STATE_ERROR,
STATE_PAUSED,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
@ -96,6 +98,30 @@ async def test_vacuum_with_error(
assert vacuum.state == STATE_ERROR
@pytest.mark.parametrize(
("robot_data", "expected_state"),
[
({"displayCode": "DC_CAT_DETECT"}, STATE_DOCKED),
({"isDFIFull": True}, STATE_ERROR),
({"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, STATE_PAUSED),
],
)
async def test_vacuum_states(
hass: HomeAssistant,
mock_account_with_litterrobot_4: MagicMock,
robot_data: dict[str, str | bool],
expected_state: str,
) -> None:
"""Test sending commands to the switch."""
await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN)
robot: Robot = mock_account_with_litterrobot_4.robots[0]
robot._update_data(robot_data, partial=True)
vacuum = hass.states.get(VACUUM_ENTITY_ID)
assert vacuum
assert vacuum.state == expected_state
@pytest.mark.parametrize(
("service", "command", "extra"),
[

View File

@ -45,7 +45,7 @@ from homeassistant.const import (
EVENT_STATE_CHANGED,
SERVICE_RELOAD,
)
from homeassistant.core import Context, CoreState, HomeAssistant, State
from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.restore_state import StoredState, async_get
@ -156,11 +156,12 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
assert state
assert state.state == STATUS_IDLE
results = []
results: list[tuple[Event, str]] = []
def fake_event_listener(event):
@callback
def fake_event_listener(event: Event):
"""Fake event listener for trigger."""
results.append(event)
results.append((event, hass.states.get("timer.test1").state))
hass.bus.async_listen(EVENT_TIMER_STARTED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_RESTARTED, fake_event_listener)
@ -262,7 +263,10 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
if step["event"] is not None:
expected_events += 1
assert results[-1].event_type == step["event"]
last_result = results[-1]
event, state = last_result
assert event.event_type == step["event"]
assert state == step["state"]
assert len(results) == expected_events
@ -404,6 +408,7 @@ async def test_wait_till_timer_expires(hass: HomeAssistant) -> None:
results = []
@callback
def fake_event_listener(event):
"""Fake event listener for trigger."""
results.append(event)
@ -580,6 +585,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
results = []
@callback
def fake_event_listener(event):
"""Fake event listener for trigger."""
results.append(event)
@ -647,6 +653,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None:
results = []
@callback
def fake_event_listener(event):
"""Fake event listener for trigger."""
results.append(event)

View File

@ -983,6 +983,139 @@ async def test_service_reset_no_tariffs(
assert state.attributes.get("last_period") == "3"
@pytest.mark.parametrize(
("yaml_config", "config_entry_configs"),
[
(
{
"utility_meter": {
"energy_bill": {
"source": "sensor.energy",
},
"water_bill": {
"source": "sensor.water",
},
},
},
None,
),
(
None,
[
{
"cycle": "none",
"delta_values": False,
"name": "Energy bill",
"net_consumption": False,
"offset": 0,
"periodically_resetting": True,
"source": "sensor.energy",
"tariffs": [],
},
{
"cycle": "none",
"delta_values": False,
"name": "Water bill",
"net_consumption": False,
"offset": 0,
"periodically_resetting": True,
"source": "sensor.water",
"tariffs": [],
},
],
),
],
)
async def test_service_reset_no_tariffs_correct_with_multi(
hass: HomeAssistant, yaml_config, config_entry_configs
) -> None:
"""Test complex utility sensor service reset for multiple sensors with no tarrifs.
See GitHub issue #114864: Service "utility_meter.reset" affects all meters.
"""
# Home assistant is not runnit yet
hass.state = CoreState.not_running
last_reset = "2023-10-01T00:00:00+00:00"
mock_restore_cache_with_extra_data(
hass,
[
(
State(
"sensor.energy_bill",
"3",
attributes={
ATTR_LAST_RESET: last_reset,
},
),
{},
),
(
State(
"sensor.water_bill",
"6",
attributes={
ATTR_LAST_RESET: last_reset,
},
),
{},
),
],
)
if yaml_config:
assert await async_setup_component(hass, DOMAIN, yaml_config)
await hass.async_block_till_done()
else:
for entry in config_entry_configs:
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options=entry,
title=entry["name"],
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill")
assert state
assert state.state == "3"
assert state.attributes.get("last_reset") == last_reset
assert state.attributes.get("last_period") == "0"
state = hass.states.get("sensor.water_bill")
assert state
assert state.state == "6"
assert state.attributes.get("last_reset") == last_reset
assert state.attributes.get("last_period") == "0"
now = dt_util.utcnow()
with freeze_time(now):
await hass.services.async_call(
domain=DOMAIN,
service=SERVICE_RESET,
service_data={},
target={"entity_id": "sensor.energy_bill"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energy_bill")
assert state
assert state.state == "0"
assert state.attributes.get("last_reset") == now.isoformat()
assert state.attributes.get("last_period") == "3"
state = hass.states.get("sensor.water_bill")
assert state
assert state.state == "6"
assert state.attributes.get("last_reset") == last_reset
assert state.attributes.get("last_period") == "0"
@pytest.mark.parametrize(
("yaml_config", "config_entry_config"),
[

View File

@ -5,7 +5,7 @@ from collections.abc import Iterable
from datetime import timedelta
import logging
from typing import Any
from unittest.mock import ANY, Mock, patch
from unittest.mock import ANY, AsyncMock, Mock, patch
import pytest
@ -78,6 +78,40 @@ async def test_polling_only_updates_entities_it_should_poll(
assert poll_ent.async_update.called
async def test_polling_check_works_if_entity_add_fails(
hass: HomeAssistant,
) -> None:
"""Test the polling check works if an entity add fails."""
component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20))
await component.async_setup({})
class MockEntityNeedsSelfHassInShouldPoll(MockEntity):
"""Mock entity that needs self.hass in should_poll."""
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled."""
return self.hass.data is not None
working_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True)
working_poll_ent.async_update = AsyncMock()
broken_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True)
broken_poll_ent.async_update = AsyncMock(side_effect=Exception("Broken"))
await component.async_add_entities(
[broken_poll_ent, working_poll_ent], update_before_add=True
)
working_poll_ent.async_update.reset_mock()
broken_poll_ent.async_update.reset_mock()
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20))
await hass.async_block_till_done(wait_background_tasks=True)
assert not broken_poll_ent.async_update.called
assert working_poll_ent.async_update.called
async def test_polling_disabled_by_config_entry(hass: HomeAssistant) -> None:
"""Test the polling of only updated entities."""
entity_platform = MockEntityPlatform(hass)

View File

@ -2837,6 +2837,58 @@ async def test_repeat_nested(
assert_action_trace(expected_trace)
@pytest.mark.parametrize(
("condition", "check"), [("while", "above"), ("until", "below")]
)
async def test_repeat_limits(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str, check: str
) -> None:
"""Test limits on repeats prevent the system from hanging."""
event = "test_event"
events = async_capture_events(hass, event)
hass.states.async_set("sensor.test", "0.5")
sequence = {
"repeat": {
"sequence": [
{
"event": event,
},
],
}
}
sequence["repeat"][condition] = {
"condition": "numeric_state",
"entity_id": "sensor.test",
check: "0",
}
with (
patch.object(script, "REPEAT_WARN_ITERATIONS", 5),
patch.object(script, "REPEAT_TERMINATE_ITERATIONS", 10),
):
script_obj = script.Script(
hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain"
)
caplog.clear()
caplog.set_level(logging.WARNING)
hass.async_create_task(script_obj.async_run(context=Context()))
await asyncio.wait_for(hass.async_block_till_done(), 1)
title_condition = condition.title()
assert f"{title_condition} condition" in caplog.text
assert f"in script `Test {condition}` looped 5 times" in caplog.text
assert (
f"script `Test {condition}` terminated because it looped 10 times"
in caplog.text
)
assert len(events) == 10
async def test_choose_warning(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: