diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a0ff4930b51..5e1b53c535e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -71,9 +71,6 @@ from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime from .config import AutomationConfig, async_validate_config_item - -# Not used except by packages to check config structure -from .config import PLATFORM_SCHEMA # noqa: F401 from .const import ( CONF_ACTION, CONF_INITIAL_STATE, diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 83076778b91..e852b6cc4c0 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -37,6 +37,8 @@ from .helpers import async_get_blueprints # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any +PACKAGE_MERGE_HINT = "list" + _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index faf2be23164..d7a34564b43 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -81,12 +81,12 @@ def _retrieve_max_kb_s_received_state(status: FritzStatus, last_value: str) -> f def _retrieve_gb_sent_state(status: FritzStatus, last_value: str) -> float: """Return upload total data.""" - return round(status.bytes_sent * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_sent / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] def _retrieve_gb_received_state(status: FritzStatus, last_value: str) -> float: """Return download total data.""" - return round(status.bytes_received * 8 / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] + return round(status.bytes_received / 1000 / 1000 / 1000, 1) # type: ignore[no-any-return] class SensorData(TypedDict, total=False): diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 10eb6553dbd..da17bef7159 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -13,7 +13,6 @@ from fritzconnection.core.exceptions import ( FritzSecurityError, FritzServiceError, ) -import slugify as unicode_slug import xmltodict from homeassistant.components.network import async_get_source_ip @@ -248,10 +247,18 @@ def wifi_entities_list( ) if network_info: ssid = network_info["NewSSID"] - if unicode_slug.slugify(ssid, lowercase=False) in networks.values(): + _LOGGER.debug("SSID from device: <%s>", ssid) + if ( + slugify( + ssid, + ) + in [slugify(v) for v in networks.values()] + ): + _LOGGER.debug("SSID duplicated, adding suffix") networks[i] = f'{ssid} {std_table[network_info["NewStandard"]]}' else: networks[i] = ssid + _LOGGER.debug("SSID normalized: <%s>", networks[i]) return [ FritzBoxWifiSwitch(fritzbox_tools, device_friendly_name, net, network_name) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 49ba1103ac5..772143e7c1f 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -228,17 +228,17 @@ class HomeAccessory(Accessory): self.config = config or {} domain = split_entity_id(entity_id)[0].replace("_", " ") - if ATTR_MANUFACTURER in self.config: + if self.config.get(ATTR_MANUFACTURER) is not None: manufacturer = self.config[ATTR_MANUFACTURER] - elif ATTR_INTEGRATION in self.config: + elif self.config.get(ATTR_INTEGRATION) is not None: manufacturer = self.config[ATTR_INTEGRATION].replace("_", " ").title() else: manufacturer = f"{MANUFACTURER} {domain}".title() - if ATTR_MODEL in self.config: + if self.config.get(ATTR_MODEL) is not None: model = self.config[ATTR_MODEL] else: model = domain.title() - if ATTR_SOFTWARE_VERSION in self.config: + if self.config.get(ATTR_SOFTWARE_VERSION) is not None: sw_version = format_sw_version(self.config[ATTR_SOFTWARE_VERSION]) else: sw_version = __version__ diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 381110a4e79..ef9dadff287 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -57,6 +57,8 @@ VALVE_TYPE = { ACTIVATE_ONLY_SWITCH_DOMAINS = {"scene", "script"} +ACTIVATE_ONLY_RESET_SECONDS = 10 + @TYPES.register("Outlet") class Outlet(HomeAccessory): @@ -141,7 +143,7 @@ class Switch(HomeAccessory): self.async_call_service(self._domain, service, params) if self.activate_only: - async_call_later(self.hass, 1, self.reset_switch) + async_call_later(self.hass, ACTIVATE_ONLY_RESET_SECONDS, self.reset_switch) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index a22499fb062..facf79a7bd7 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.7.2"], + "requirements": ["pylitterbot==2021.8.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fb3b31764f8..1b46632051e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -204,6 +205,8 @@ class SAJsensor(SensorEntity): """Return the device class the sensor belongs to.""" if self.unit_of_measurement == POWER_WATT: return DEVICE_CLASS_POWER + if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: + return DEVICE_CLASS_ENERGY if ( self.unit_of_measurement == TEMP_CELSIUS or self._sensor.unit == TEMP_FAHRENHEIT diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 6993b7181e1..44b739e84c7 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -39,6 +39,8 @@ from .const import ( ) from .helpers import async_get_blueprints +PACKAGE_MERGE_HINT = "dict" + SCRIPT_ENTITY_SCHEMA = make_script_schema( { vol.Optional(CONF_ALIAS): cv.string, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index a1ce2e671d1..0d23f5abffc 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -285,7 +285,6 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): self._unit: None | str | Callable[[dict], str] = unit self._unique_id: str = f"{super().unique_id}-{self.attribute}" self._name = get_entity_name(wrapper.device, block, self.description.name) - self._last_value: str | None = None @property def unique_id(self) -> str: diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 56e4f63bc75..07e4f4a4fe3 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,9 +1,12 @@ """Sensor for Shelly.""" from __future__ import annotations -from datetime import datetime +from datetime import timedelta +import logging from typing import Final, cast +import aioshelly + from homeassistant.components import sensor from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -23,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt +from . import ShellyDeviceWrapper from .const import LAST_RESET_NEVER, LAST_RESET_UPTIME, SHAIR_MAX_WORK_HOURS from .entity import ( BlockAttributeDescription, @@ -35,6 +39,8 @@ from .entity import ( ) from .utils import get_device_uptime, temperature_unit +_LOGGER: Final = logging.getLogger(__name__) + SENSORS: Final = { ("device", "battery"): BlockAttributeDescription( name="Battery", @@ -255,9 +261,39 @@ async def async_setup_entry( class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + description: BlockAttributeDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block, attribute, description) + self._last_value: float | None = None + + if description.last_reset == LAST_RESET_NEVER: + self._attr_last_reset = dt.utc_from_timestamp(0) + elif description.last_reset == LAST_RESET_UPTIME: + self._attr_last_reset = ( + dt.utcnow() - timedelta(seconds=wrapper.device.status["uptime"]) + ).replace(second=0, microsecond=0) + @property def state(self) -> StateType: """Return value of sensor.""" + if ( + self.description.last_reset == LAST_RESET_UPTIME + and self.attribute_value is not None + ): + value = cast(float, self.attribute_value) + + if self._last_value and self._last_value > value: + self._attr_last_reset = dt.utcnow().replace(second=0, microsecond=0) + _LOGGER.info("Energy reset detected for entity %s", self.name) + + self._last_value = value + return self.attribute_value @property @@ -265,20 +301,6 @@ class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """State class of sensor.""" return self.description.state_class - @property - def last_reset(self) -> datetime | None: - """State class of sensor.""" - if self.description.last_reset == LAST_RESET_UPTIME: - self._last_value = get_device_uptime( - self.wrapper.device.status, self._last_value - ) - return dt.parse_datetime(self._last_value) - - if self.description.last_reset == LAST_RESET_NEVER: - return dt.utc_from_timestamp(0) - - return None - @property def unit_of_measurement(self) -> str | None: """Return unit of sensor.""" diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 007f40a6d0a..165420bf404 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -13,6 +13,8 @@ from homeassistant.helpers.trigger import async_validate_trigger_config from . import binary_sensor as binary_sensor_platform, sensor as sensor_platform from .const import CONF_TRIGGER, DOMAIN +PACKAGE_MERGE_HINT = "list" + CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ee1e9a8e1ab..7b3cfa1fefd 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.33.2"], + "requirements": ["zeroconf==0.33.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fa117a3f1ff..5200c0a8b31 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,11 +9,11 @@ "pyserial-asyncio==0.5", "zha-quirks==0.0.59", "zigpy-cc==0.5.2", - "zigpy-deconz==0.12.0", + "zigpy-deconz==0.12.1", "zigpy==0.36.1", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.5.2" + "zigpy-znp==0.5.3" ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/homeassistant/config.py b/homeassistant/config.py index e7b6e04e8cf..12a39ab291b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -723,7 +723,22 @@ async def merge_packages_config( _log_pkg_error(pack_name, comp_name, config, str(ex)) continue - merge_list = hasattr(component, "PLATFORM_SCHEMA") + try: + config_platform: ModuleType | None = integration.get_platform("config") + # Test if config platform has a config validator + if not hasattr(config_platform, "async_validate_config"): + config_platform = None + except ImportError: + config_platform = None + + merge_list = False + + # If integration has a custom config validator, it needs to provide a hint. + if config_platform is not None: + merge_list = config_platform.PACKAGE_MERGE_HINT == "list" # type: ignore[attr-defined] + + if not merge_list: + merge_list = hasattr(component, "PLATFORM_SCHEMA") if not merge_list and hasattr(component, "CONFIG_SCHEMA"): merge_list = _identify_config_schema(component) == "list" diff --git a/homeassistant/const.py b/homeassistant/const.py index 65ae1a569c3..b8134905927 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 8 -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, 8, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc4bb9d72ac..d435f165f61 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ sqlalchemy==1.4.17 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.33.2 +zeroconf==0.33.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 11fa22976fd..248a33ee293 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1562,7 +1562,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.7.2 +pylitterbot==2021.8.0 # homeassistant.components.loopenergy pyloopenergy==0.2.1 @@ -2439,7 +2439,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 @@ -2454,7 +2454,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.12.0 +zigpy-deconz==0.12.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2463,7 +2463,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.2 +zigpy-znp==0.5.3 # homeassistant.components.zha zigpy==0.36.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fea89dc8b57..0ec7f8b25f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.7.2 +pylitterbot==2021.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 @@ -1341,7 +1341,7 @@ youless-api==0.10 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.33.2 +zeroconf==0.33.3 # homeassistant.components.zha zha-quirks==0.0.59 @@ -1350,7 +1350,7 @@ zha-quirks==0.0.59 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.12.0 +zigpy-deconz==0.12.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1359,7 +1359,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.5.2 +zigpy-znp==0.5.3 # homeassistant.components.zha zigpy==0.36.1 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 257be293fc0..00e894b91cb 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -41,6 +41,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, __version__, + __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -130,6 +131,7 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == "0.4.3" hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -157,6 +159,31 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_MODEL).value == "Test Model" +async def test_accessory_with_missing_basic_service_info(hass, hk_driver): + """Test HomeAccessory class.""" + entity_id = "sensor.accessory" + hass.states.async_set(entity_id, "on") + acc = HomeAccessory( + hass, + hk_driver, + "Home Accessory", + entity_id, + 3, + { + ATTR_MODEL: None, + ATTR_MANUFACTURER: None, + ATTR_SOFTWARE_VERSION: None, + ATTR_INTEGRATION: None, + }, + ) + serv = acc.get_service(SERV_ACCESSORY_INFO) + assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" + assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" + assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + + async def test_battery_service(hass, hk_driver, caplog): """Test battery service.""" entity_id = "homekit.accessory" diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 455f7a6141a..6df1f0182ed 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -329,7 +329,13 @@ async def test_reset_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off @@ -367,7 +373,13 @@ async def test_script_switch(hass, hk_driver, events): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done() + assert acc.char_on.value is True + + future = dt_util.utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert acc.char_on.value is False + assert len(events) == 1 assert not call_turn_off diff --git a/tests/test_config.py b/tests/test_config.py index c1eb1ab7540..96196c943aa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -650,19 +650,30 @@ async def test_merge(merge_log_err, hass): "pack_list": {"light": {"platform": "test"}}, "pack_list2": {"light": [{"platform": "test"}]}, "pack_none": {"wake_on_lan": None}, + "pack_special": { + "automation": [{"some": "yay"}], + "script": {"a_script": "yay"}, + "template": [{"some": "yay"}], + }, } config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, + "automation": [], + "script": {}, + "template": [], } await config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 - assert len(config) == 5 + assert len(config) == 8 assert len(config["input_boolean"]) == 2 assert len(config["input_select"]) == 1 assert len(config["light"]) == 3 + assert len(config["automation"]) == 1 + assert len(config["script"]) == 1 + assert len(config["template"]) == 1 assert isinstance(config["wake_on_lan"], OrderedDict)