mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
2024.4.2 (#115186)
This commit is contained in:
commit
04072cb3c1
@ -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
|
||||
|
@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==60"],
|
||||
"requirements": ["axis==61"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
@ -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.",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"requirements": ["pylitterbot==2023.4.9"]
|
||||
"requirements": ["pylitterbot==2023.4.11"]
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
|
@ -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]:
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2024.4.0"],
|
||||
"requirements": ["velbus-aio==2024.4.1"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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"),
|
||||
[
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
[
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user