diff --git a/.coveragerc b/.coveragerc index 71542ebad3af89..02b0cf7a14399e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -168,6 +168,10 @@ omit = homeassistant/components/cmus/media_player.py homeassistant/components/coinbase/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py + homeassistant/components/comelit/__init__.py + homeassistant/components/comelit/const.py + homeassistant/components/comelit/coordinator.py + homeassistant/components/comelit/light.py homeassistant/components/comfoconnect/fan.py homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py @@ -1183,6 +1187,7 @@ omit = homeassistant/components/starlink/binary_sensor.py homeassistant/components/starlink/button.py homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/device_tracker.py homeassistant/components/starlink/sensor.py homeassistant/components/starlink/switch.py homeassistant/components/starline/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18cbb082145a22..77740d6279e72b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.0.285 hooks: - id: ruff args: diff --git a/CODEOWNERS b/CODEOWNERS index bd1b8ed49f03cf..812caea4da5856 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,6 +209,8 @@ build.json @home-assistant/supervisor /tests/components/coinbase/ @tombrien /homeassistant/components/color_extractor/ @GenericStudent /tests/components/color_extractor/ @GenericStudent +/homeassistant/components/comelit/ @chemelli74 +/tests/components/comelit/ @chemelli74 /homeassistant/components/comfoconnect/ @michaelarnauts /tests/components/comfoconnect/ @michaelarnauts /homeassistant/components/command_line/ @gjohansson-ST diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index f7ba18d3d758fe..9e4afa018a63ae 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -148,16 +148,6 @@ def get_arguments() -> argparse.Namespace: return arguments -def cmdline() -> list[str]: - """Collect path and arguments to re-execute the current hass instance.""" - if os.path.basename(sys.argv[0]) == "__main__.py": - modulepath = os.path.dirname(sys.argv[0]) - os.environ["PYTHONPATH"] = os.path.dirname(modulepath) - return [sys.executable, "-m", "homeassistant"] + list(sys.argv[1:]) - - return sys.argv - - def check_threads() -> None: """Check if there are any lingering threads.""" try: diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 16d790cc09cae9..0d259cf337a12a 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -100,7 +100,7 @@ async def _get_token_info(self, code): try: token_info = await oauth.get_access_token(code) except ambiclimate.AmbiclimateOauthError: - _LOGGER.error("Failed to get access token", exc_info=True) + _LOGGER.exception("Failed to get access token") return None store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 3303895eec2ea1..320812b2039edd 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -254,6 +254,8 @@ class PipelineEventType(StrEnum): WAKE_WORD_START = "wake_word-start" WAKE_WORD_END = "wake_word-end" STT_START = "stt-start" + STT_VAD_START = "stt-vad-start" + STT_VAD_END = "stt-vad-end" STT_END = "stt-end" INTENT_START = "intent-start" INTENT_END = "intent-end" @@ -612,11 +614,31 @@ async def segment_stream( stream: AsyncIterable[bytes], ) -> AsyncGenerator[bytes, None]: """Stop stream when voice command is finished.""" + sent_vad_start = False + timestamp_ms = 0 async for chunk in stream: if not segmenter.process(chunk): + # Silence detected at the end of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_END, + {"timestamp": timestamp_ms}, + ) + ) break + if segmenter.in_command and (not sent_vad_start): + # Speech detected at start of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_START, + {"timestamp": timestamp_ms}, + ) + ) + sent_vad_start = True + yield chunk + timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz # Transcribe audio stream result = await self.stt_provider.async_process_audio_stream( diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index b1281af2bc26ed..1ae23633bdf096 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -19,6 +19,6 @@ "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.8.0", - "dbus-fast==1.91.2" + "dbus-fast==1.92.0" ] } diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index f0b7df528e1487..eb3ce11b644a94 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -349,11 +349,10 @@ async def _async_restart_scanner(self) -> None: try: await self._async_start() except ScannerStartError as ex: - _LOGGER.error( + _LOGGER.exception( "%s: Failed to restart Bluetooth scanner: %s", self.name, ex, - exc_info=True, ) async def _async_reset_adapter(self) -> None: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 486c964bb45536..af64b2f195341d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -246,8 +246,8 @@ async def write_to_mjpeg_stream(img_bytes: bytes) -> None: await response.write( bytes( "--frameboundary\r\n" - "Content-Type: {}\r\n" - "Content-Length: {}\r\n\r\n".format(content_type, len(img_bytes)), + f"Content-Type: {content_type}\r\n" + f"Content-Length: {len(img_bytes)}\r\n\r\n", "utf-8", ) + img_bytes diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py new file mode 100644 index 00000000000000..2c73922582cd1a --- /dev/null +++ b/homeassistant/components/comelit/__init__.py @@ -0,0 +1,34 @@ +"""Comelit integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PIN, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ComelitSerialBridge + +PLATFORMS = [Platform.LIGHT] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Comelit platform.""" + coordinator = ComelitSerialBridge(hass, entry.data[CONF_HOST], entry.data[CONF_PIN]) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: ComelitSerialBridge = hass.data[DOMAIN][entry.entry_id] + await coordinator.api.logout() + await coordinator.api.close() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py new file mode 100644 index 00000000000000..dd6227a6583f6e --- /dev/null +++ b/homeassistant/components/comelit/config_flow.py @@ -0,0 +1,145 @@ +"""Config flow for Comelit integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions +import voluptuous as vol + +from homeassistant import core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PIN +from homeassistant.data_entry_flow import FlowResult + +from .const import _LOGGER, DOMAIN + +DEFAULT_HOST = "192.168.1.252" +DEFAULT_PIN = "111111" + + +def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: + """Return user form schema.""" + user_input = user_input or {} + return vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): str, + } + ) + + +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str}) + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: + """Validate the user input allows us to connect.""" + + api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN]) + + try: + await api.login() + except aiocomelit_exceptions.CannotConnect as err: + raise CannotConnect from err + except aiocomelit_exceptions.CannotAuthenticate as err: + raise InvalidAuth from err + finally: + await api.logout() + await api.close() + + return {"title": data[CONF_HOST]} + + +class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Comelit.""" + + VERSION = 1 + _reauth_entry: ConfigEntry | None + _reauth_host: str + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input) + ) + + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input), errors=errors + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauth flow.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._reauth_host = entry_data[CONF_HOST] + self.context["title_placeholders"] = {"host": self._reauth_host} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth confirm.""" + assert self._reauth_entry + errors = {} + + if user_input is not None: + try: + await validate_input( + self.hass, {CONF_HOST: self._reauth_host} | user_input + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data={ + CONF_HOST: self._reauth_host, + CONF_PIN: user_input[CONF_PIN], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_HOST: self._reauth_entry.data[CONF_HOST]}, + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/comelit/const.py b/homeassistant/components/comelit/const.py new file mode 100644 index 00000000000000..e08caa55f760f8 --- /dev/null +++ b/homeassistant/components/comelit/const.py @@ -0,0 +1,6 @@ +"""Comelit constants.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "comelit" diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py new file mode 100644 index 00000000000000..beb7266c403c35 --- /dev/null +++ b/homeassistant/components/comelit/coordinator.py @@ -0,0 +1,50 @@ +"""Support for Comelit.""" +import asyncio +from datetime import timedelta +from typing import Any + +from aiocomelit import ComeliteSerialBridgeAPi +import aiohttp + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import _LOGGER, DOMAIN + + +class ComelitSerialBridge(DataUpdateCoordinator): + """Queries Comelit Serial Bridge.""" + + def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None: + """Initialize the scanner.""" + + self._host = host + self._pin = pin + + self.api = ComeliteSerialBridgeAPi(host, pin) + + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{DOMAIN}-{host}-coordinator", + update_interval=timedelta(seconds=5), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update router data.""" + _LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host) + try: + logged = await self.api.login() + except (asyncio.exceptions.TimeoutError, aiohttp.ClientConnectorError) as err: + _LOGGER.warning("Connection error for %s", self._host) + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + + if not logged: + raise ConfigEntryAuthFailed + + devices_data = await self.api.get_all_devices() + alarm_data = await self.api.get_alarm_config() + await self.api.logout() + + return devices_data | alarm_data diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py new file mode 100644 index 00000000000000..9a893bd929ca68 --- /dev/null +++ b/homeassistant/components/comelit/light.py @@ -0,0 +1,78 @@ +"""Support for lights.""" +from __future__ import annotations + +from typing import Any + +from aiocomelit import ComelitSerialBridgeObject +from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON + +from homeassistant.components.light import LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ComelitSerialBridge + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Comelit lights.""" + + coordinator: ComelitSerialBridge = hass.data[DOMAIN][config_entry.entry_id] + + # Use config_entry.entry_id as base for unique_id because no serial number or mac is available + async_add_entities( + ComelitLightEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[LIGHT].values() + ) + + +class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity): + """Light device.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + coordinator: ComelitSerialBridge, + device: ComelitSerialBridgeObject, + config_entry_unique_id: str | None, + ) -> None: + """Init light entity.""" + self._api = coordinator.api + self._device = device + super().__init__(coordinator) + self._attr_unique_id = f"{config_entry_unique_id}-{device.index}" + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, self._attr_unique_id), + }, + manufacturer="Comelit", + model="Serial Bridge", + name=device.name, + ) + + async def _light_set_state(self, state: int) -> None: + """Set desired light state.""" + await self.coordinator.api.light_switch(self._device.index, state) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + await self._light_set_state(LIGHT_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._light_set_state(LIGHT_OFF) + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.coordinator.data[LIGHT][self._device.index].status == LIGHT_ON diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json new file mode 100644 index 00000000000000..fc7f2a3fc12dc9 --- /dev/null +++ b/homeassistant/components/comelit/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "comelit", + "name": "Comelit SimpleHome", + "codeowners": ["@chemelli74"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/comelit", + "iot_class": "local_polling", + "loggers": ["aiocomelit"], + "requirements": ["aiocomelit==0.0.5"] +} diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json new file mode 100644 index 00000000000000..6508f58412ea25 --- /dev/null +++ b/homeassistant/components/comelit/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "description": "Please enter the correct PIN for VEDO system: {host}", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 2ccbdbc4785479..f04320b159e5e4 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -16,13 +16,12 @@ PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, SensorDeviceClass, - SensorEntity, - SensorStateClass, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, @@ -36,7 +35,11 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerSensorEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -47,6 +50,16 @@ DEFAULT_NAME = "Command Sensor" +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + SCAN_INTERVAL = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -87,30 +100,25 @@ async def async_setup_platform( name: str = sensor_config[CONF_NAME] command: str = sensor_config[CONF_COMMAND] - unit: str | None = sensor_config.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = sensor_config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - state_class: SensorStateClass | None = sensor_config.get(CONF_STATE_CLASS) data = CommandSensorData(hass, command, command_timeout) - trigger_entity_config = { - CONF_UNIQUE_ID: unique_id, - CONF_NAME: Template(name, hass), - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - } + trigger_entity_config = {CONF_NAME: Template(name, hass)} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] async_add_entities( [ CommandSensor( data, trigger_entity_config, - unit, - state_class, value_template, json_attributes, scan_interval, @@ -119,7 +127,7 @@ async def async_setup_platform( ) -class CommandSensor(ManualTriggerEntity, SensorEntity): +class CommandSensor(ManualTriggerSensorEntity): """Representation of a sensor that is using shell commands.""" _attr_should_poll = False @@ -128,8 +136,6 @@ def __init__( self, data: CommandSensorData, config: ConfigType, - unit_of_measurement: str | None, - state_class: SensorStateClass | None, value_template: Template | None, json_attributes: list[str] | None, scan_interval: timedelta, @@ -141,8 +147,6 @@ def __init__( self._json_attributes = json_attributes self._attr_native_value = None self._value_template = value_template - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_state_class = state_class self._scan_interval = scan_interval self._process_updates: asyncio.Lock | None = None diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index cad6656d01d407..c3dfbeb101130d 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -196,11 +196,10 @@ async def wrapper( ) except DenonAvrError as err: available = False - _LOGGER.error( + _LOGGER.exception( "Error %s occurred in method %s for Denon AVR receiver", err, func.__name__, - exc_info=True, ) finally: if available and not self.available: diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index a6fb49c30e0b19..0be9daf572bddb 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -8,11 +8,10 @@ from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -54,29 +53,17 @@ def is_closing(self) -> bool: """Return if the cover is closing.""" return self._unit.is_closing() + @api_call async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - try: - await self._unit.open() - except OSError as err: - raise HomeAssistantError( - "Transmit for the open_cover packet failed" - ) from err + await self._unit.open() + @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - try: - await self._unit.close() - except OSError as err: - raise HomeAssistantError( - "Transmit for the close_cover packet failed" - ) from err + await self._unit.close() + @api_call async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - try: - await self._unit.stop() - except OSError as err: - raise HomeAssistantError( - "Transmit for the stop_cover packet failed" - ) from err + await self._unit.stop() diff --git a/homeassistant/components/duotecno/entity.py b/homeassistant/components/duotecno/entity.py index 5715593ad2d44a..d38d52a0d2691c 100644 --- a/homeassistant/components/duotecno/entity.py +++ b/homeassistant/components/duotecno/entity.py @@ -1,8 +1,13 @@ """Support for Velbus devices.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar + from duotecno.unit import BaseUnit +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -35,3 +40,25 @@ async def async_added_to_hass(self) -> None: async def _on_update(self) -> None: """When a unit has an update.""" self.async_write_ha_state() + + +_T = TypeVar("_T", bound="DuotecnoEntity") +_P = ParamSpec("_P") + + +def api_call( + func: Callable[Concatenate[_T, _P], Awaitable[None]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Catch command exceptions.""" + + @wraps(func) + async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except OSError as exc: + raise HomeAssistantError( + f"Error calling {func.__name__} on entity {self.entity_id}" + ) from exc + + return cmd_wrapper diff --git a/homeassistant/components/duotecno/light.py b/homeassistant/components/duotecno/light.py index da288b6cbe0ef0..9aee4513fca01e 100644 --- a/homeassistant/components/duotecno/light.py +++ b/homeassistant/components/duotecno/light.py @@ -6,11 +6,10 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -40,6 +39,7 @@ def brightness(self) -> int: """Return the brightness of the light.""" return int((self._unit.get_dimmer_state() * 255) / 100) + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if (val := kwargs.get(ATTR_BRIGHTNESS)) is not None: @@ -48,18 +48,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: else: # restore state val = None - try: - await self._unit.set_dimmer_state(val) - except OSError as err: - raise HomeAssistantError( - "Transmit for the set_dimmer_state packet failed" - ) from err + await self._unit.set_dimmer_state(val) + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - try: - await self._unit.set_dimmer_state(0) - except OSError as err: - raise HomeAssistantError( - "Transmit for the set_dimmer_state packet failed" - ) from err + await self._unit.set_dimmer_state(0) diff --git a/homeassistant/components/duotecno/switch.py b/homeassistant/components/duotecno/switch.py index a9921de85d3e19..63bab7505430f4 100644 --- a/homeassistant/components/duotecno/switch.py +++ b/homeassistant/components/duotecno/switch.py @@ -6,11 +6,10 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -35,16 +34,12 @@ def is_on(self) -> bool: """Return true if the switch is on.""" return self._unit.is_on() + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on.""" - try: - await self._unit.turn_on() - except OSError as err: - raise HomeAssistantError("Transmit for the turn_on packet failed") from err + await self._unit.turn_on() + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" - try: - await self._unit.turn_off() - except OSError as err: - raise HomeAssistantError("Transmit for the turn_off packet failed") from err + await self._unit.turn_off() diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 77d41ccf37596d..009b5d18338e1a 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -51,12 +51,6 @@ class EnvoyEnchargeBinarySensorEntityDescription( entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda encharge: not encharge.dc_switch_off, ), - EnvoyEnchargeBinarySensorEntityDescription( - key="operating", - translation_key="operating", - entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda encharge: encharge.operating, - ), ) RELAY_STATUS_SENSOR = BinarySensorEntityDescription( @@ -88,12 +82,6 @@ class EnvoyEnpowerBinarySensorEntityDescription( entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda enpower: enpower.communicating, ), - EnvoyEnpowerBinarySensorEntityDescription( - key="operating", - translation_key="operating", - entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda enpower: enpower.operating, - ), EnvoyEnpowerBinarySensorEntityDescription( key="mains_oper_state", translation_key="grid_status", diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index f023bc7d114424..477da2b3211ccc 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -29,9 +29,6 @@ "dc_switch": { "name": "DC switch" }, - "operating": { - "name": "Operating" - }, "grid_status": { "name": "Grid status" }, diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 5011439c7780e9..898fb55a3ac25f 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -427,9 +427,7 @@ async def _retrieve_encryption_key_from_dashboard(self) -> bool: _LOGGER.error("Error talking to the dashboard: %s", err) return False except json.JSONDecodeError as err: - _LOGGER.error( - "Error parsing response from dashboard: %s", err, exc_info=True - ) + _LOGGER.exception("Error parsing response from dashboard: %s", err) return False self._noise_psk = noise_psk diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7870e9cca0847..ad9403e3601a1f 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -312,7 +312,7 @@ def async_update_state(self, state: EntityState) -> None: and subscription_key not in stale_state and state_type is not CameraState and not ( - state_type is SensorState # pylint: disable=unidiomatic-typecheck + state_type is SensorState # noqa: E721 and (platform_info := self.info.get(SensorInfo)) and (entity_info := platform_info.get(state.key)) and (cast(SensorInfo, entity_info)).force_update diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 373f9af22fc799..11144f8ae71067 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -223,11 +223,6 @@ "name": "Follow movement" } }, - "update": { - "firmware": { - "name": "[%key:component::update::entity_component::firmware::name%]" - } - }, "siren": { "siren": { "name": "[%key:component::siren::title%]" diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index 6a80a579080fab..003397d8dda973 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -24,7 +24,6 @@ UPDATE_ENTITY_TYPES = UpdateEntityDescription( key="version", - translation_key="firmware", device_class=UpdateDeviceClass.FIRMWARE, ) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 70a99f56968b57..1f590b0cd16229 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -93,8 +93,11 @@ def __init__(self, hass: HomeAssistant, auth) -> None: def _update_lists(self): """Query flume for notification list.""" + # Get notifications (read or unread). + # The related binary sensors (leak detected, high flow, low battery) + # will be active until the notification is deleted in the Flume app. self.notifications: list[dict[str, Any]] = pyflume.FlumeNotificationList( - self.auth, read="true" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) diff --git a/homeassistant/components/freebox/home_base.py b/homeassistant/components/freebox/home_base.py index 37709cbf49484c..dc8872290862c0 100644 --- a/homeassistant/components/freebox/home_base.py +++ b/homeassistant/components/freebox/home_base.py @@ -126,7 +126,7 @@ def get_value(self, ep_type, name): ) if not node: _LOGGER.warning( - "The Freebox Home device has no node for: " + ep_type + "/" + name + "The Freebox Home device has no node for: %s/%s", ep_type, name ) return None return node.get("value") diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d61ce334804be8..6977377812160b 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -785,27 +785,20 @@ async def _async_service_call( ) return result except FritzSecurityError: - _LOGGER.error( - ( - "Authorization Error: Please check the provided credentials and" - " verify that you can log into the web interface" - ), - exc_info=True, + _LOGGER.exception( + "Authorization Error: Please check the provided credentials and" + " verify that you can log into the web interface" ) except FRITZ_EXCEPTIONS: - _LOGGER.error( + _LOGGER.exception( "Service/Action Error: cannot execute service %s with action %s", service_name, action_name, - exc_info=True, ) except FritzConnectionException: - _LOGGER.error( - ( - "Connection Error: Please check the device is properly configured" - " for remote login" - ), - exc_info=True, + _LOGGER.exception( + "Connection Error: Please check the device is properly configured" + " for remote login" ) return {} diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 84d1d4f5e277dd..986dfd6ba525a8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230802.0"] + "requirements": ["home-assistant-frontend==20230802.1"] } diff --git a/homeassistant/components/garages_amsterdam/entity.py b/homeassistant/components/garages_amsterdam/entity.py index 894506f7da974f..df06f47dff5ef4 100644 --- a/homeassistant/components/garages_amsterdam/entity.py +++ b/homeassistant/components/garages_amsterdam/entity.py @@ -1,12 +1,13 @@ """Generic entity for Garages Amsterdam.""" from __future__ import annotations +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ATTRIBUTION +from .const import ATTRIBUTION, DOMAIN class GaragesAmsterdamEntity(CoordinatorEntity): @@ -22,3 +23,8 @@ def __init__( self._attr_unique_id = f"{garage_name}-{info_type}" self._garage_name = garage_name self._info_type = info_type + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, garage_name)}, + name=garage_name, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 6e4959a4789764..09d00e9bee1354 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -180,7 +180,7 @@ def preset_modes(self) -> list[str]: ) or self._has_switch: if not profile_names: presets.append(PRESET_NONE) - presets.append(PRESET_BOOST) + presets.extend([PRESET_BOOST, PRESET_ECO]) presets.extend(profile_names) @@ -223,6 +223,8 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode == PRESET_ECO: + await self._device.set_control_mode(HMIP_ECO_CM) if preset_mode in self._device_profile_names: profile_idx = self._get_profile_idx_by_name(preset_mode) if self._device.controlMode != HMIP_AUTOMATIC_CM: diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 6486d584b0e8ca..6faa690b4cbc3f 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -78,8 +78,10 @@ async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] - if not uploaded_file.content_type.startswith( - ("image/gif", "image/jpeg", "image/png") + if uploaded_file.content_type not in ( + "image/gif", + "image/jpeg", + "image/png", ): raise vol.Invalid("Only jpeg, png, and gif images are allowed") diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 6daecc6a305967..ba17a4484778eb 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -298,18 +298,14 @@ def calc_integration(event: EventType[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] - # We may want to update our state before an early return, - # based on the source sensor's unit_of_measurement - # or device_class. - update_state = False - if ( source_state := self.hass.states.get(self._sensor_source_id) ) is None or source_state.state == STATE_UNAVAILABLE: self._attr_available = False - update_state = True - else: - self._attr_available = True + self.async_write_ha_state() + return + + self._attr_available = True if old_state is None or new_state is None: # we can't calculate the elapsed time, so we can't calculate the integral @@ -317,10 +313,7 @@ def calc_integration(event: EventType[EventStateChangedData]) -> None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: - new_unit_of_measurement = self._unit(unit) - if self._unit_of_measurement != new_unit_of_measurement: - self._unit_of_measurement = new_unit_of_measurement - update_state = True + self._unit_of_measurement = self._unit(unit) if ( self.device_class is None @@ -329,10 +322,8 @@ def calc_integration(event: EventType[EventStateChangedData]) -> None: ): self._attr_device_class = SensorDeviceClass.ENERGY self._attr_icon = None - update_state = True - if update_state: - self.async_write_ha_state() + self.async_write_ha_state() try: # integration as the Riemann integral of previous measures. diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 8436d24902c7a0..7acfad69735294 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -131,11 +131,6 @@ "litter_box": { "name": "Litter box" } - }, - "update": { - "firmware": { - "name": "Firmware" - } } }, "services": { diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 9b8391c5bae53d..584a6af77c2f37 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -24,7 +24,6 @@ FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( key="firmware", - translation_key="firmware", device_class=UpdateDeviceClass.FIRMWARE, ) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index e351ee6bb6133f..82c05e612e3962 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -65,7 +65,7 @@ def __init__( self.context_parent_id_bin: bytes | None = self.row.context_parent_id_bin # We need to explicitly check for the row is EventAsRow as the unhappy path # to fetch row.data for Row is very expensive - if type(row) is EventAsRow: # pylint: disable=unidiomatic-typecheck + if type(row) is EventAsRow: # noqa: E721 # If its an EventAsRow we can avoid the whole # json decode process as we already have the data self.data = row.data diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 099a0a028d0b00..df90ebcd6cfc98 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -21,7 +21,12 @@ HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_WHOLE, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -113,7 +118,6 @@ async def async_setup_entry( ), location, device, - hass.config.units.temperature_unit, ) ) @@ -140,10 +144,15 @@ def __init__( description: ClimateEntityDescription, location: LyricLocation, device: LyricDevice, - temperature_unit: str, ) -> None: """Initialize Honeywell Lyric climate entity.""" - self._temperature_unit = temperature_unit + # Use the native temperature unit from the device settings + if device.units == "Fahrenheit": + self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + self._attr_precision = PRECISION_WHOLE + else: + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_precision = PRECISION_HALVES # Setup supported hvac modes self._attr_hvac_modes = [HVACMode.OFF] @@ -176,11 +185,6 @@ def supported_features(self) -> ClimateEntityFeature: return SUPPORT_FLAGS_LCC return SUPPORT_FLAGS_TCC - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return self._temperature_unit - @property def current_temperature(self) -> float | None: """Return the current temperature.""" diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 1201a675a5d645..1e15ff58b188c9 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -17,7 +17,7 @@ SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -76,6 +76,11 @@ def get_setpoint_status(status: str, time: str) -> str | None: for location in coordinator.data.locations: for device in location.devices: if device.indoorTemperature: + if device.units == "Fahrenheit": + native_temperature_unit = UnitOfTemperature.FAHRENHEIT + else: + native_temperature_unit = UnitOfTemperature.CELSIUS + entities.append( LyricSensor( coordinator, @@ -84,7 +89,7 @@ def get_setpoint_status(status: str, time: str) -> str | None: name="Indoor Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=hass.config.units.temperature_unit, + native_unit_of_measurement=native_temperature_unit, value=lambda device: device.indoorTemperature, ), location, @@ -108,6 +113,11 @@ def get_setpoint_status(status: str, time: str) -> str | None: ) ) if device.outdoorTemperature: + if device.units == "Fahrenheit": + native_temperature_unit = UnitOfTemperature.FAHRENHEIT + else: + native_temperature_unit = UnitOfTemperature.CELSIUS + entities.append( LyricSensor( coordinator, @@ -116,7 +126,7 @@ def get_setpoint_status(status: str, time: str) -> str | None: name="Outdoor Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=hass.config.units.temperature_unit, + native_unit_of_measurement=native_temperature_unit, value=lambda device: device.outdoorTemperature, ), location, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index e4c657a6c542ba..9cf582a5dda4cc 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -50,10 +50,12 @@ CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, + CONF_SLAVE_COUNT, CONF_STATE_OFF, CONF_STATE_ON, CONF_SWAP, CONF_SWAP_BYTE, + CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_VERIFY, @@ -154,15 +156,25 @@ def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" super().__init__(hub, config) self._swap = config[CONF_SWAP] + if self._swap == CONF_SWAP_NONE: + self._swap = None self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] self._precision = config[CONF_PRECISION] self._scale = config[CONF_SCALE] self._offset = config[CONF_OFFSET] - self._count = config[CONF_COUNT] + self._slave_count = config.get(CONF_SLAVE_COUNT, 0) + self._slave_size = self._count = config[CONF_COUNT] - def _swap_registers(self, registers: list[int]) -> list[int]: + def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" + if slave_count: + swapped = [] + for i in range(0, self._slave_count + 1): + inx = i * self._slave_size + inx2 = inx + self._slave_size + swapped.extend(self._swap_registers(registers[inx:inx2], 0)) + return swapped if self._swap in (CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE): # convert [12][34] --> [21][43] for i, register in enumerate(registers): @@ -192,7 +204,8 @@ def __process_raw_value(self, entry: float | int | str) -> float | int | str: def unpack_structure_result(self, registers: list[int]) -> str | None: """Convert registers to proper result.""" - registers = self._swap_registers(registers) + if self._swap: + registers = self._swap_registers(registers, self._slave_count) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DataType.STRING: return byte_string.decode() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 95f8bee0bc9685..7170716d43e951 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -210,7 +210,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: int.from_bytes(as_bytes[i : i + 2], "big") for i in range(0, len(as_bytes), 2) ] - registers = self._swap_registers(raw_regs) + registers = self._swap_registers(raw_regs, 0) if self._data_type in ( DataType.INT16, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index bed5932a3033ce..fe2d4bc415d88a 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -11,6 +11,7 @@ SensorEntity, ) from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_SENSORS, CONF_UNIQUE_ID, @@ -68,10 +69,11 @@ def __init__( """Initialize the modbus register sensor.""" super().__init__(hub, entry) if slave_count: - self._count = self._count * slave_count + self._count = self._count * (slave_count + 1) self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) + self._attr_device_class = entry.get(CONF_DEVICE_CLASS) async def async_setup_slaves( self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] @@ -132,10 +134,7 @@ async def async_update(self, now: datetime | None = None) -> None: self._coordinator.async_set_updated_data(None) else: self._attr_native_value = result - if self._attr_native_value is None: - self._attr_available = False - else: - self._attr_available = True + self._attr_available = self._attr_native_value is not None self._lazy_errors = self._lazy_error_count self.async_write_ha_state() @@ -160,6 +159,8 @@ def __init__( self._attr_unique_id = entry.get(CONF_UNIQUE_ID) if self._attr_unique_id: self._attr_unique_id = f"{self._attr_unique_id}_{idx}" + self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_available = False super().__init__(coordinator) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index ee9d40dd874187..b2e33a0f1f1958 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -65,25 +65,24 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 - swap_type = config.get(CONF_SWAP) + swap_type = config.get(CONF_SWAP, CONF_SWAP_NONE) + if ( + slave_count > 1 + and count > 1 + and data_type not in (DataType.CUSTOM, DataType.STRING) + ): + error = f"{name} {CONF_COUNT} cannot be mixed with {data_type}" + raise vol.Invalid(error) if config[CONF_DATA_TYPE] != DataType.CUSTOM: if structure: error = f"{name} structure: cannot be mixed with {data_type}" - raise vol.Invalid(error) - if data_type not in DEFAULT_STRUCT_FORMAT: - error = f"Error in sensor {name}. data_type `{data_type}` not supported" - raise vol.Invalid(error) - structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - if CONF_COUNT not in config: - config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if config[CONF_DATA_TYPE] == DataType.CUSTOM: if slave_count > 1: - structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - else: - structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - else: - if slave_count > 1: - error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}" + error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" + raise vol.Invalid(error) + if swap_type != CONF_SWAP_NONE: + error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SWAP}`" raise vol.Invalid(error) if not structure: error = ( @@ -102,19 +101,37 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: f"Structure request {size} bytes, " f"but {count} registers have a size of {bytecount} bytes" ) + return { + **config, + CONF_STRUCTURE: structure, + CONF_SWAP: swap_type, + } + if data_type not in DEFAULT_STRUCT_FORMAT: + error = f"Error in sensor {name}. data_type `{data_type}` not supported" + raise vol.Invalid(error) + if slave_count > 1 and data_type == DataType.STRING: + error = f"{name}: `{data_type}` illegal with `{CONF_SLAVE_COUNT}`" + raise vol.Invalid(error) - if swap_type != CONF_SWAP_NONE: - if swap_type == CONF_SWAP_BYTE: - regs_needed = 1 - else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD - regs_needed = 2 - if count < regs_needed or (count % regs_needed) != 0: - raise vol.Invalid( - f"Error in sensor {name} swap({swap_type}) " - "not possible due to the registers " - f"count: {count}, needed: {regs_needed}" - ) - + if CONF_COUNT not in config: + config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if swap_type != CONF_SWAP_NONE: + if swap_type == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + count = config[CONF_COUNT] + if count < regs_needed or (count % regs_needed) != 0: + raise vol.Invalid( + f"Error in sensor {name} swap({swap_type}) " + "not possible due to the registers " + f"count: {count}, needed: {regs_needed}" + ) + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + if slave_count > 1: + structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + else: + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" return { **config, CONF_STRUCTURE: structure, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 06f91403057b02..a0939fdc615647 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -39,6 +39,7 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_SUPPORTED_FEATURES, ) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -47,6 +48,15 @@ _LOGGER = logging.getLogger(__name__) +_SUPPORTED_FEATURES = { + "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, + "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, + "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, + "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, + "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + "trigger": AlarmControlPanelEntityFeature.TRIGGER, +} + CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" @@ -81,6 +91,9 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { + vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [ + vol.In(_SUPPORTED_FEATURES) + ], vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, @@ -167,6 +180,9 @@ def _setup_from_config(self, config: ConfigType) -> None: config[CONF_COMMAND_TEMPLATE], entity=self ).async_render + for feature in self._config[CONF_SUPPORTED_FEATURES]: + self._attr_supported_features |= _SUPPORTED_FEATURES[feature] + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @@ -214,18 +230,6 @@ def state(self) -> str | None: """Return the state of the device.""" return self._state - @property - def supported_features(self) -> AlarmControlPanelEntityFeature: - """Return the list of supported features.""" - return ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - | AlarmControlPanelEntityFeature.ARM_NIGHT - | AlarmControlPanelEntityFeature.ARM_VACATION - | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - | AlarmControlPanelEntityFeature.TRIGGER - ) - @property def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 0c351e69bcf7c1..62f1f55401d0b8 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -361,7 +361,7 @@ async def async_cleanup(self) -> None: except asyncio.CancelledError: pass except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error cleaning up task", exc_info=True) + _LOGGER.exception("Error cleaning up task") class MQTT: diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index fcdfeb4bd7df59..97d2e1473f5da2 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -28,6 +28,7 @@ CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_PAYLOAD_RESET = "payload_reset" +CONF_SUPPORTED_FEATURES = "supported_features" CONF_ACTION_TEMPLATE = "action_template" CONF_ACTION_TOPIC = "action_topic" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9afa3de3f48d98..99267d9572ac63 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -231,11 +231,21 @@ def async_render_with_possible_json_value( values, self._value_template, ) - rendered_payload = ( - self._value_template.async_render_with_possible_json_value( - payload, variables=values + try: + rendered_payload = ( + self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) ) - ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( + "%s: %s rendering template for entity '%s', template: '%s'", + type(ex).__name__, + ex, + self._entity.entity_id if self._entity else "n/a", + self._value_template.template, + ) + raise ex return rendered_payload _LOGGER.debug( @@ -248,9 +258,24 @@ def async_render_with_possible_json_value( default, self._value_template, ) - rendered_payload = self._value_template.async_render_with_possible_json_value( - payload, default, variables=values - ) + try: + rendered_payload = ( + self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( + "%s: %s rendering template for entity '%s', template: " + "'%s', default value: %s and payload: %s", + type(ex).__name__, + ex, + self._entity.entity_id if self._entity else "n/a", + self._value_template.template, + default, + payload, + ) + raise ex return rendered_payload @@ -269,13 +294,12 @@ def process_write_state_requests(self, msg: MQTTMessage) -> None: try: entity.async_write_ha_state() except Exception: # pylint: disable=broad-except - _LOGGER.error( + _LOGGER.exception( "Exception raised when updating state of %s, topic: " "'%s' with payload: %s", entity.entity_id, msg.topic, msg.payload, - exc_info=True, ) @callback diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 493fdd8a403563..fe2d6527ea0bd5 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -2,10 +2,11 @@ from homeassistant.const import Platform PLATFORMS = [ - Platform.SENSOR, Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.NUMBER, Platform.SCENE, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/nexia/number.py b/homeassistant/components/nexia/number.py new file mode 100644 index 00000000000000..acb99c2ed01b10 --- /dev/null +++ b/homeassistant/components/nexia/number.py @@ -0,0 +1,72 @@ +"""Support for Nexia / Trane XL Thermostats.""" +from __future__ import annotations + +from nexia.home import NexiaHome +from nexia.thermostat import NexiaThermostat + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import NexiaDataUpdateCoordinator +from .entity import NexiaThermostatEntity +from .util import percent_conv + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors for a Nexia device.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home: NexiaHome = coordinator.nexia_home + + entities: list[NexiaThermostatEntity] = [] + for thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + if thermostat.has_variable_fan_speed(): + entities.append( + NexiaFanSpeedEntity( + coordinator, thermostat, thermostat.get_variable_fan_speed_limits() + ) + ) + async_add_entities(entities) + + +class NexiaFanSpeedEntity(NexiaThermostatEntity, NumberEntity): + """Provides Nexia Fan Speed support.""" + + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:fan" + + def __init__( + self, + coordinator: NexiaDataUpdateCoordinator, + thermostat: NexiaThermostat, + valid_range: tuple[float, float], + ) -> None: + """Initialize the entity.""" + super().__init__( + coordinator, + thermostat, + name=f"{thermostat.get_name()} Fan speed", + unique_id=f"{thermostat.thermostat_id}_fan_speed_setpoint", + ) + min_value, max_value = valid_range + self._attr_native_min_value = percent_conv(min_value) + self._attr_native_max_value = percent_conv(max_value) + + @property + def native_value(self) -> float: + """Return the current value.""" + fan_speed = self._thermostat.get_fan_speed_setpoint() + return percent_conv(fan_speed) + + async def async_set_native_value(self, value: float) -> None: + """Set a new value.""" + await self._thermostat.set_fan_setpoint(value / 100) + self._signal_thermostat_update() diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 0df787de9869d0..4ab709ae9473ac 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -24,6 +24,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -70,7 +71,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.TARGET_TEMPERATURE ) - _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF, HVACMode.HEAT] + _attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT, HVACMode.HEAT_COOL] _attr_target_temperature_step = 0.5 _attr_max_temp = 35.0 _attr_min_temp = 5.0 @@ -101,7 +102,7 @@ def __init__( self._attr_unique_id = f"{coordinator.unique_id}-{key}" self._attr_device_info = coordinator.device_info self._attr_hvac_action = HVACAction.IDLE - self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_mode = HVACMode.AUTO self._attr_target_temperature_high = None self._attr_target_temperature_low = None self._attr_target_temperature = None @@ -138,7 +139,7 @@ def _get_float(coil: Coil) -> float | None: self._attr_current_temperature = _get_float(self._coil_current) - mode = HVACMode.OFF + mode = HVACMode.AUTO if _get_value(self._coil_use_room_sensor) == "ON": if ( _get_value(self._coil_cooling_with_room_sensor) @@ -225,3 +226,25 @@ async def async_set_temperature(self, **kwargs: Any) -> None: if (temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH)) is not None: await coordinator.async_write_coil(self._coil_setpoint_cool, temperature) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + coordinator = self.coordinator + + if hvac_mode == HVACMode.HEAT_COOL: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "ON" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "ON") + elif hvac_mode == HVACMode.HEAT: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "OFF" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "ON") + elif hvac_mode == HVACMode.AUTO: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "OFF" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "OFF") + else: + raise HomeAssistantError(f"{hvac_mode} mode not supported for {self.name}") diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 14720106f7417a..58d642ef9a1649 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.29"] + "requirements": ["opower==0.0.30"] } diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index fc48ebce4eb14b..ac2b86754e5de1 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -91,11 +91,6 @@ "hot_days_extra_watering": { "name": "Extra water on hot days" } - }, - "update": { - "firmware": { - "name": "Firmware" - } } }, "services": { diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index 372319ba9a0f71..8d5690b5320046 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -44,7 +44,6 @@ class UpdateStates(Enum): UPDATE_DESCRIPTION = RainMachineEntityDescription( key="update", - translation_key="firmware", api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, ) diff --git a/homeassistant/components/recorder/models/context.py b/homeassistant/components/recorder/models/context.py index f722c519833311..f25e4d4412f23e 100644 --- a/homeassistant/components/recorder/models/context.py +++ b/homeassistant/components/recorder/models/context.py @@ -18,7 +18,7 @@ def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None: try: return ulid_to_bytes(ulid) except ValueError as ex: - _LOGGER.error("Error converting ulid %s to bytes: %s", ulid, ex, exc_info=True) + _LOGGER.exception("Error converting ulid %s to bytes: %s", ulid, ex) return None @@ -29,9 +29,7 @@ def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None: try: return bytes_to_ulid(_bytes) except ValueError as ex: - _LOGGER.error( - "Error converting bytes %s to ulid: %s", _bytes, ex, exc_info=True - ) + _LOGGER.exception("Error converting bytes %s to ulid: %s", _bytes, ex) return None diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f3de9824a16241..1c3e07f40fd210 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -132,7 +132,7 @@ def session_scope( need_rollback = True session.commit() except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Error executing query: %s", err, exc_info=True) + _LOGGER.exception("Error executing query: %s", err) if need_rollback: session.rollback() if not exception_filter or not exception_filter(err): diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index af8d049dbc67e2..6282f29e4424b3 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -21,14 +21,30 @@ from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkHostCoordinatorEntity +from .entity import ReolinkChannelCoordinatorEntity, ReolinkHostCoordinatorEntity + + +@dataclass +class ReolinkSensorEntityDescriptionMixin: + """Mixin values for Reolink sensor entities for a camera channel.""" + + value: Callable[[Host, int], int] + + +@dataclass +class ReolinkSensorEntityDescription( + SensorEntityDescription, ReolinkSensorEntityDescriptionMixin +): + """A class that describes sensor entities for a camera channel.""" + + supported: Callable[[Host, int], bool] = lambda api, ch: True @dataclass class ReolinkHostSensorEntityDescriptionMixin: """Mixin values for Reolink host sensor entities.""" - value: Callable[[Host], bool] + value: Callable[[Host], int] @dataclass @@ -37,9 +53,21 @@ class ReolinkHostSensorEntityDescription( ): """A class that describes host sensor entities.""" - supported: Callable[[Host], bool] = lambda host: True + supported: Callable[[Host], bool] = lambda api: True +SENSORS = ( + ReolinkSensorEntityDescription( + key="ptz_pan_position", + translation_key="ptz_pan_position", + icon="mdi:pan", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda api, ch: api.ptz_pan_position(ch), + supported=lambda api, ch: api.supported(ch, "ptz_position"), + ), +) + HOST_SENSORS = ( ReolinkHostSensorEntityDescription( key="wifi_signal", @@ -62,11 +90,45 @@ async def async_setup_entry( """Set up a Reolink IP Camera.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - ReolinkHostSensorEntity(reolink_data, entity_description) - for entity_description in HOST_SENSORS - if entity_description.supported(reolink_data.host.api) + entities: list[ReolinkSensorEntity | ReolinkHostSensorEntity] = [ + ReolinkSensorEntity(reolink_data, channel, entity_description) + for entity_description in SENSORS + for channel in reolink_data.host.api.channels + if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + [ + ReolinkHostSensorEntity(reolink_data, entity_description) + for entity_description in HOST_SENSORS + if entity_description.supported(reolink_data.host.api) + ] ) + async_add_entities(entities) + + +class ReolinkSensorEntity(ReolinkChannelCoordinatorEntity, SensorEntity): + """Base sensor class for Reolink IP camera sensors.""" + + entity_description: ReolinkSensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkSensorEntityDescription, + ) -> None: + """Initialize Reolink sensor.""" + super().__init__(reolink_data, channel) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._host.unique_id}_{channel}_{entity_description.key}" + ) + + @property + def native_value(self) -> StateType | date | datetime | Decimal: + """Return the value reported by the sensor.""" + return self.entity_description.value(self._host.api, self._channel) class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): @@ -79,7 +141,7 @@ def __init__( reolink_data: ReolinkData, entity_description: ReolinkHostSensorEntityDescription, ) -> None: - """Initialize Reolink binary sensor.""" + """Initialize Reolink host sensor.""" super().__init__(reolink_data) self.entity_description = entity_description diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 08ee78fd9300ce..cdaeb7d06560c0 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -102,6 +102,9 @@ "sensor": { "wifi_signal": { "name": "Wi-Fi signal" + }, + "ptz_pan_position": { + "name": "PTZ pan position" } } } diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 1f331651165f5f..61c88a14400d5c 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -81,7 +81,7 @@ def data_without_xml(self) -> str | None: "REST xml result could not be parsed and converted to JSON" ) else: - _LOGGER.debug("JSON converted from XML: %s", self.data) + _LOGGER.debug("JSON converted from XML: %s", value) return value async def async_update(self, log_errors: bool = True) -> None: diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index f7743a853ad608..63a9d6f210c67f 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -12,7 +12,6 @@ DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, - SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( @@ -118,7 +117,7 @@ async def async_setup_platform( ) -class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity): +class RestSensor(ManualTriggerSensorEntity, RestEntity): """Implementation of a REST sensor.""" def __init__( diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 7cd7e2197ab875..2763d034804aa4 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,11 +6,7 @@ import voluptuous as vol -from homeassistant.components.sensor import ( - CONF_STATE_CLASS, - SensorDeviceClass, - SensorEntity, -) +from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -32,6 +28,7 @@ CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, ManualTriggerEntity, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -41,6 +38,16 @@ _LOGGER = logging.getLogger(__name__) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + async def async_setup_platform( hass: HomeAssistant, @@ -63,25 +70,17 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - trigger_entity_config = { - CONF_NAME: sensor_config[CONF_NAME], - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - CONF_UNIQUE_ID: sensor_config.get(CONF_UNIQUE_ID), - } - if available := sensor_config.get(CONF_AVAILABILITY): - trigger_entity_config[CONF_AVAILABILITY] = available - if icon := sensor_config.get(CONF_ICON): - trigger_entity_config[CONF_ICON] = icon - if picture := sensor_config.get(CONF_PICTURE): - trigger_entity_config[CONF_PICTURE] = picture + trigger_entity_config = {CONF_NAME: sensor_config[CONF_NAME]} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] entities.append( ScrapeSensor( hass, coordinator, trigger_entity_config, - sensor_config.get(CONF_UNIT_OF_MEASUREMENT), - sensor_config.get(CONF_STATE_CLASS), sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], @@ -113,19 +112,17 @@ async def async_setup_entry( Template(value_string, hass) if value_string is not None else None ) - trigger_entity_config = { - CONF_NAME: name, - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - CONF_UNIQUE_ID: sensor_config[CONF_UNIQUE_ID], - } + trigger_entity_config = {CONF_NAME: name} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] entities.append( ScrapeSensor( hass, coordinator, trigger_entity_config, - sensor_config.get(CONF_UNIT_OF_MEASUREMENT), - sensor_config.get(CONF_STATE_CLASS), sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], @@ -137,9 +134,7 @@ async def async_setup_entry( async_add_entities(entities) -class ScrapeSensor( - CoordinatorEntity[ScrapeCoordinator], ManualTriggerEntity, SensorEntity -): +class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEntity): """Representation of a web scrape sensor.""" def __init__( @@ -147,8 +142,6 @@ def __init__( hass: HomeAssistant, coordinator: ScrapeCoordinator, trigger_entity_config: ConfigType, - unit_of_measurement: str | None, - state_class: str | None, select: str, attr: str | None, index: int, @@ -157,9 +150,7 @@ def __init__( ) -> None: """Initialize a web scrape sensor.""" CoordinatorEntity.__init__(self, coordinator) - ManualTriggerEntity.__init__(self, hass, trigger_entity_config) - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_state_class = state_class + ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config) self._select = select self._attr = attr self._index = index diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index edc33c9a8a05d6..a505867b3e82b5 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -154,6 +154,7 @@ class ShellyButton( entity_description: ShellyButtonDescription[ ShellyRpcCoordinator | ShellyBlockCoordinator ] + _attr_has_entity_name = True def __init__( self, @@ -166,7 +167,6 @@ def __init__( super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.device.name} {description.name}" self._attr_unique_id = f"{coordinator.mac}_{description.key}" self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a9712e62d25d02..d77a491661c640 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -130,6 +130,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -173,11 +174,6 @@ def unique_id(self) -> str: """Set unique id of entity.""" return self._unique_id - @property - def name(self) -> str: - """Name of entity.""" - return self.coordinator.name - @property def target_temperature(self) -> float | None: """Set target temperature.""" @@ -354,7 +350,7 @@ def _handle_coordinator_update(self) -> None: severity=ir.IssueSeverity.ERROR, translation_key="device_not_calibrated", translation_placeholders={ - "device_name": self.name, + "device_name": self.coordinator.name, "ip_address": self.coordinator.device.ip_address, }, ) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 1dc7573b738294..ac06624c7503b6 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -321,6 +321,8 @@ class RestEntityDescription(EntityDescription): class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -359,6 +361,8 @@ async def set_state(self, **kwargs: Any) -> Any: class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -462,6 +466,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" entity_description: RestEntityDescription + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index d55ffe0fd28fdd..b8f0c8e1744d59 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -42,7 +42,8 @@ def async_describe_shelly_click_event(event: Event) -> dict[str, str]: rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel-1}" - input_name = get_rpc_entity_name(rpc_coordinator.device, key) + if iname := get_rpc_entity_name(rpc_coordinator.device, key): + input_name = iname elif click_type in BLOCK_INPUTS_EVENTS_TYPES: block_coordinator = get_block_coordinator_by_device_id(hass, device_id) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 896ffd72327cfd..cd9980921c857d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -541,7 +541,8 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - available=lambda status: status["n_current"] is not None, + available=lambda status: (status and status["n_current"]) is not None, + removal_condition=lambda _config, status, _key: "n_current" not in status, entity_registry_enabled_default=False, ), "total_current": RpcSensorDescription( diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a66b77ed94b61d..1faa36ce118dbd 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -72,26 +72,26 @@ def get_block_entity_name( device: BlockDevice, block: Block | None, description: str | None = None, -) -> str: +) -> str | None: """Naming for block based switch and sensors.""" channel_name = get_block_channel_name(device, block) + if description and channel_name: + return f"{channel_name} {uncapitalize(description)}" if description: - return f"{channel_name} {description.lower()}" + return description return channel_name -def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: +def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None: """Get name based on device and channel name.""" - entity_name = device.name - if ( not block or block.type == "device" or get_number_of_channels(device, block) == 1 ): - return entity_name + return None assert block.channel @@ -108,7 +108,7 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: else: base = ord("1") - return f"{entity_name} channel {chr(int(block.channel)+base)}" + return f"Channel {chr(int(block.channel)+base)}" def is_block_momentary_input( @@ -285,32 +285,32 @@ def get_model_name(info: dict[str, Any]) -> str: return cast(str, MODEL_NAMES.get(info["type"], info["type"])) -def get_rpc_channel_name(device: RpcDevice, key: str) -> str: +def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None: """Get name based on device and channel name.""" key = key.replace("emdata", "em") if device.config.get("switch:0"): key = key.replace("input", "switch") - device_name = device.name entity_name: str | None = None if key in device.config: - entity_name = device.config[key].get("name", device_name) + entity_name = device.config[key].get("name") if entity_name is None: if key.startswith(("input:", "light:", "switch:")): - return f"{device_name} {key.replace(':', '_')}" - return device_name + return key.replace(":", " ").capitalize() return entity_name def get_rpc_entity_name( device: RpcDevice, key: str, description: str | None = None -) -> str: +) -> str | None: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) + if description and channel_name: + return f"{channel_name} {uncapitalize(description)}" if description: - return f"{channel_name} {description.lower()}" + return description return channel_name @@ -405,3 +405,8 @@ def mac_address_from_name(name: str) -> str | None: """Convert a name to a mac address.""" mac = name.partition(".")[0].partition("-")[-1] return mac.upper() if len(mac) == 12 else None + + +def uncapitalize(description: str) -> str: + """Uncapitalize the first letter of a description.""" + return description[:1].lower() + description[1:] diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f750b364106d8b..0b32b10f972ec7 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -19,12 +19,7 @@ SupportedDialect, get_instance, ) -from homeassistant.components.sensor import ( - CONF_STATE_CLASS, - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) +from homeassistant.components.sensor import CONF_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -44,7 +39,7 @@ from homeassistant.helpers.template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, - ManualTriggerEntity, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -56,6 +51,16 @@ _SQL_LAMBDA_CACHE: LRUCache = LRUCache(1000) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + async def async_setup_platform( hass: HomeAssistant, @@ -69,43 +74,29 @@ async def async_setup_platform( name: Template = conf[CONF_NAME] query_str: str = conf[CONF_QUERY] - unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) column_name: str = conf[CONF_COLUMN_NAME] unique_id: str | None = conf.get(CONF_UNIQUE_ID) db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL)) - device_class: SensorDeviceClass | None = conf.get(CONF_DEVICE_CLASS) - state_class: SensorStateClass | None = conf.get(CONF_STATE_CLASS) - availability: Template | None = conf.get(CONF_AVAILABILITY) - icon: Template | None = conf.get(CONF_ICON) - picture: Template | None = conf.get(CONF_PICTURE) if value_template is not None: value_template.hass = hass - trigger_entity_config = { - CONF_NAME: name, - CONF_DEVICE_CLASS: device_class, - CONF_UNIQUE_ID: unique_id, - } - if availability: - trigger_entity_config[CONF_AVAILABILITY] = availability - if icon: - trigger_entity_config[CONF_ICON] = icon - if picture: - trigger_entity_config[CONF_PICTURE] = picture + trigger_entity_config = {CONF_NAME: name} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in conf: + continue + trigger_entity_config[key] = conf[key] await async_setup_sensor( hass, trigger_entity_config, query_str, column_name, - unit, value_template, unique_id, db_url, True, - state_class, async_add_entities, ) @@ -118,11 +109,8 @@ async def async_setup_entry( db_url: str = resolve_db_url(hass, entry.options.get(CONF_DB_URL)) name: str = entry.options[CONF_NAME] query_str: str = entry.options[CONF_QUERY] - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) column_name: str = entry.options[CONF_COLUMN_NAME] - device_class: SensorDeviceClass | None = entry.options.get(CONF_DEVICE_CLASS, None) - state_class: SensorStateClass | None = entry.options.get(CONF_STATE_CLASS, None) value_template: Template | None = None if template is not None: @@ -135,23 +123,21 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = { - CONF_NAME: name_template, - CONF_DEVICE_CLASS: device_class, - CONF_UNIQUE_ID: entry.entry_id, - } + trigger_entity_config = {CONF_NAME: name_template} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in entry.options: + continue + trigger_entity_config[key] = entry.options[key] await async_setup_sensor( hass, trigger_entity_config, query_str, column_name, - unit, value_template, entry.entry_id, db_url, False, - state_class, async_add_entities, ) @@ -191,12 +177,10 @@ async def async_setup_sensor( trigger_entity_config: ConfigType, query_str: str, column_name: str, - unit: str | None, value_template: Template | None, unique_id: str | None, db_url: str, yaml: bool, - state_class: SensorStateClass | None, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SQL sensor.""" @@ -274,10 +258,8 @@ async def async_setup_sensor( sessmaker, query_str, column_name, - unit, value_template, yaml, - state_class, use_database_executor, ) ], @@ -317,7 +299,7 @@ def _generate_lambda_stmt(query: str) -> StatementLambdaElement: return lambda_stmt(lambda: text, lambda_cache=_SQL_LAMBDA_CACHE) -class SQLSensor(ManualTriggerEntity, SensorEntity): +class SQLSensor(ManualTriggerSensorEntity): """Representation of an SQL sensor.""" def __init__( @@ -326,17 +308,13 @@ def __init__( sessmaker: scoped_session, query: str, column: str, - unit: str | None, value_template: Template | None, yaml: bool, - state_class: SensorStateClass | None, use_database_executor: bool, ) -> None: """Initialize the SQL sensor.""" super().__init__(self.hass, trigger_entity_config) self._query = query - self._attr_native_unit_of_measurement = unit - self._attr_state_class = state_class self._template = value_template self._column_name = column self.sessionmaker = sessmaker diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index c59269d2e07737..3413c4ff5952d6 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -11,6 +11,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 3359706372e970..95a5515ab21160 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -10,8 +10,10 @@ AlertDict, ChannelContext, GrpcError, + LocationDict, ObstructionDict, StatusDict, + location_data, reboot, set_stow_state, status_data, @@ -28,6 +30,7 @@ class StarlinkData: """Contains data pulled from the Starlink system.""" + location: LocationDict status: StatusDict obstruction: ObstructionDict alert: AlertDict @@ -53,7 +56,10 @@ async def _async_update_data(self) -> StarlinkData: status = await self.hass.async_add_executor_job( status_data, self.channel_context ) - return StarlinkData(*status) + location = await self.hass.async_add_executor_job( + location_data, self.channel_context + ) + return StarlinkData(location, *status) except GrpcError as exc: raise UpdateFailed from exc diff --git a/homeassistant/components/starlink/device_tracker.py b/homeassistant/components/starlink/device_tracker.py new file mode 100644 index 00000000000000..eb832741f406ee --- /dev/null +++ b/homeassistant/components/starlink/device_tracker.py @@ -0,0 +1,73 @@ +"""Contains device trackers exposed by the Starlink integration.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.device_tracker import SourceType, TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkData +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkDeviceTrackerEntity(coordinator, description) + for description in DEVICE_TRACKERS + ) + + +@dataclass +class StarlinkDeviceTrackerEntityDescriptionMixin: + """Describes a Starlink device tracker.""" + + latitude_fn: Callable[[StarlinkData], float] + longitude_fn: Callable[[StarlinkData], float] + + +@dataclass +class StarlinkDeviceTrackerEntityDescription( + EntityDescription, StarlinkDeviceTrackerEntityDescriptionMixin +): + """Describes a Starlink button entity.""" + + +DEVICE_TRACKERS = [ + StarlinkDeviceTrackerEntityDescription( + key="device_location", + translation_key="device_location", + entity_registry_enabled_default=False, + latitude_fn=lambda data: data.location["latitude"], + longitude_fn=lambda data: data.location["longitude"], + ), +] + + +class StarlinkDeviceTrackerEntity(StarlinkEntity, TrackerEntity): + """A TrackerEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkDeviceTrackerEntityDescription + + @property + def source_type(self) -> SourceType | str: + """Return the source type, eg gps or router, of the device.""" + return SourceType.GPS + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self.entity_description.latitude_fn(self.coordinator.data) + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self.entity_description.longitude_fn(self.coordinator.data) diff --git a/homeassistant/components/starlink/diagnostics.py b/homeassistant/components/starlink/diagnostics.py index 10711e7155e25c..88e6485cf77d0e 100644 --- a/homeassistant/components/starlink/diagnostics.py +++ b/homeassistant/components/starlink/diagnostics.py @@ -10,7 +10,7 @@ from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -TO_REDACT = {"id"} +TO_REDACT = {"id", "latitude", "longitude", "altitude"} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index a9e50f5d39f394..0ec85c68956f10 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -44,6 +44,11 @@ "name": "Unexpected location" } }, + "device_tracker": { + "device_location": { + "name": "Device location" + } + }, "sensor": { "ping": { "name": "Ping" diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index f235256f7726a4..220bc4e31fb7ca 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.6.5"] + "requirements": ["HATasmota==0.7.0"] } diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 859b11ebd4c5db..e99106d09e89a6 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -32,6 +32,8 @@ class TasmotaEntity(Entity): """Base class for Tasmota entities.""" + _attr_has_entity_name = True + def __init__(self, tasmota_entity: HATasmotaEntity) -> None: """Initialize.""" self._tasmota_entity = tasmota_entity diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b403034208a37d..a3dd1fd1ef3d6d 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -459,9 +459,8 @@ def _update_brightness(self, brightness): ) self._brightness = None except ValueError: - _LOGGER.error( - "Template must supply an integer brightness from 0-255, or 'None'", - exc_info=True, + _LOGGER.exception( + "Template must supply an integer brightness from 0-255, or 'None'" ) self._brightness = None @@ -559,12 +558,9 @@ def _update_temperature(self, render): ) self._temperature = None except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._temperature = None @@ -620,12 +616,9 @@ def _update_max_mireds(self, render): return self._max_mireds = int(render) except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._max_mireds = None @@ -638,12 +631,9 @@ def _update_min_mireds(self, render): return self._min_mireds = int(render) except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._min_mireds = None diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 333aa0cd472e1e..ec77a2c8040cf1 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -7,6 +7,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, @@ -44,6 +46,7 @@ DOMAIN, MAX_FORECASTS, TMRW_ATTR_CONDITION, + TMRW_ATTR_DEW_POINT, TMRW_ATTR_HUMIDITY, TMRW_ATTR_OZONE, TMRW_ATTR_PRECIPITATION, @@ -138,6 +141,8 @@ def _forecast_dict( precipitation_probability: int | None, temp: float | None, temp_low: float | None, + humidity: float | None, + dew_point: float | None, wind_direction: float | None, wind_speed: float | None, ) -> Forecast: @@ -156,6 +161,8 @@ def _forecast_dict( ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, ATTR_FORECAST_NATIVE_TEMP: temp, ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, + ATTR_FORECAST_HUMIDITY: humidity, + ATTR_FORECAST_NATIVE_DEW_POINT: dew_point, ATTR_FORECAST_WIND_BEARING: wind_direction, ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } @@ -259,6 +266,8 @@ def _forecast(self, forecast_type: str) -> list[Forecast] | None: temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) temp_low = None + dew_point = values.get(TMRW_ATTR_DEW_POINT) + humidity = values.get(TMRW_ATTR_HUMIDITY) wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) wind_speed = values.get(TMRW_ATTR_WIND_SPEED) @@ -285,6 +294,8 @@ def _forecast(self, forecast_type: str) -> list[Forecast] | None: precipitation_probability, temp, temp_low, + humidity, + dew_point, wind_direction, wind_speed, ) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 043e074270e19d..c04676768c564b 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + ATTR_ACTIVITY_LABEL, ATTR_BUZZER, ATTR_CALORIES, ATTR_DAILY_GOAL, @@ -32,6 +33,7 @@ ATTR_MINUTES_DAY_SLEEP, ATTR_MINUTES_NIGHT_SLEEP, ATTR_MINUTES_REST, + ATTR_SLEEP_LABEL, ATTR_TRACKER_STATE, CLIENT, CLIENT_ID, @@ -281,10 +283,12 @@ def _send_activity_update(self, event: dict[str, Any]) -> None: def _send_wellness_update(self, event: dict[str, Any]) -> None: payload = { + ATTR_ACTIVITY_LABEL: event["wellness"]["activity_label"], ATTR_CALORIES: event["activity"]["calories"], ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], ATTR_MINUTES_REST: event["activity"]["minutes_rest"], + ATTR_SLEEP_LABEL: event["wellness"]["sleep_label"], } self._dispatch_tracker_event( TRACKER_WELLNESS_STATUS_UPDATED, event["pet_id"], payload diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 81936ae5d80ff0..254a8c274f3df0 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,6 +6,7 @@ RECONNECT_INTERVAL = timedelta(seconds=10) +ATTR_ACTIVITY_LABEL = "activity_label" ATTR_BUZZER = "buzzer" ATTR_CALORIES = "calories" ATTR_DAILY_GOAL = "daily_goal" @@ -15,6 +16,7 @@ ATTR_MINUTES_DAY_SLEEP = "minutes_day_sleep" ATTR_MINUTES_NIGHT_SLEEP = "minutes_night_sleep" ATTR_MINUTES_REST = "minutes_rest" +ATTR_SLEEP_LABEL = "sleep_label" ATTR_TRACKER_STATE = "tracker_state" # This client ID was issued by Tractive specifically for Home Assistant. diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 6891b74d31b313..0d48660680253b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -1,6 +1,7 @@ """Support for Tractive sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -19,15 +20,18 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import Trackables, TractiveClient from .const import ( + ATTR_ACTIVITY_LABEL, ATTR_CALORIES, ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, ATTR_MINUTES_DAY_SLEEP, ATTR_MINUTES_NIGHT_SLEEP, ATTR_MINUTES_REST, + ATTR_SLEEP_LABEL, ATTR_TRACKER_STATE, CLIENT, DOMAIN, @@ -53,11 +57,14 @@ class TractiveSensorEntityDescription( """Class describing Tractive sensor entities.""" hardware_sensor: bool = False + value_fn: Callable[[StateType], StateType] = lambda state: state class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" + entity_description: TractiveSensorEntityDescription + def __init__( self, client: TractiveClient, @@ -82,7 +89,9 @@ def __init__( @callback def handle_status_update(self, event: dict[str, Any]) -> None: """Handle status update.""" - self._attr_native_value = event[self.entity_description.key] + self._attr_native_value = self.entity_description.value_fn( + event[self.entity_description.key] + ) super().handle_status_update(event) @@ -159,6 +168,30 @@ def handle_status_update(self, event: dict[str, Any]) -> None: signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), + TractiveSensorEntityDescription( + key=ATTR_SLEEP_LABEL, + translation_key="sleep", + icon="mdi:sleep", + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, + value_fn=lambda state: state.lower() if isinstance(state, str) else state, + device_class=SensorDeviceClass.ENUM, + options=[ + "ok", + "good", + ], + ), + TractiveSensorEntityDescription( + key=ATTR_ACTIVITY_LABEL, + translation_key="activity", + icon="mdi:run", + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, + value_fn=lambda state: state.lower() if isinstance(state, str) else state, + device_class=SensorDeviceClass.ENUM, + options=[ + "ok", + "good", + ], + ), ) diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 4053d2658f5922..e315a8e601307b 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -30,15 +30,22 @@ } }, "sensor": { + "activity": { + "name": "Activity", + "state": { + "ok": "OK", + "good": "Good" + } + }, + "activity_time": { + "name": "Activity time" + }, "calories": { "name": "Calories burned" }, "daily_goal": { "name": "Daily goal" }, - "activity_time": { - "name": "Activity time" - }, "minutes_day_sleep": { "name": "Day sleep" }, @@ -48,6 +55,13 @@ "rest_time": { "name": "Rest time" }, + "sleep": { + "name": "Sleep", + "state": { + "ok": "OK", + "good": "Good" + } + }, "tracker_battery_level": { "name": "Tracker battery" }, diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 6b0660325f0360..0235f6156cc3ad 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -89,7 +89,7 @@ async def async_setup_entry( """Set up button platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return controller.register_platform_add_entities( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 12f2d49e416cc3..8c0696463c5413 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -13,6 +13,7 @@ from typing import Any from urllib.parse import urlparse +from aiounifi.interfaces.sites import Sites import voluptuous as vol from homeassistant import config_entries @@ -63,6 +64,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): VERSION = 1 + sites: Sites + @staticmethod @callback def async_get_options_flow( @@ -74,8 +77,6 @@ def async_get_options_flow( def __init__(self) -> None: """Initialize the UniFi Network flow.""" self.config: dict[str, Any] = {} - self.site_ids: dict[str, str] = {} - self.site_names: dict[str, str] = {} self.reauth_config_entry: config_entries.ConfigEntry | None = None self.reauth_schema: dict[vol.Marker, Any] = {} @@ -99,7 +100,8 @@ async def async_step_user( controller = await get_unifi_controller( self.hass, MappingProxyType(self.config) ) - sites = await controller.sites() + await controller.sites.update() + self.sites = controller.sites except AuthenticationRequired: errors["base"] = "faulty_credentials" @@ -108,12 +110,10 @@ async def async_step_user( errors["base"] = "service_unavailable" else: - self.site_ids = {site["_id"]: site["name"] for site in sites.values()} - self.site_names = {site["_id"]: site["desc"] for site in sites.values()} - if ( self.reauth_config_entry - and self.reauth_config_entry.unique_id in self.site_names + and self.reauth_config_entry.unique_id is not None + and self.reauth_config_entry.unique_id in self.sites ): return await self.async_step_site( {CONF_SITE_ID: self.reauth_config_entry.unique_id} @@ -148,7 +148,7 @@ async def async_step_site( """Select site to control.""" if user_input is not None: unique_id = user_input[CONF_SITE_ID] - self.config[CONF_SITE_ID] = self.site_ids[unique_id] + self.config[CONF_SITE_ID] = self.sites[unique_id].name config_entry = await self.async_set_unique_id(unique_id) abort_reason = "configuration_updated" @@ -171,19 +171,16 @@ async def async_step_site( await self.hass.config_entries.async_reload(config_entry.entry_id) return self.async_abort(reason=abort_reason) - site_nice_name = self.site_names[unique_id] + site_nice_name = self.sites[unique_id].description return self.async_create_entry(title=site_nice_name, data=self.config) - if len(self.site_names) == 1: - return await self.async_step_site( - {CONF_SITE_ID: next(iter(self.site_names))} - ) + if len(self.sites.values()) == 1: + return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))}) + site_names = {site.site_id: site.description for site in self.sites.values()} return self.async_show_form( step_id="site", - data_schema=vol.Schema( - {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} - ), + data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(site_names)}), ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 649d7c30fdb0b1..c1ffa0aa57d46a 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -87,9 +87,8 @@ def __init__( self.available = True self.wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] - self.site_id: str = "" - self._site_name: str | None = None - self._site_role: str | None = None + self.site = config_entry.data[CONF_SITE_ID] + self.is_admin = False self._cancel_heartbeat_check: CALLBACK_TYPE | None = None self._heartbeat_time: dict[str, datetime] = {} @@ -154,22 +153,6 @@ def host(self) -> str: host: str = self.config_entry.data[CONF_HOST] return host - @property - def site(self) -> str: - """Return the site of this config entry.""" - site_id: str = self.config_entry.data[CONF_SITE_ID] - return site_id - - @property - def site_name(self) -> str | None: - """Return the nice name of site.""" - return self._site_name - - @property - def site_role(self) -> str | None: - """Return the site user role of this controller.""" - return self._site_role - @property def mac(self) -> str | None: """Return the mac address of this controller.""" @@ -264,15 +247,8 @@ async def initialize(self) -> None: """Set up a UniFi Network instance.""" await self.api.initialize() - sites = await self.api.sites() - for site in sites.values(): - if self.site == site["name"]: - self.site_id = site["_id"] - self._site_name = site["desc"] - break - - description = await self.api.site_description() - self._site_role = description[0]["site_role"] + assert self.config_entry.unique_id is not None + self.is_admin = self.api.sites[self.config_entry.unique_id].role == "admin" # Restore clients that are not a part of active clients list. entity_registry = er.async_get(self.hass) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 3c72c06d6f290f..c01dc19307880e 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -94,7 +94,7 @@ async def async_get_config_entry_diagnostics( diag["config"] = async_redact_data( async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG ) - diag["site_role"] = controller.site_role + diag["role_is_admin"] = controller.is_admin diag["clients"] = { macs_to_redact[k]: async_redact_data( async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index 3ff893838c9459..8231b87ee85ba8 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -85,7 +85,7 @@ async def async_setup_entry( """Set up image platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return controller.register_platform_add_entities( diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 8f27263b288ba7..579e64c58621b4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==53"], + "requirements": ["aiounifi==55"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index a82b9e35d459d0..e2b4dda39123a1 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -274,7 +274,7 @@ async def async_setup_entry( """Set up switches for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return for mac in controller.option_block_clients: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 661a9016bdc7a8..6526a02da838ee 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -103,7 +103,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): def async_initiate_state(self) -> None: """Initiate entity state.""" self._attr_supported_features = UpdateEntityFeature.PROGRESS - if self.controller.site_role == "admin": + if self.controller.is_admin: self._attr_supported_features |= UpdateEntityFeature.INSTALL self.async_update_state(ItemEvent.ADDED, self._obj_id) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 283842adaaad94..7f832eb733f401 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -122,10 +122,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(manager.authenticate) except upcloud_api.UpCloudAPIError: - _LOGGER.error("Authentication failed", exc_info=True) + _LOGGER.exception("Authentication failed") return False except requests.exceptions.RequestException as err: - _LOGGER.error("Failed to connect", exc_info=True) + _LOGGER.exception("Failed to connect") raise ConfigEntryNotReady from err if entry.options.get(CONF_SCAN_INTERVAL): diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index b9d016295367ed..e23032e24fe75d 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -216,6 +216,13 @@ def installed_version(self) -> str | None: """Version installed and in use.""" return self._attr_installed_version + def _default_to_device_class_name(self) -> bool: + """Return True if an unnamed entity should be named by its device class. + + For updates this is True if the entity has a device class. + """ + return self.device_class is not None + @property def device_class(self) -> UpdateDeviceClass | None: """Return the class of this entity.""" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 096bc5163e8063..ce47931bc33489 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -91,13 +91,16 @@ async def _async_update_data(self) -> dict: raise UpdateFailed("Could not read overview") from err def unpack(overview: list, value: str) -> dict | list: - return next( - ( - item["data"]["installation"][value] - for item in overview - if value in item.get("data", {}).get("installation", {}) - ), - [], + return ( + next( + ( + item["data"]["installation"][value] + for item in overview + if value in item.get("data", {}).get("installation", {}) + ), + [], + ) + or [] ) # Store data in a way Home Assistant can easily consume it diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index a4b9e9d7f92e01..24f23b0da0ad5d 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -228,6 +228,14 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), + ViCareSensorEntityDescription( + key="gas_summary_consumption_heating_lastsevendays", + name="Heating gas consumption last seven days", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionHeatingLastSevenDays(), + unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentday", name="Hot water gas consumption current day", diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index f598906661c86e..1dbda62ab95379 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -164,12 +164,12 @@ def async_handle(self, msg: JsonValueType) -> None: if ( # Not using isinstance as we don't care about children # as these are always coming from JSON - type(msg) is not dict # pylint: disable=unidiomatic-typecheck + type(msg) is not dict # noqa: E721 or ( not (cur_id := msg.get("id")) - or type(cur_id) is not int # pylint: disable=unidiomatic-typecheck + or type(cur_id) is not int # noqa: E721 or not (type_ := msg.get("type")) - or type(type_) is not str # pylint: disable=unidiomatic-typecheck + or type(type_) is not str # noqa: E721 ) ): self.logger.error("Received invalid command: %s", msg) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index d1666fa9097f9f..4c3835431254ed 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -4,8 +4,13 @@ from datetime import date, timedelta from typing import Any -import holidays -from holidays import DateLike, HolidayBase +from holidays import ( + DateLike, + HolidayBase, + __version__ as python_holidays_version, + country_holidays, + list_supported_countries, +) import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -43,7 +48,6 @@ def valid_country(value: Any) -> str: """Validate that the given country is supported.""" value = cv.string(value) - all_supported_countries = holidays.list_supported_countries() try: raw_value = value.encode("utf-8") @@ -53,7 +57,7 @@ def valid_country(value: Any) -> str: ) from err if not raw_value: raise vol.Invalid("Country name or the abbreviation must not be empty.") - if value not in all_supported_countries: + if value not in list_supported_countries(): raise vol.Invalid("Country is not supported.") return value @@ -123,17 +127,17 @@ async def async_setup_entry( province: str | None = entry.options.get(CONF_PROVINCE) sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] - - cls: HolidayBase = getattr(holidays, country) year: int = (dt_util.now() + timedelta(days=days_offset)).year - if province and province not in cls.subdivisions: + if country and country not in list_supported_countries(): + LOGGER.error("There is no country %s", country) + return + + if province and province not in list_supported_countries()[country]: LOGGER.error("There is no subdivision %s in country %s", province, country) return - obj_holidays = cls( - subdiv=province, years=year, language=cls.default_language - ) # type: ignore[operator] + obj_holidays: HolidayBase = country_holidays(country, subdiv=province, years=year) # Add custom holidays try: @@ -209,7 +213,7 @@ def __init__( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="python-holidays", - model=holidays.__version__, + model=python_holidays_version, name=name, ) diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 15e04ffca93e3a..54c6196b75b8c4 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -3,8 +3,7 @@ from typing import Any -import holidays -from holidays import HolidayBase, list_supported_countries +from holidays import HolidayBase, country_holidays, list_supported_countries import voluptuous as vol from homeassistant.config_entries import ( @@ -77,12 +76,14 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: if dt_util.parse_date(add_date) is None: raise AddDatesError("Incorrect date") - cls: HolidayBase = getattr(holidays, user_input[CONF_COUNTRY]) + cls: HolidayBase = country_holidays(user_input[CONF_COUNTRY]) year: int = dt_util.now().year - - obj_holidays = cls( - subdiv=user_input.get(CONF_PROVINCE), years=year, language=cls.default_language - ) # type: ignore[operator] + obj_holidays: HolidayBase = country_holidays( + user_input[CONF_COUNTRY], + subdiv=user_input.get(CONF_PROVINCE), + years=year, + language=cls.default_language, + ) for remove_date in user_input[CONF_REMOVE_HOLIDAYS]: if dt_util.parse_date(remove_date) is None: diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 776f6c6e20f04a..1a5c7ae39a2d4a 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -115,8 +115,8 @@ def update(self) -> None: start = int(time.time()) resource = ( "https://www.worldtides.info/api?extremes&length=86400" - "&key={}&lat={}&lon={}&start={}" - ).format(self._key, self._lat, self._lon, start) + f"&key={self._key}&lat={self._lat}&lon={self._lon}&start={start}" + ) try: self.data = requests.get(resource, timeout=10).json() diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 763742cce705ff..830d8d9f69efe8 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -1,11 +1,14 @@ """The yale_smart_alarm component.""" from __future__ import annotations +from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import entity_registry as er -from .const import COORDINATOR, DOMAIN, PLATFORMS +from .const import COORDINATOR, DOMAIN, LOGGER, PLATFORMS from .coordinator import YaleDataUpdateCoordinator @@ -39,3 +42,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return True return False + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + if config_entry_default_code := entry.options.get(CONF_CODE): + entity_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) + for entity in entries: + if entity.entity_id.startswith("lock"): + entity_reg.async_update_entity_options( + entity.entity_id, + LOCK_DOMAIN, + {CONF_DEFAULT_CODE: config_entry_default_code}, + ) + new_options = entry.options.copy() + del new_options[CONF_CODE] + + hass.config_entries.async_update_entry(entry, options=new_options) + + entry.version = 2 + + LOGGER.info("Migration to version %s successful", entry.version) + + return True diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index a2462df41cb0f6..ff813d43d78479 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -9,7 +9,7 @@ from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yale integration.""" - VERSION = 1 + VERSION = 2 entry: ConfigEntry | None @@ -155,32 +155,22 @@ async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage Yale options.""" - errors = {} + errors: dict[str, Any] = {} if user_input: - if len(user_input.get(CONF_CODE, "")) not in [ - 0, - user_input[CONF_LOCK_CODE_DIGITS], - ]: - errors["base"] = "code_format_mismatch" - else: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Optional( - CONF_CODE, + CONF_LOCK_CODE_DIGITS, description={ - "suggested_value": self.entry.options.get(CONF_CODE) + "suggested_value": self.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS + ) }, - ): str, - vol.Optional( - CONF_LOCK_CODE_DIGITS, - default=self.entry.options.get( - CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS - ), ): int, } ), diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 397a9cc8db1909..50d7b28c52b4bc 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -5,7 +5,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CODE, CONF_CODE +from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -52,9 +52,7 @@ def __init__( async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" - code: str | None = kwargs.get( - ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE) - ) + code: str | None = kwargs.get(ATTR_CODE) return await self.async_set_lock("unlocked", code) async def async_lock(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index ec0c5d0702a914..a51d151d7d9248 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -31,13 +31,9 @@ "step": { "init": { "data": { - "code": "Default code for locks, used if none is given", "lock_code_digits": "Number of digits in PIN code for locks" } } - }, - "error": { - "code_format_mismatch": "The code does not match the required number of digits" } }, "entity": { diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index e68c51cd7eb472..a9fd20ce241062 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -17,7 +17,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: devices = await pyzerproc.discover() return len(devices) > 0 except pyzerproc.ZerprocException: - _LOGGER.error("Unable to discover nearby Zerproc devices", exc_info=True) + _LOGGER.exception("Unable to discover nearby Zerproc devices") return False diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7de32dc5071c8e..0bfbf362eb38a6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ "cloudflare", "co2signal", "coinbase", + "comelit", "control4", "coolmaster", "cpuspeed", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ed51bcc7dbf995..40883ef3d7c65d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -883,6 +883,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "comelit": { + "name": "Comelit SimpleHome", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "comfoconnect": { "name": "Zehnder ComfoAir Q", "integration_type": "hub", diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 122fd752a84366..5e0d66e0a9a1a5 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -586,7 +586,7 @@ def string(value: Any) -> str: raise vol.Invalid("string value is None") # This is expected to be the most common case, so check it first. - if type(value) is str: # pylint: disable=unidiomatic-typecheck + if type(value) is str: # noqa: E721 return value if isinstance(value, template_helper.ResultWrapper): @@ -1122,6 +1122,7 @@ def raise_issue() -> None: # pylint: disable-next=import-outside-toplevel from .issue_registry import IssueSeverity, async_create_issue + # HomeAssistantError is raised if called from the wrong thread with contextlib.suppress(HomeAssistantError): hass = async_get_hass() async_create_issue( diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 30866ccf7cd87e..27d568a13dea0c 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -95,16 +95,18 @@ async def _async_migrate_func( class IssueRegistry: """Class to hold a registry of issues.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, *, read_only: bool = False) -> None: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} + self._read_only = read_only self._store = IssueRegistryStore( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, atomic_writes=True, minor_version=STORAGE_VERSION_MINOR, + read_only=read_only, ) @callback @@ -278,10 +280,10 @@ def async_get(hass: HomeAssistant) -> IssueRegistry: return cast(IssueRegistry, hass.data[DATA_REGISTRY]) -async def async_load(hass: HomeAssistant) -> None: +async def async_load(hass: HomeAssistant, *, read_only: bool = False) -> None: """Load issue registry.""" assert DATA_REGISTRY not in hass.data - hass.data[DATA_REGISTRY] = IssueRegistry(hass) + hass.data[DATA_REGISTRY] = IssueRegistry(hass, read_only=read_only) await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 74823dea95335d..3eb537f96497d0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -597,7 +597,7 @@ async def async_get_all_descriptions( ints_or_excs = await async_get_integrations(hass, missing) integrations: list[Integration] = [] for domain, int_or_exc in ints_or_excs.items(): - if type(int_or_exc) is Integration: # pylint: disable=unidiomatic-typecheck + if type(int_or_exc) is Integration: # noqa: E721 integrations.append(int_or_exc) continue if TYPE_CHECKING: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index dd394c84f9111d..c83481365abd50 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -93,6 +93,7 @@ def __init__( atomic_writes: bool = False, encoder: type[JSONEncoder] | None = None, minor_version: int = 1, + read_only: bool = False, ) -> None: """Initialize storage class.""" self.version = version @@ -107,6 +108,7 @@ def __init__( self._load_task: asyncio.Future[_T | None] | None = None self._encoder = encoder self._atomic_writes = atomic_writes + self._read_only = read_only @property def path(self): @@ -344,6 +346,9 @@ async def _async_handle_write_data(self, *_args): self._data = None + if self._read_only: + return + try: await self._async_write_data(self.path, data) except (json_util.SerializationError, WriteError) as err: diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 07e68152d64b17..70a0ee1d16c39d 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -486,7 +486,7 @@ class TriggerBaseEntity(Entity): def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the entity.""" self.hass = hass @@ -623,7 +623,7 @@ class ManualTriggerEntity(TriggerBaseEntity): def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the entity.""" TriggerBaseEntity.__init__(self, hass, config) @@ -655,13 +655,13 @@ def _process_manual_data(self, value: Any | None = None) -> None: self._render_templates(variables) -class ManualTriggerSensorEntity(ManualTriggerEntity): +class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity): """Template entity based on manual trigger data for sensor.""" def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the sensor entity.""" ManualTriggerEntity.__init__(self, hass, config) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bac607545e6557..78b14aaa590be5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,13 +16,13 @@ bluetooth-data-tools==1.8.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.91.2 +dbus-fast==1.92.0 fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 5c81c4664da98a..38fa9cc2463f29 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -19,6 +19,7 @@ area_registry as ar, device_registry as dr, entity_registry as er, + issue_registry as ir, ) from homeassistant.helpers.check_config import async_check_ha_config_file from homeassistant.util.yaml import Secrets @@ -237,6 +238,7 @@ async def async_check_config(config_dir): await ar.async_load(hass) await dr.async_load(hass) await er.async_load(hass) + await ir.async_load(hass, read_only=True) components = await async_check_ha_config_file(hass) await hass.async_stop(force=True) return components diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 724c3ebf8d3513..60aa920ed6a026 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -42,7 +42,7 @@ def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayTy """Parse JSON data and ensure result is a list.""" value: JsonValueType = json_loads(__obj) # Avoid isinstance overhead as we are not interested in list subclasses - if type(value) is list: # pylint: disable=unidiomatic-typecheck + if type(value) is list: # noqa: E721 return value raise ValueError(f"Expected JSON to be parsed as a list got {type(value)}") @@ -51,7 +51,7 @@ def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObject """Parse JSON data and ensure result is a dictionary.""" value: JsonValueType = json_loads(__obj) # Avoid isinstance overhead as we are not interested in dict subclasses - if type(value) is dict: # pylint: disable=unidiomatic-typecheck + if type(value) is dict: # noqa: E721 return value raise ValueError(f"Expected JSON to be parsed as a dict got {type(value)}") @@ -89,7 +89,7 @@ def load_json_array( default = [] value: JsonValueType = load_json(filename, default=default) # Avoid isinstance overhead as we are not interested in list subclasses - if type(value) is list: # pylint: disable=unidiomatic-typecheck + if type(value) is list: # noqa: E721 return value _LOGGER.exception( "Expected JSON to be parsed as a list got %s in: %s", {type(value)}, filename @@ -108,7 +108,7 @@ def load_json_object( default = {} value: JsonValueType = load_json(filename, default=default) # Avoid isinstance overhead as we are not interested in dict subclasses - if type(value) is dict: # pylint: disable=unidiomatic-typecheck + if type(value) is dict: # noqa: E721 return value _LOGGER.exception( "Expected JSON to be parsed as a dict got %s in: %s", {type(value)}, filename diff --git a/pyproject.toml b/pyproject.toml index 4e477440cde1c5..f8753e3468035d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,6 +266,7 @@ disable = [ "missing-module-docstring", # D100 "multiple-imports", #E401 "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 "superfluous-parens", # UP034 "ungrouped-imports", # I001 "unidiomatic-typecheck", # E721 @@ -488,6 +489,7 @@ select = [ "SIM401", # Use get from dict with default instead of an if block "T100", # Trace found: {name} used "T20", # flake8-print + "TID251", # Banned imports "TRY004", # Prefer TypeError exception for invalid type "TRY200", # Use raise from to specify exception cause "TRY302", # Remove exception handler; error is immediately re-raised @@ -531,6 +533,9 @@ voluptuous = "vol" [tool.ruff.flake8-pytest-style] fixture-parentheses = false +[tool.ruff.flake8-tidy-imports.banned-api] +"pytz".msg = "use zoneinfo instead" + [tool.ruff.isort] force-sort-within-sections = true known-first-party = [ diff --git a/requirements_all.txt b/requirements_all.txt index bfb333dfaf1427..6ca0c5769b292e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.6.5 +HATasmota==0.7.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -208,6 +208,9 @@ aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 +# homeassistant.components.comelit +aiocomelit==0.0.5 + # homeassistant.components.dhcp aiodiscover==1.4.16 @@ -357,7 +360,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==53 +aiounifi==55 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -632,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.91.2 +dbus-fast==1.92.0 # homeassistant.components.debugpy debugpy==1.6.7 @@ -985,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 @@ -1365,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.29 +opower==0.0.30 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dace2048da1b8..d902e9303c5ee6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.6.5 +HATasmota==0.7.0 # homeassistant.components.doods # homeassistant.components.generic @@ -189,6 +189,9 @@ aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 +# homeassistant.components.comelit +aiocomelit==0.0.5 + # homeassistant.components.dhcp aiodiscover==1.4.16 @@ -332,7 +335,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==53 +aiounifi==55 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -512,7 +515,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.91.2 +dbus-fast==1.92.0 # homeassistant.components.debugpy debugpy==1.6.7 @@ -768,7 +771,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 @@ -1031,7 +1034,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.29 +opower==0.0.30 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e91cbe1ff6271a..844d796e7af72a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,5 +2,5 @@ black==23.7.0 codespell==2.2.2 -ruff==0.0.280 +ruff==0.0.285 yamllint==1.32.0 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4515f52d8a3c0c..4a15acb2d1d5ef 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -397,4 +397,5 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] + manifests_resorted, stdout=subprocess.DEVNULL, + check=True, ) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 2d4454c254b580..42a8355db5926e 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -77,11 +77,13 @@ def main(): pipe_null = {} if args.develop else {"stdout": subprocess.DEVNULL} print("Running hassfest to pick up new information.") - subprocess.run(["python", "-m", "script.hassfest"], **pipe_null) + subprocess.run(["python", "-m", "script.hassfest"], **pipe_null, check=True) print() print("Running gen_requirements_all to pick up new information.") - subprocess.run(["python", "-m", "script.gen_requirements_all"], **pipe_null) + subprocess.run( + ["python", "-m", "script.gen_requirements_all"], **pipe_null, check=True + ) print() print("Running script/translations_develop to pick up new translation strings.") @@ -95,13 +97,16 @@ def main(): info.domain, ], **pipe_null, + check=True, ) print() if args.develop: print("Running tests") print(f"$ pytest -vvv tests/components/{info.domain}") - subprocess.run(["pytest", "-vvv", f"tests/components/{info.domain}"]) + subprocess.run( + ["pytest", "-vvv", f"tests/components/{info.domain}"], check=True + ) print() docs.print_relevant_docs(args.template, info) diff --git a/script/translations/download.py b/script/translations/download.py index 6d4ce91263aa73..bcab3b511c3d2f 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -44,7 +44,8 @@ def run_download_docker(): "json", "--unzip-to", "/opt/dest", - ] + ], + check=False, ) print() diff --git a/script/translations/upload.py b/script/translations/upload.py index 02d964a94c99b4..1a1819af863a9f 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -42,6 +42,7 @@ def run_upload_docker(): "--convert-placeholders=false", "--replace-modified", ], + check=False, ) print() diff --git a/script/translations/util.py b/script/translations/util.py index 0c8c8a2a30f784..9f41253fa021ae 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -48,7 +48,9 @@ def get_current_branch(): """Get current branch.""" return ( subprocess.run( - ["git", "rev-parse", "--abbrev-ref", "HEAD"], stdout=subprocess.PIPE + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + stdout=subprocess.PIPE, + check=True, ) .stdout.decode() .strip() diff --git a/script/version_bump.py b/script/version_bump.py index 4a38adbd677afc..ae01b1e6bedb48 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -161,7 +161,10 @@ def main(): ) arguments = parser.parse_args() - if arguments.commit and subprocess.run(["git", "diff", "--quiet"]).returncode == 1: + if ( + arguments.commit + and subprocess.run(["git", "diff", "--quiet"], check=False).returncode == 1 + ): print("Cannot use --commit because git is dirty.") return @@ -177,7 +180,7 @@ def main(): if not arguments.commit: return - subprocess.run(["git", "commit", "-nam", f"Bumped version to {bumped}"]) + subprocess.run(["git", "commit", "-nam", f"Bumped version to {bumped}"], check=True) def test_bump_version(): diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index d0330952f04ffb..58835e37973350 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -311,6 +311,12 @@ }), 'type': , }), + dict({ + 'data': dict({ + 'timestamp': 0, + }), + 'type': , + }), dict({ 'data': dict({ 'stt_output': dict({ diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 44e448aa785bee..184f479f83057e 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -40,7 +40,7 @@ async def test_pipeline_from_audio_stream_auto( In this test, no pipeline is specified. """ - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -79,7 +79,7 @@ async def test_pipeline_from_audio_stream_legacy( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -139,7 +139,7 @@ async def test_pipeline_from_audio_stream_entity( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -199,7 +199,7 @@ async def test_pipeline_from_audio_stream_no_stt( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -257,7 +257,7 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( In this test, the pipeline does not exist. """ - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -294,7 +294,7 @@ async def test_pipeline_from_audio_stream_wake_word( ) -> None: """Test creating a pipeline from an audio stream with wake word.""" - events = [] + events: list[assist_pipeline.PipelineEvent] = [] # [0, 1, ...] wake_chunk_1 = bytes(it.islice(it.cycle(range(256)), BYTES_ONE_SECOND)) diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 45dd9d6afe146f..02aebf3ce92294 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -100,7 +100,7 @@ async def async_get_events( async def fire_time(self, trigger_time: datetime.datetime) -> None: """Fire an alarm and wait.""" - _LOGGER.debug(f"Firing alarm @ {dt_util.as_local(trigger_time)}") + _LOGGER.debug("Firing alarm @ %s", dt_util.as_local(trigger_time)) self.freezer.move_to(trigger_time) async_fire_time_changed(self.hass, trigger_time) await self.hass.async_block_till_done() diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index bc5d149e914d89..9207c1fef2c92b 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -16,7 +16,7 @@ @pytest.fixture(name="mocked_cloud") -def mocked_cloud_object(hass: HomeAssistant) -> Cloud: +async def mocked_cloud_object(hass: HomeAssistant) -> Cloud: """Mock cloud object.""" return Mock( accounts_server="accounts.nabucasa.com", diff --git a/tests/components/comelit/__init__.py b/tests/components/comelit/__init__.py new file mode 100644 index 00000000000000..916a684de4b722 --- /dev/null +++ b/tests/components/comelit/__init__.py @@ -0,0 +1 @@ +"""Tests for the Comelit SimpleHome integration.""" diff --git a/tests/components/comelit/const.py b/tests/components/comelit/const.py new file mode 100644 index 00000000000000..36955b0b0a96b3 --- /dev/null +++ b/tests/components/comelit/const.py @@ -0,0 +1,16 @@ +"""Common stuff for Comelit SimpleHome tests.""" +from homeassistant.components.comelit.const import DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PIN + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PIN: "1234", + } + ] + } +} + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py new file mode 100644 index 00000000000000..2fb9e836efb92d --- /dev/null +++ b/tests/components/comelit/test_config_flow.py @@ -0,0 +1,154 @@ +"""Tests for Comelit SimpleHome config flow.""" +from unittest.mock import patch + +from aiocomelit import CannotAuthenticate, CannotConnect +import pytest + +from homeassistant.components.comelit.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_user(hass: HomeAssistant) -> None: + """Test starting a flow by user.""" + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch( + "homeassistant.components.comelit.async_setup_entry" + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PIN] == "1234" + assert not result["result"].unique_id + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a flow by user with a connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == error + + +async def test_reauth_successful(hass: HomeAssistant) -> None: + """Test starting a reauthentication flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch("homeassistant.components.comelit.async_setup_entry"), patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "other_fake_pin", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a reauthentication flow but no connection found.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch( + "homeassistant.components.comelit.async_setup_entry" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "other_fake_pin", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == error diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index acfb11ced0a4f5..b42b40b2739156 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -314,7 +314,7 @@ async def test_discover_lights(hass: HomeAssistant, hue_client) -> None: await hass.async_block_till_done() result_json = await async_get_lights(hue_client) - assert "1" not in result_json.keys() + assert "1" not in result_json devices = {val["uniqueid"] for val in result_json.values()} assert "00:2f:d2:31:ce:c5:55:cc-ee" not in devices # light.ceiling_lights diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index a586d5fe27d386..b042e3daa6c3c0 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -59,7 +59,12 @@ async def test_hmip_heating_group_heat( assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "Winter"] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "STD", + "Winter", + ] service_call_counter = len(hmip_device.mock_calls) @@ -219,6 +224,21 @@ async def test_hmip_heating_group_heat( # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" + assert ha_state.state == HVACMode.AUTO + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_ECO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 25 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("ECO",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + assert ha_state.state == HVACMode.AUTO + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.1) ha_state = hass.states.get(entity_id) @@ -376,7 +396,12 @@ async def test_hmip_heating_group_heat_with_switch( assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 43 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "P2"] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "STD", + "P2", + ] async def test_hmip_heating_group_heat_with_radiator( @@ -401,7 +426,11 @@ async def test_hmip_heating_group_heat_with_radiator( assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes[ATTR_PRESET_MODE] is None - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + PRESET_ECO, + ] async def test_hmip_climate_services( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 35c01ec478b816..e305a0294c8ea7 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -181,7 +181,6 @@ async def test_nan_validator() -> None: CONF_COUNT: 2, CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">i", - CONF_SWAP: CONF_SWAP_BYTE, }, ], ) @@ -239,6 +238,22 @@ async def test_ok_struct_validator(do_config) -> None: CONF_STRUCTURE: ">f", CONF_SLAVE_COUNT: 5, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.STRING, + CONF_SLAVE_COUNT: 2, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.INT16, + CONF_SWAP: CONF_SWAP_WORD, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 2, + CONF_SLAVE_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, + }, ], ) async def test_exception_struct_validator(do_config) -> None: diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 06b0b68a74644d..8481adc0d0f3dc 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -246,7 +246,7 @@ async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None: }, ] }, - f"Error in sensor {TEST_ENTITY_NAME} swap(word) not possible due to the registers count: 1, needed: 2", + f"{TEST_ENTITY_NAME}: `structure` illegal with `swap`", ), ], ) @@ -505,7 +505,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102], False, - str(int(0x0102)), + str(0x0102), ), ( { @@ -514,7 +514,7 @@ async def test_config_wrong_struct_sensor( }, [0x0201], False, - str(int(0x0102)), + str(0x0102), ), ( { @@ -523,7 +523,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x02010403)), + str(0x02010403), ), ( { @@ -532,7 +532,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x03040102)), + str(0x03040102), ), ( { @@ -541,25 +541,25 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x04030201)), + str(0x04030201), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_MAX_VALUE: int(0x02010400), + CONF_MAX_VALUE: 0x02010400, }, [0x0201, 0x0403], False, - str(int(0x02010400)), + str(0x02010400), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_MIN_VALUE: int(0x02010404), + CONF_MIN_VALUE: 0x02010404, }, [0x0201, 0x0403], False, - str(int(0x02010404)), + str(0x02010404), ), ( { @@ -573,20 +573,20 @@ async def test_config_wrong_struct_sensor( ( { CONF_DATA_TYPE: DataType.INT32, - CONF_ZERO_SUPPRESS: int(0x00000001), + CONF_ZERO_SUPPRESS: 0x00000001, }, [0x0000, 0x0002], False, - str(int(0x00000002)), + str(0x00000002), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_ZERO_SUPPRESS: int(0x00000002), + CONF_ZERO_SUPPRESS: 0x00000002, }, [0x0000, 0x0002], False, - str(int(0)), + str(0), ), ( { @@ -615,9 +615,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: CONF_ADDRESS: 51, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DATA_TYPE: DataType.UINT32, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, + CONF_SCAN_INTERVAL: 1, }, ], }, @@ -689,17 +687,184 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: ) async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: """Run test for sensor.""" - assert hass.states.get(ENTITY_ID).state == expected[0] entity_registry = er.async_get(hass) - - for i in range(1, len(expected)): - entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") - assert hass.states.get(entity_id).state == expected[i] - unique_id = f"{SLAVE_UNIQUE_ID}_{i}" + for i in range(0, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") + unique_id = f"{SLAVE_UNIQUE_ID}" + if i: + entity_id = f"{entity_id}_{i}" + unique_id = f"{unique_id}_{i}" entry = entity_registry.async_get(entity_id) + state = hass.states.get(entity_id).state + assert state == expected[i] assert entry.unique_id == unique_id +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + ("config_addon", "register_words", "do_exception", "expected"), + [ + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_BYTE, + CONF_DATA_TYPE: DataType.UINT16, + }, + [0x0102], + False, + [str(0x0201)], + ), + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + False, + [str(0x03040102)], + ), + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT64, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(0x0708050603040102)], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT16, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0102, 0x0304], + False, + [str(0x0201), str(0x0403)], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SWAP: CONF_SWAP_WORD, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(0x03040102), str(0x07080506)], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT64, + CONF_SWAP: CONF_SWAP_WORD, + }, + [0x0102, 0x0304, 0x0506, 0x0708, 0x0901, 0x0902, 0x0903, 0x0904], + False, + [str(0x0708050603040102), str(0x0904090309020901)], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT16, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(0x0201), str(0x0403), str(0x0605), str(0x0807)], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SWAP: CONF_SWAP_WORD, + }, + [ + 0x0102, + 0x0304, + 0x0506, + 0x0708, + 0x090A, + 0x0B0C, + 0x0D0E, + 0x0F00, + ], + False, + [ + str(0x03040102), + str(0x07080506), + str(0x0B0C090A), + str(0x0F000D0E), + ], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT64, + CONF_SWAP: CONF_SWAP_WORD, + }, + [ + 0x0601, + 0x0602, + 0x0603, + 0x0604, + 0x0701, + 0x0702, + 0x0703, + 0x0704, + 0x0801, + 0x0802, + 0x0803, + 0x0804, + 0x0901, + 0x0902, + 0x0903, + 0x0904, + ], + False, + [ + str(0x0604060306020601), + str(0x0704070307020701), + str(0x0804080308020801), + str(0x0904090309020901), + ], + ), + ], +) +async def test_slave_swap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: + """Run test for sensor.""" + for i in range(0, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") + if i: + entity_id = f"{entity_id}_{i}" + state = hass.states.get(entity_id).state + assert state == expected[i] + + @pytest.mark.parametrize( "do_config", [ diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index e69839e6b16314..35fba9e2a0c5e3 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components import alarm_control_panel, mqtt +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.mqtt.alarm_control_panel import ( MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -74,6 +75,15 @@ CODE_NUMBER = "1234" CODE_TEXT = "HELLO_CODE" +DEFAULT_FEATURES = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + | AlarmControlPanelEntityFeature.TRIGGER +) + DEFAULT_CONFIG = { mqtt.DOMAIN: { alarm_control_panel.DOMAIN: { @@ -223,6 +233,89 @@ async def test_ignore_update_state_if_unknown_via_state_topic( assert hass.states.get(entity_id).state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("hass_config", "expected_features", "valid"), + [ + ( + DEFAULT_CONFIG, + DEFAULT_FEATURES, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": []},), + ), + AlarmControlPanelEntityFeature(0), + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home"]},), + ), + AlarmControlPanelEntityFeature.ARM_HOME, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home", "arm_away"]},), + ), + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": "invalid"},), + ), + None, + False, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["invalid"]},), + ), + None, + False, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home", "invalid"]},), + ), + None, + False, + ), + ], +) +async def test_supported_features( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_features: AlarmControlPanelEntityFeature | None, + valid: bool, +) -> None: + """Test conditional enablement of supported features.""" + if valid: + await mqtt_mock_entry() + assert ( + hass.states.get("alarm_control_panel.test").attributes["supported_features"] + == expected_features + ) + else: + with pytest.raises(AssertionError): + await mqtt_mock_entry() + + @pytest.mark.parametrize( ("hass_config", "service", "payload"), [ diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index c0d7a94de5b79f..e3a12a2c24ec80 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -41,6 +41,7 @@ from tests.common import ( MockConfigEntry, + MockEntity, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, @@ -417,6 +418,37 @@ async def test_value_template_value(hass: HomeAssistant) -> None: assert template_state_calls.call_count == 1 +async def test_value_template_fails( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the rendering of MQTT value template fails.""" + + # test rendering a value fails + entity = MockEntity(entity_id="sensor.test") + entity.hass = hass + tpl = template.Template("{{ value_json.some_var * 2 }}") + val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) + with pytest.raises(TypeError): + val_tpl.async_render_with_possible_json_value('{"some_var": null }') + await hass.async_block_till_done() + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " + "rendering template for entity 'sensor.test', " + "template: '{{ value_json.some_var * 2 }}'" + ) in caplog.text + caplog.clear() + with pytest.raises(TypeError): + val_tpl.async_render_with_possible_json_value( + '{"some_var": null }', default=100 + ) + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " + "rendering template for entity 'sensor.test', " + "template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: " + '{"some_var": null }' + ) in caplog.text + + async def test_service_call_without_topic_does_not_publish( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/nexia/fixtures/set_fan_speed_2293892.json b/tests/components/nexia/fixtures/set_fan_speed_2293892.json new file mode 100644 index 00000000000000..bad0fccb2adf27 --- /dev/null +++ b/tests/components/nexia/fixtures/set_fan_speed_2293892.json @@ -0,0 +1,3086 @@ +{ + "success": true, + "error": null, + "result": { + "id": 2293892, + "name": "Master Suite", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "0281B02C" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 73, + "status": "Cooling", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_on", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.69 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.5, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, + 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "Cooling", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + } +} diff --git a/tests/components/nexia/test_number.py b/tests/components/nexia/test_number.py new file mode 100644 index 00000000000000..7f4c5f92ab6916 --- /dev/null +++ b/tests/components/nexia/test_number.py @@ -0,0 +1,62 @@ +"""The number entity tests for the nexia platform.""" + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_create_fan_speed_number_entities(hass: HomeAssistant) -> None: + """Test creation of fan speed number entities.""" + + await async_init_integration(hass) + + state = hass.states.get("number.master_suite_fan_speed") + assert state.state == "35.0" + expected_attributes = { + "attribution": "Data provided by Trane Technologies", + "friendly_name": "Master Suite Fan speed", + "min": 35, + "max": 100, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("number.downstairs_east_wing_fan_speed") + assert state.state == "35.0" + expected_attributes = { + "attribution": "Data provided by Trane Technologies", + "friendly_name": "Downstairs East Wing Fan speed", + "min": 35, + "max": 100, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + +async def test_set_fan_speed(hass: HomeAssistant) -> None: + """Test setting fan speed.""" + + await async_init_integration(hass) + + state_before = hass.states.get("number.master_suite_fan_speed") + assert state_before.state == "35.0" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + blocking=True, + target={"entity_id": "number.master_suite_fan_speed"}, + ) + state = hass.states.get("number.master_suite_fan_speed") + assert state.state == "50.0" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 318a317fae4f75..d47e3fd3d6a951 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -22,6 +22,7 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" + set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" with mock_aiohttp_client() as mock_session, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): @@ -46,6 +47,10 @@ async def _raise_exception(*args, **kwargs): nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), ) + mock_session.post( + "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed", + text=load_fixture(set_fan_speed_fixture), + ) entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} ) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 37c0b175faa2f4..23758fe345d3f1 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -506,7 +506,7 @@ async def test_restore_number_save_state( assert state["entity_id"] == entity0.entity_id extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] assert extra_data == RESTORE_DATA - assert type(extra_data["native_value"]) == float + assert isinstance(extra_data["native_value"], float) @pytest.mark.parametrize( @@ -818,22 +818,22 @@ async def async_setup_entry_init( ), ) - # Unnamed sensor without device class -> no name + # Unnamed number without device class -> no name entity1 = NumberEntity() entity1.entity_id = "number.test1" - # Unnamed sensor with device class but has_entity_name False -> no name + # Unnamed number with device class but has_entity_name False -> no name entity2 = NumberEntity() entity2.entity_id = "number.test2" entity2._attr_device_class = NumberDeviceClass.TEMPERATURE - # Unnamed sensor with device class and has_entity_name True -> named + # Unnamed number with device class and has_entity_name True -> named entity3 = NumberEntity() entity3.entity_id = "number.test3" entity3._attr_device_class = NumberDeviceClass.TEMPERATURE entity3._attr_has_entity_name = True - # Unnamed sensor with device class and has_entity_name True -> named + # Unnamed number with device class and has_entity_name True -> named entity4 = NumberEntity() entity4.entity_id = "number.test4" entity4.entity_description = NumberEntityDescription( diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index dfda0b0d282c03..8dd8326ddd809b 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -157,7 +157,7 @@ async def test_openalpr_process_image( ] assert len(event_data) == 1 assert event_data[0]["plate"] == "H786P0J" - assert event_data[0]["confidence"] == float(90.436699) + assert event_data[0]["confidence"] == 90.436699 assert event_data[0]["entity_id"] == "image_processing.test_local" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 702fd313e6d470..242470fa8e791f 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -56,8 +56,8 @@ CONF_CODE: MOCK_CODE, } MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} -MOCK_UDP_PORT = int(987) -MOCK_TCP_PORT = int(997) +MOCK_UDP_PORT = 987 +MOCK_TCP_PORT = 997 MOCK_AUTO = {"Config Mode": "Auto Discover"} MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index c067f5dffc9175..ebc5089f884b54 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -165,7 +165,7 @@ async def test_rpc_binary_sensor( hass: HomeAssistant, mock_rpc_device, monkeypatch ) -> None: """Test RPC binary sensor.""" - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering" await init_integration(hass, 2) assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 8536c3d72e6d52..eb546ce58352ce 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -353,7 +353,7 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") is not None + assert hass.states.get("switch.test_name_test_switch_0") is not None # Wait for debouncer async_fire_time_changed( @@ -361,7 +361,7 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") is None + assert hass.states.get("switch.test_name_test_switch_0") is None async def test_rpc_reload_with_invalid_auth( @@ -588,7 +588,7 @@ async def test_rpc_reconnect_error( """Test RPC reconnect error.""" await init_integration(hass, 2) - assert hass.states.get("switch.test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr( @@ -605,7 +605,7 @@ async def test_rpc_reconnect_error( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0").state == STATE_UNAVAILABLE + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_UNAVAILABLE async def test_rpc_polling_connection_error( diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 08c0c76d35e6ed..56740981fc5f81 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -97,10 +97,10 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0", ATTR_POSITION: 50}, blocking=True, ) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get("cover.test_name_test_cover_0") assert state.attributes[ATTR_CURRENT_POSITION] == 50 mutate_rpc_device_status( @@ -109,11 +109,11 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_OPENING + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPENING mutate_rpc_device_status( monkeypatch, mock_rpc_device, "cover:0", "state", "closing" @@ -121,21 +121,21 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSING mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED async def test_rpc_device_no_cover_keys( @@ -144,7 +144,7 @@ async def test_rpc_device_no_cover_keys( """Test RPC device without cover keys.""" monkeypatch.delitem(mock_rpc_device.status, "cover:0") await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0") is None + assert hass.states.get("cover.test_name_test_cover_0") is None async def test_rpc_device_update( @@ -153,11 +153,11 @@ async def test_rpc_device_update( """Test RPC device update.""" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN async def test_rpc_device_no_position_control( @@ -168,4 +168,4 @@ async def test_rpc_device_no_position_control( monkeypatch, mock_rpc_device, "cover:0", "pos_control", False ) await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index be6e319c8ac72a..a62dfda82f9d03 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -175,7 +175,7 @@ async def test_sleeping_rpc_device_online_new_firmware( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload( @@ -198,7 +198,7 @@ async def test_entry_unload( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload_device_not_ready( @@ -226,7 +226,7 @@ async def test_entry_unload_not_connected( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON @@ -252,7 +252,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index 69d0fccf421b06..ab631516ec24a1 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -385,25 +385,25 @@ async def test_rpc_device_switch_type_lights_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, blocking=True, ) - assert hass.states.get("light.test_switch_0").state == STATE_ON + assert hass.states.get("light.test_name_test_switch_0").state == STATE_ON mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("light.test_switch_0").state == STATE_OFF + assert hass.states.get("light.test_name_test_switch_0").state == STATE_OFF async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC light.""" - entity_id = f"{LIGHT_DOMAIN}.test_light_0" + entity_id = f"{LIGHT_DOMAIN}.test_name_test_light_0" monkeypatch.delitem(mock_rpc_device.status, "switch:0") await init_integration(hass, 2) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index d87460fb17d0dc..fe79b1d010a1ab 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -262,7 +262,7 @@ async def test_block_sensor_unknown_value( async def test_rpc_sensor(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC sensor.""" - entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" + entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power" await init_integration(hass, 2) assert hass.states.get(entity_id).state == "85.3" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 7a709e0cc2e6f0..a93d752f9e2b18 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -149,20 +149,20 @@ async def test_rpc_device_services( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) - assert hass.states.get("switch.test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("switch.test_switch_0").state == STATE_OFF + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_OFF async def test_rpc_device_switch_type_lights_mode( @@ -173,7 +173,7 @@ async def test_rpc_device_switch_type_lights_mode( mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) await init_integration(hass, 2) - assert hass.states.get("switch.test_switch_0") is None + assert hass.states.get("switch.test_name_test_switch_0") is None @pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")]) @@ -188,7 +188,7 @@ async def test_rpc_set_state_errors( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -209,7 +209,7 @@ async def test_rpc_auth_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 1bf660deb2a4c5..53fc77ed6efbb4 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -58,7 +58,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Test name channel 1" + == "Channel 1" ) monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHEM-3") @@ -68,7 +68,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Test name channel A" + == "Channel A" ) monkeypatch.setitem( @@ -207,7 +207,7 @@ async def test_get_block_input_triggers(mock_block_device, monkeypatch) -> None: async def test_get_rpc_channel_name(mock_rpc_device) -> None: """Test get RPC channel name.""" assert get_rpc_channel_name(mock_rpc_device, "input:0") == "test switch_0" - assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name switch_3" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Switch 3" async def test_get_rpc_input_triggers(mock_rpc_device, monkeypatch) -> None: diff --git a/tests/components/starlink/fixtures/location_data_success.json b/tests/components/starlink/fixtures/location_data_success.json new file mode 100644 index 00000000000000..4d18d22d12ec55 --- /dev/null +++ b/tests/components/starlink/fixtures/location_data_success.json @@ -0,0 +1,5 @@ +{ + "latitude": 37.422, + "longitude": -122.084, + "altitude": 100 +} diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index dfc0d2415df6b0..d83451ecc1708d 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -8,11 +8,16 @@ "homeassistant.components.starlink.async_setup_entry", return_value=True ) -COORDINATOR_SUCCESS_PATCHER = patch( +STATUS_DATA_SUCCESS_PATCHER = patch( "homeassistant.components.starlink.coordinator.status_data", return_value=json.loads(load_fixture("status_data_success.json", "starlink")), ) +LOCATION_DATA_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.location_data", + return_value=json.loads(load_fixture("location_data_success.json", "starlink")), +) + DEVICE_FOUND_PATCHER = patch( "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" ) diff --git a/tests/components/starlink/snapshots/test_diagnostics.ambr b/tests/components/starlink/snapshots/test_diagnostics.ambr index 6f859aaf50d57c..3bb7f235017c12 100644 --- a/tests/components/starlink/snapshots/test_diagnostics.ambr +++ b/tests/components/starlink/snapshots/test_diagnostics.ambr @@ -16,6 +16,11 @@ 'alert_thermal_throttle': False, 'alert_unexpected_location': False, }), + 'location': dict({ + 'altitude': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), 'obstruction': dict({ 'raw_wedges_fraction_obstructed[]': list([ None, diff --git a/tests/components/starlink/test_diagnostics.py b/tests/components/starlink/test_diagnostics.py index 4bf8a619c88bc8..231b58a2d5e430 100644 --- a/tests/components/starlink/test_diagnostics.py +++ b/tests/components/starlink/test_diagnostics.py @@ -5,7 +5,7 @@ from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -23,7 +23,7 @@ async def test_diagnostics( data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 72d3be52b4a4fb..94a8a2a341b4ba 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -4,7 +4,7 @@ from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_successful_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -33,7 +33,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 6a82a0f0e736f1..2bfb4a9d5e2436 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -125,13 +125,13 @@ async def test_controlling_state_via_mqtt_switchname( ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -139,35 +139,35 @@ async def test_controlling_state_via_mqtt_switchname( async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"ON"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"OFF"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF # Test periodic state update async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"ON"}') - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"OFF"}') - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF # Test polled state update async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"ON"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"OFF"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF @@ -243,9 +243,9 @@ async def test_friendly_names( assert state.state == "unavailable" assert state.attributes.get("friendly_name") == "Tasmota binary_sensor 1" - state = hass.states.get("binary_sensor.beer") + state = hass.states.get("binary_sensor.tasmota_beer") assert state.state == "unavailable" - assert state.attributes.get("friendly_name") == "Beer" + assert state.attributes.get("friendly_name") == "Tasmota Beer" async def test_off_delay( diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 703dd2a1893309..a184f650faea81 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -129,7 +129,7 @@ async def help_test_availability_when_connection_lost( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test availability after MQTT disconnection. @@ -156,7 +156,7 @@ async def help_test_availability_when_connection_lost( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE # Disconnected from MQTT server -> state changed to unavailable @@ -165,7 +165,7 @@ async def help_test_availability_when_connection_lost( await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Reconnected to MQTT server -> state still unavailable @@ -174,7 +174,7 @@ async def help_test_availability_when_connection_lost( await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Receive LWT again @@ -184,7 +184,7 @@ async def help_test_availability_when_connection_lost( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE @@ -194,7 +194,7 @@ async def help_test_availability( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test availability. @@ -214,7 +214,7 @@ async def help_test_availability( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message( @@ -223,7 +223,7 @@ async def help_test_availability( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message( @@ -232,7 +232,7 @@ async def help_test_availability( config_get_state_offline(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE @@ -242,7 +242,7 @@ async def help_test_availability_discovery_update( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test update of discovered TasmotaAvailability. @@ -280,17 +280,17 @@ async def help_test_availability_discovery_update( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, online1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, offline1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Change availability settings @@ -302,13 +302,13 @@ async def help_test_availability_discovery_update( async_fire_mqtt_message(hass, availability_topic1, online2) async_fire_mqtt_message(hass, availability_topic2, online1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, availability_topic2, online2) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE @@ -390,8 +390,8 @@ async def help_test_discovery_removal( config2, sensor_config1=None, sensor_config2=None, - entity_id="test", - name="Test", + object_id="tasmota_test", + name="Tasmota Test", ): """Test removal of discovered entity.""" device_reg = dr.async_get(hass) @@ -416,11 +416,11 @@ async def help_test_discovery_removal( connections={(dr.CONNECTION_NETWORK_MAC, config1[CONF_MAC])} ) assert device_entry is not None - entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") + entity_entry = entity_reg.async_get(f"{domain}.{object_id}") assert entity_entry is not None # Verify state is added - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert state.name == name @@ -439,11 +439,11 @@ async def help_test_discovery_removal( connections={(dr.CONNECTION_NETWORK_MAC, config2[CONF_MAC])} ) assert device_entry is not None - entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") + entity_entry = entity_reg.async_get(f"{domain}.{object_id}") assert entity_entry is None # Verify state is removed - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is None @@ -455,8 +455,8 @@ async def help_test_discovery_update_unchanged( config, discovery_update, sensor_config=None, - entity_id="test", - name="Test", + object_id="tasmota_test", + name="Tasmota Test", ): """Test update of discovered component with and without changes. @@ -479,7 +479,7 @@ async def help_test_discovery_update_unchanged( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert state.name == name @@ -538,7 +538,13 @@ async def help_test_discovery_device_remove( async def help_test_entity_id_update_subscriptions( - hass, mqtt_mock, domain, config, topics=None, sensor_config=None, entity_id="test" + hass, + mqtt_mock, + domain, + config, + topics=None, + sensor_config=None, + object_id="tasmota_test", ): """Test MQTT subscriptions are managed when entity_id is updated.""" entity_reg = er.async_get(hass) @@ -562,7 +568,7 @@ async def help_test_entity_id_update_subscriptions( topics = [get_topic_tele_state(config), get_topic_tele_will(config)] assert len(topics) > 0 - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert mqtt_mock.async_subscribe.call_count == len(topics) for topic in topics: @@ -570,11 +576,11 @@ async def help_test_entity_id_update_subscriptions( mqtt_mock.async_subscribe.reset_mock() entity_reg.async_update_entity( - f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk" + f"{domain}.{object_id}", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is None state = hass.states.get(f"{domain}.milk") @@ -584,7 +590,7 @@ async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_discovery_update( - hass, mqtt_mock, domain, config, sensor_config=None, entity_id="test" + hass, mqtt_mock, domain, config, sensor_config=None, object_id="tasmota_test" ): """Test MQTT discovery update after entity_id is updated.""" entity_reg = er.async_get(hass) @@ -606,16 +612,16 @@ async def help_test_entity_id_update_discovery_update( async_fire_mqtt_message(hass, topic, config_get_state_online(config)) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, topic, config_get_state_offline(config)) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE entity_reg.async_update_entity( - f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk" + f"{domain}.{object_id}", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() assert hass.states.get(f"{domain}.milk") diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 156ea365b48533..5c1364f1f77f80 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -658,7 +658,7 @@ async def test_availability_when_connection_lost( mqtt_mock, Platform.COVER, config, - entity_id="test_cover_1", + object_id="test_cover_1", ) @@ -671,7 +671,7 @@ async def test_availability( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) @@ -684,7 +684,7 @@ async def test_availability_discovery_update( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability_discovery_update( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) @@ -727,7 +727,7 @@ async def test_discovery_removal_cover( Platform.COVER, config1, config2, - entity_id="test_cover_1", + object_id="test_cover_1", name="Test cover 1", ) @@ -753,7 +753,7 @@ async def test_discovery_update_unchanged_cover( Platform.COVER, config, discovery_update, - entity_id="test_cover_1", + object_id="test_cover_1", name="Test cover 1", ) @@ -787,7 +787,7 @@ async def test_entity_id_update_subscriptions( get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, Platform.COVER, config, topics, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, topics, object_id="test_cover_1" ) @@ -800,5 +800,5 @@ async def test_entity_id_update_discovery_update( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 9a3f4f91ec754b..4fd9f293498f6b 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -143,12 +143,12 @@ async def test_correct_config_discovery( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - entity_entry = entity_reg.async_get("switch.test") + entity_entry = entity_reg.async_get("switch.tasmota_test") assert entity_entry is not None - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state is not None - assert state.name == "Test" + assert state.name == "Tasmota Test" assert (mac, "switch", "relay", 0) in hass.data[ALREADY_DISCOVERED] @@ -530,11 +530,11 @@ async def test_entity_duplicate_discovery( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") state_duplicate = hass.states.get("binary_sensor.beer1") assert state is not None - assert state.name == "Test" + assert state.name == "Tasmota Test" assert state_duplicate is None assert ( f"Entity already added, sending update: switch ('{mac}', 'switch', 'relay', 0)" diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 0b99036518e0dd..2a50e2d43b57c4 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -226,10 +226,9 @@ async def test_availability_when_connection_lost( ) -> None: """Test availability after MQTT disconnection.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota" ) @@ -238,9 +237,10 @@ async def test_availability( ) -> None: """Test availability.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 - await help_test_availability(hass, mqtt_mock, Platform.FAN, config) + await help_test_availability( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) async def test_availability_discovery_update( @@ -248,9 +248,10 @@ async def test_availability_discovery_update( ) -> None: """Test availability discovery update.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 - await help_test_availability_discovery_update(hass, mqtt_mock, Platform.FAN, config) + await help_test_availability_discovery_update( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) async def test_availability_poll_state( @@ -276,14 +277,19 @@ async def test_discovery_removal_fan( ) -> None: """Test removal of discovered fan.""" config1 = copy.deepcopy(DEFAULT_CONFIG) - config1["dn"] = "Test" config1["if"] = 1 config2 = copy.deepcopy(DEFAULT_CONFIG) - config2["dn"] = "Test" config2["if"] = 0 await help_test_discovery_removal( - hass, mqtt_mock, caplog, Platform.FAN, config1, config2 + hass, + mqtt_mock, + caplog, + Platform.FAN, + config1, + config2, + object_id="tasmota", + name="Tasmota", ) @@ -295,13 +301,19 @@ async def test_discovery_update_unchanged_fan( ) -> None: """Test update of discovered fan.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 with patch( "homeassistant.components.tasmota.fan.TasmotaFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, Platform.FAN, config, discovery_update + hass, + mqtt_mock, + caplog, + Platform.FAN, + config, + discovery_update, + object_id="tasmota", + name="Tasmota", ) @@ -310,7 +322,6 @@ async def test_discovery_device_remove( ) -> None: """Test device registry remove.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 unique_id = f"{DEFAULT_CONFIG['mac']}_fan_fan_ifan" await help_test_discovery_device_remove( @@ -323,7 +334,6 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 topics = [ get_topic_stat_result(config), @@ -331,7 +341,7 @@ async def test_entity_id_update_subscriptions( get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, Platform.FAN, config, topics + hass, mqtt_mock, Platform.FAN, config, topics, object_id="tasmota" ) @@ -340,8 +350,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, Platform.FAN, config + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" ) diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 612bda8bb08b82..5c8339a6f8933c 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -53,7 +53,7 @@ async def test_attributes_on_off( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -82,7 +82,7 @@ async def test_attributes_dimmer_tuya( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -110,7 +110,7 @@ async def test_attributes_dimmer( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -138,7 +138,7 @@ async def test_attributes_ct( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 500 @@ -167,7 +167,7 @@ async def test_attributes_ct_reduced( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 200 assert state.attributes.get("max_mireds") == 380 @@ -195,7 +195,7 @@ async def test_attributes_rgb( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -232,7 +232,7 @@ async def test_attributes_rgbw( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -269,7 +269,7 @@ async def test_attributes_rgbww( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -307,7 +307,7 @@ async def test_attributes_rgbww_reduced( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -341,37 +341,37 @@ async def test_controlling_state_via_mqtt_on_off( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes @@ -392,32 +392,32 @@ async def test_controlling_state_via_mqtt_ct( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -425,7 +425,7 @@ async def test_controlling_state_via_mqtt_ct( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_ct( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128"}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("brightness") == 128 @@ -457,32 +457,32 @@ async def test_controlling_state_via_mqtt_rgbw( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "hs" @@ -490,7 +490,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 191 assert state.attributes.get("color_mode") == "white" @@ -500,7 +500,7 @@ async def test_controlling_state_via_mqtt_rgbw( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("hs_color") == (30, 100) @@ -509,7 +509,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") is None @@ -518,7 +518,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 0 assert state.attributes.get("rgb_color") is None @@ -527,18 +527,18 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -558,32 +558,32 @@ async def test_controlling_state_via_mqtt_rgbww( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -593,7 +593,7 @@ async def test_controlling_state_via_mqtt_rgbww( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -601,7 +601,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes @@ -610,7 +610,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -618,7 +618,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp assert "color_temp" not in state.attributes @@ -628,18 +628,18 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -660,32 +660,32 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -695,7 +695,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","HSBColor":"30,100,0","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -705,7 +705,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -713,7 +713,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes @@ -722,7 +722,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -730,7 +730,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp assert not state.attributes.get("color_temp") @@ -739,18 +739,18 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -772,25 +772,25 @@ async def test_sending_mqtt_commands_on_off( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "OFF", 0, False ) @@ -816,32 +816,32 @@ async def test_sending_mqtt_commands_rgbww_tuya( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer3 75", 0, False ) @@ -866,39 +866,39 @@ async def test_sending_mqtt_commands_rgbw_legacy( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", hs_color=[0, 100]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[0, 100]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100", @@ -908,7 +908,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # Set white when setting white - await common.async_turn_on(hass, "light.test", white=128) + await common.async_turn_on(hass, "light.tasmota_test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;White 50", @@ -918,7 +918,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", @@ -928,7 +928,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", @@ -937,7 +937,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -965,39 +965,39 @@ async def test_sending_mqtt_commands_rgbw( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", hs_color=[180, 50]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[180, 50]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 180;NoDelay;HsbColor2 50", @@ -1007,7 +1007,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # Set white when setting white - await common.async_turn_on(hass, "light.test", white=128) + await common.async_turn_on(hass, "light.tasmota_test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;White 50", @@ -1017,7 +1017,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", @@ -1027,7 +1027,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", @@ -1036,7 +1036,7 @@ async def test_sending_mqtt_commands_rgbw( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -1064,38 +1064,38 @@ async def test_sending_mqtt_commands_rgbww( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", hs_color=[240, 75]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[240, 75]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 240;NoDelay;HsbColor2 75", @@ -1104,7 +1104,7 @@ async def test_sending_mqtt_commands_rgbww( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", color_temp=200) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=200) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;CT 200", @@ -1113,7 +1113,7 @@ async def test_sending_mqtt_commands_rgbww( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -1142,32 +1142,32 @@ async def test_sending_mqtt_commands_power_unlinked( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent; POWER should be sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75;NoDelay;Power1 ON", @@ -1195,14 +1195,14 @@ async def test_transition( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=255, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", @@ -1212,7 +1212,9 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be capped at 40 - await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + await common.async_turn_on( + hass, "light.tasmota_test", brightness=255, transition=100 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", @@ -1222,7 +1224,7 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->0: Speed should be 1 - await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + await common.async_turn_on(hass, "light.tasmota_test", brightness=0, transition=100) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 1;NoDelay;Power1 OFF", @@ -1232,7 +1234,7 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 4*2*2=16 - await common.async_turn_on(hass, "light.test", brightness=128, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 16;NoDelay;Dimmer 50", @@ -1245,12 +1247,12 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 # Dim the light from 50->0: Speed should be 6*2*2=24 - await common.async_turn_off(hass, "light.test", transition=6) + await common.async_turn_off(hass, "light.tasmota_test", transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 OFF", @@ -1263,12 +1265,12 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 # Dim the light from 100->0: Speed should be 0 - await common.async_turn_off(hass, "light.test", transition=0) + await common.async_turn_off(hass, "light.tasmota_test", transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 0;NoDelay;Power1 OFF", @@ -1286,13 +1288,15 @@ async def test_transition( ' "Color":"0,255,0","HSBColor":"120,100,50","White":0}' ), ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") == (0, 255, 0) # Set color of the light from 0,255,0 to 255,0,0 @ 50%: Speed should be 6*2*2=24 - await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + await common.async_turn_on( + hass, "light.tasmota_test", rgb_color=[255, 0, 0], transition=6 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", ( @@ -1310,13 +1314,15 @@ async def test_transition( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100, "Color":"0,255,0","HSBColor":"120,100,50"}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 assert state.attributes.get("rgb_color") == (0, 255, 0) # Set color of the light from 0,255,0 to 255,0,0 @ 100%: Speed should be 6*2=12 - await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + await common.async_turn_on( + hass, "light.tasmota_test", rgb_color=[255, 0, 0], transition=6 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", ( @@ -1334,13 +1340,13 @@ async def test_transition( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153, "White":50}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 153 # Set color_temp of the light from 153 to 500 @ 50%: Speed should be 6*2*2=24 - await common.async_turn_on(hass, "light.test", color_temp=500, transition=6) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=500, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;CT 500", @@ -1353,13 +1359,13 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":500}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 500 # Set color_temp of the light from 500 to 326 @ 50%: Speed should be 6*2*2*2=48->40 - await common.async_turn_on(hass, "light.test", color_temp=326, transition=6) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=326, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Power1 ON;NoDelay;CT 326", @@ -1388,14 +1394,14 @@ async def test_transition_fixed( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=255, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", @@ -1405,7 +1411,9 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be capped at 40 - await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + await common.async_turn_on( + hass, "light.tasmota_test", brightness=255, transition=100 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", @@ -1415,7 +1423,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->0: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=0, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=0, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Power1 OFF", @@ -1425,7 +1433,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=128, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 50", @@ -1435,7 +1443,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 0 - await common.async_turn_on(hass, "light.test", brightness=128, transition=0) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 0;NoDelay;Dimmer 50", @@ -1463,7 +1471,7 @@ async def test_relay_as_light( state = hass.states.get("switch.test") assert state is None - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state is not None @@ -1631,14 +1639,14 @@ async def test_discovery_update_reconfigure_light( # Simple dimmer async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("supported_features") == LightEntityFeature.TRANSITION assert state.attributes.get("supported_color_modes") == ["brightness"] # Reconfigure as RGB light async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data2) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert ( state.attributes.get("supported_features") == LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 6a896615c734c8..22ee652aef3a31 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -38,7 +38,7 @@ from tests.common import async_fire_mqtt_message, async_fire_time_changed from tests.typing import MqttMockHAClient, MqttMockPahoClient -BAD_INDEXED_SENSOR_CONFIG_3 = { +BAD_LIST_SENSOR_CONFIG_3 = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -47,7 +47,9 @@ } } -INDEXED_SENSOR_CONFIG = { +# This configuration has some sensors where values are lists +# Home Assistant maps this to one sensor for each list item +LIST_SENSOR_CONFIG = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -74,7 +76,8 @@ } } -INDEXED_SENSOR_CONFIG_2 = { +# Same as LIST_SENSOR_CONFIG, but Total is also a list +LIST_SENSOR_CONFIG_2 = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -101,8 +104,9 @@ } } - -NESTED_SENSOR_CONFIG_1 = { +# This configuration has some sensors where values are dicts +# Home Assistant maps this to one sensor for each dictionary item +DICT_SENSOR_CONFIG_1 = { "sn": { "Time": "2020-03-03T00:00:00+00:00", "TX23": { @@ -119,7 +123,22 @@ } } -NESTED_SENSOR_CONFIG_2 = { +# Similar to LIST_SENSOR_CONFIG, but Total is a dict +DICT_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2023-01-27T11:04:56", + "ENERGY": { + "Total": { + "Phase1": 0.017, + "Phase2": 0.017, + }, + "TotalStartTime": "2018-11-23T15:33:47", + }, + } +} + + +TEMPERATURE_SENSOR_CONFIG = { "sn": { "Time": "2023-01-27T11:04:56", "DS18B20": { @@ -131,65 +150,33 @@ } -async def test_controlling_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - entity_reg = er.async_get(hass) - entry = entity_reg.async_get("sensor.tasmota_dht11_temperature") - assert entry.disabled is False - assert entry.disabled_by is None - assert entry.entity_category is None - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"DHT11":{"Temperature":20.5}}' - ) - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "20.5" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', - ) - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "20.0" - - @pytest.mark.parametrize( ("sensor_config", "entity_ids", "messages", "states"), [ ( - NESTED_SENSOR_CONFIG_1, + DEFAULT_SENSOR_CONFIG, + ["sensor.tasmota_dht11_temperature"], + ( + '{"DHT11":{"Temperature":20.5}}', + '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', + ), + ( + { + "sensor.tasmota_dht11_temperature": { + "state": "20.5", + "attributes": { + "device_class": "temperature", + "unit_of_measurement": "°C", + }, + }, + }, + { + "sensor.tasmota_dht11_temperature": {"state": "20.0"}, + }, + ), + ), + ( + DICT_SENSOR_CONFIG_1, ["sensor.tasmota_tx23_speed_act", "sensor.tasmota_tx23_dir_card"], ( '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', @@ -197,17 +184,50 @@ async def test_controlling_state_via_mqtt( ), ( { - "sensor.tasmota_tx23_speed_act": "12.3", - "sensor.tasmota_tx23_dir_card": "WSW", + "sensor.tasmota_tx23_speed_act": { + "state": "12.3", + "attributes": { + "device_class": None, + "unit_of_measurement": "km/h", + }, + }, + "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, }, { - "sensor.tasmota_tx23_speed_act": "23.4", - "sensor.tasmota_tx23_dir_card": "ESE", + "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, + "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, }, ), ), ( - NESTED_SENSOR_CONFIG_2, + LIST_SENSOR_CONFIG, + [ + "sensor.tasmota_energy_totaltariff_0", + "sensor.tasmota_energy_totaltariff_1", + ], + ( + '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + ), + ( + { + "sensor.tasmota_energy_totaltariff_0": { + "state": "1.2", + "attributes": { + "device_class": None, + "unit_of_measurement": None, + }, + }, + "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, + }, + { + "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, + "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, + }, + ), + ), + ( + TEMPERATURE_SENSOR_CONFIG, ["sensor.tasmota_ds18b20_temperature", "sensor.tasmota_ds18b20_id"], ( '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', @@ -215,18 +235,117 @@ async def test_controlling_state_via_mqtt( ), ( { - "sensor.tasmota_ds18b20_temperature": "12.3", - "sensor.tasmota_ds18b20_id": "01191ED79190", + "sensor.tasmota_ds18b20_temperature": { + "state": "12.3", + "attributes": { + "device_class": "temperature", + "unit_of_measurement": "°C", + }, + }, + "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, + }, + { + "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, + "sensor.tasmota_ds18b20_id": {"state": "meep"}, + }, + ), + ), + # Test simple Total sensor + ( + LIST_SENSOR_CONFIG, + ["sensor.tasmota_energy_total"], + ( + '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, }, { - "sensor.tasmota_ds18b20_temperature": "23.4", - "sensor.tasmota_ds18b20_id": "meep", + "sensor.tasmota_energy_total": {"state": "5.6"}, + }, + ), + ), + # Test list Total sensors + ( + LIST_SENSOR_CONFIG_2, + ["sensor.tasmota_energy_total_0", "sensor.tasmota_energy_total_1"], + ( + '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total_0": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + "sensor.tasmota_energy_total_1": { + "state": "3.4", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + }, + { + "sensor.tasmota_energy_total_0": {"state": "5.6"}, + "sensor.tasmota_energy_total_1": {"state": "7.8"}, + }, + ), + ), + # Test dict Total sensors + ( + DICT_SENSOR_CONFIG_2, + [ + "sensor.tasmota_energy_total_phase1", + "sensor.tasmota_energy_total_phase2", + ], + ( + '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total_phase1": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + "sensor.tasmota_energy_total_phase2": { + "state": "3.4", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + }, + { + "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, + "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, }, ), ), ], ) -async def test_nested_sensor_state_via_mqtt( +async def test_controlling_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota, @@ -236,6 +355,7 @@ async def test_nested_sensor_state_via_mqtt( states, ) -> None: """Test state update via MQTT.""" + entity_reg = er.async_get(hass) config = copy.deepcopy(DEFAULT_CONFIG) sensor_config = copy.deepcopy(sensor_config) mac = config["mac"] @@ -258,6 +378,11 @@ async def test_nested_sensor_state_via_mqtt( assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + entry = entity_reg.async_get(entity_id) + assert entry.disabled is False + assert entry.disabled_by is None + assert entry.entity_category is None + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() for entity_id in entity_ids: @@ -269,163 +394,19 @@ async def test_nested_sensor_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - assert state.state == states[0][entity_id] + expected_state = states[0][entity_id] + assert state.state == expected_state["state"] + for attribute, expected in expected_state.get("attributes", {}).items(): + assert state.attributes.get(attribute) == expected # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - assert state.state == states[1][entity_id] - - -async def test_indexed_sensor_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"TotalTariff":[1.2,3.4]}}' - ) - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "3.4" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', - ) - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "7.8" - - -async def test_indexed_sensor_state_via_mqtt2( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT for sensor with last_reset property.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/tele/SENSOR", - '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', - ) - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "1.2" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', - ) - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "5.6" - - -async def test_indexed_sensor_state_via_mqtt3( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT for indexed sensor with last_reset property.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG_2) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/tele/SENSOR", - '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', - ) - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "3.4" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"Total":[5.6,7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', - ) - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "7.8" + expected_state = states[1][entity_id] + assert state.state == expected_state["state"] + for attribute, expected in expected_state.get("attributes", {}).items(): + assert state.attributes.get(attribute) == expected async def test_bad_indexed_sensor_state_via_mqtt( @@ -433,7 +414,7 @@ async def test_bad_indexed_sensor_state_via_mqtt( ) -> None: """Test state update via MQTT where sensor is not matching configuration.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(BAD_INDEXED_SENSOR_CONFIG_3) + sensor_config = copy.deepcopy(BAD_LIST_SENSOR_CONFIG_3) mac = config["mac"] async_fire_mqtt_message( @@ -784,7 +765,7 @@ async def test_nested_sensor_attributes( ) -> None: """Test correct attributes for sensors.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1) + sensor_config = copy.deepcopy(DICT_SENSOR_CONFIG_1) mac = config["mac"] async_fire_mqtt_message( diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index b79560214a8b8a..b8d0ed2d060cf0 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -47,34 +47,34 @@ async def test_controlling_state_via_mqtt( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF @@ -95,30 +95,30 @@ async def test_sending_mqtt_commands( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the switch on and verify MQTT message is sent - await common.async_turn_on(hass, "switch.test") + await common.async_turn_on(hass, "switch.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF # Turn the switch off and verify MQTT message is sent - await common.async_turn_off(hass, "switch.test") + await common.async_turn_off(hass, "switch.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "OFF", 0, False ) - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF @@ -138,9 +138,9 @@ async def test_relay_as_light( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state is None - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state is not None diff --git a/tests/components/tomorrowio/fixtures/v4.json b/tests/components/tomorrowio/fixtures/v4.json index 0ca4f348956104..c511263fb5f5b5 100644 --- a/tests/components/tomorrowio/fixtures/v4.json +++ b/tests/components/tomorrowio/fixtures/v4.json @@ -908,6 +908,8 @@ "values": { "temperatureMin": 44.13, "temperatureMax": 44.13, + "dewPoint": 12.76, + "humidity": 58.46, "windSpeed": 9.33, "windDirection": 315.14, "weatherCode": 1000, @@ -2206,6 +2208,8 @@ "values": { "temperatureMin": 26.11, "temperatureMax": 45.93, + "dewPoint": 12.76, + "humidity": 58.46, "windSpeed": 9.49, "windDirection": 239.6, "weatherCode": 1000, diff --git a/tests/components/tomorrowio/snapshots/test_weather.ambr b/tests/components/tomorrowio/snapshots/test_weather.ambr index 40ff18658c6da4..a938cb10e44ef8 100644 --- a/tests/components/tomorrowio/snapshots/test_weather.ambr +++ b/tests/components/tomorrowio/snapshots/test_weather.ambr @@ -4,6 +4,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -148,6 +150,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -292,6 +296,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, @@ -512,6 +518,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, @@ -733,6 +741,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -879,6 +889,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 8490b94a7f9a29..a6a5e93561405f 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -24,6 +24,8 @@ ATTR_CONDITION_SUNNY, ATTR_FORECAST, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_DEW_POINT, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -164,6 +166,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 45.9, ATTR_FORECAST_TEMP_LOW: 26.1, + ATTR_FORECAST_DEW_POINT: 12.8, + ATTR_FORECAST_HUMIDITY: 58, ATTR_FORECAST_WIND_BEARING: 239.6, ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h } @@ -191,6 +195,8 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: assert weather_state.attributes[ATTR_FORECAST][0] == { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_DEW_POINT: 12.8, + ATTR_FORECAST_HUMIDITY: 58, ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 45.9, diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 2d28240a90de0d..a2be388af4c4aa 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -80,7 +80,6 @@ CONFIGURATION = [] SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] -DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}] def mock_default_unifi_requests( @@ -88,12 +87,13 @@ def mock_default_unifi_requests( host, site_id, sites=None, - description=None, clients_response=None, clients_all_response=None, devices_response=None, dpiapp_response=None, dpigroup_response=None, + port_forward_response=None, + system_information_response=None, wlans_response=None, ): """Mock default UniFi requests responses.""" @@ -111,12 +111,6 @@ def mock_default_unifi_requests( headers={"content-type": CONTENT_TYPE_JSON}, ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/self", - json={"data": description or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( f"https://{host}:1234/api/s/{site_id}/stat/sta", json={"data": clients_response or [], "meta": {"rc": "ok"}}, @@ -142,6 +136,16 @@ def mock_default_unifi_requests( json={"data": dpigroup_response or [], "meta": {"rc": "ok"}}, headers={"content-type": CONTENT_TYPE_JSON}, ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/portforward", + json={"data": port_forward_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/stat/sysinfo", + json={"data": system_information_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) aioclient_mock.get( f"https://{host}:1234/api/s/{site_id}/rest/wlanconf", json={"data": wlans_response or [], "meta": {"rc": "ok"}}, @@ -156,12 +160,13 @@ async def setup_unifi_integration( config=ENTRY_CONFIG, options=ENTRY_OPTIONS, sites=SITE, - site_description=DESCRIPTION, clients_response=None, clients_all_response=None, devices_response=None, dpiapp_response=None, dpigroup_response=None, + port_forward_response=None, + system_information_response=None, wlans_response=None, known_wireless_clients=None, controllers=None, @@ -192,12 +197,13 @@ async def setup_unifi_integration( host=config_entry.data[CONF_HOST], site_id=config_entry.data[CONF_SITE_ID], sites=sites, - description=site_description, clients_response=clients_response, clients_all_response=clients_all_response, devices_response=devices_response, dpiapp_response=dpiapp_response, dpigroup_response=dpigroup_response, + port_forward_response=port_forward_response, + system_information_response=system_information_response, wlans_response=wlans_response, ) @@ -230,9 +236,7 @@ async def test_controller_setup( assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN) assert controller.host == ENTRY_CONFIG[CONF_HOST] - assert controller.site == ENTRY_CONFIG[CONF_SITE_ID] - assert controller.site_name == SITE[0]["desc"] - assert controller.site_role == SITE[0]["role"] + assert controller.is_admin == (SITE[0]["role"] == "admin") assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 5248836c08a229..638e79ae64961d 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -141,7 +141,7 @@ async def test_entry_diagnostics( "unique_id": "1", "version": 1, }, - "site_role": "admin", + "role_is_admin": True, "clients": { "00:00:00:00:00:00": { "blocked": False, diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index cce26ac84cc75f..a1b817d67e29d5 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -24,7 +24,7 @@ async def test_successful_config_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that configured options for a host are loaded via config entry.""" - await setup_unifi_integration(hass, aioclient_mock, unique_id=None) + await setup_unifi_integration(hass, aioclient_mock) assert hass.data[UNIFI_DOMAIN] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5344ac901b7f39..c091fc5cc5907c 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -36,8 +36,8 @@ from .test_controller import ( CONTROLLER_HOST, - DESCRIPTION, ENTRY_CONFIG, + SITE, setup_unifi_integration, ) @@ -778,7 +778,7 @@ async def test_no_clients( }, ) - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 11 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -803,13 +803,13 @@ async def test_not_admin( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that switch platform only work on an admin account.""" - description = deepcopy(DESCRIPTION) - description[0]["site_role"] = "not admin" + site = deepcopy(SITE) + site[0]["role"] = "not admin" await setup_unifi_integration( hass, aioclient_mock, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - site_description=description, + sites=site, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) @@ -867,8 +867,8 @@ async def test_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { "mac": "00:00:00:00:01:01", "cmd": "block-sta", } @@ -876,8 +876,8 @@ async def test_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { + assert aioclient_mock.call_count == 13 + assert aioclient_mock.mock_calls[12][2] == { "mac": "00:00:00:00:01:01", "cmd": "unblock-sta", } @@ -894,8 +894,8 @@ async def test_switches( {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert aioclient_mock.call_count == 13 - assert aioclient_mock.mock_calls[12][2] == {"enabled": False} + assert aioclient_mock.call_count == 14 + assert aioclient_mock.mock_calls[13][2] == {"enabled": False} await hass.services.async_call( SWITCH_DOMAIN, @@ -903,8 +903,8 @@ async def test_switches( {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert aioclient_mock.call_count == 14 - assert aioclient_mock.mock_calls[13][2] == {"enabled": True} + assert aioclient_mock.call_count == 15 + assert aioclient_mock.mock_calls[14][2] == {"enabled": True} async def test_remove_switches( @@ -990,8 +990,8 @@ async def test_block_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { "mac": "00:00:00:00:01:01", "cmd": "block-sta", } @@ -999,8 +999,8 @@ async def test_block_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { + assert aioclient_mock.call_count == 13 + assert aioclient_mock.mock_calls[12][2] == { "mac": "00:00:00:00:01:01", "cmd": "unblock-sta", } diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 7cf8495b9db5df..e59eca371d6989 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -26,7 +26,7 @@ ) from homeassistant.core import HomeAssistant -from .test_controller import DESCRIPTION, setup_unifi_integration +from .test_controller import SITE, setup_unifi_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,14 +136,11 @@ async def test_not_admin( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that the INSTALL feature is not available on a non-admin account.""" - description = deepcopy(DESCRIPTION) - description[0]["site_role"] = "not admin" + site = deepcopy(SITE) + site[0]["role"] = "not admin" await setup_unifi_integration( - hass, - aioclient_mock, - site_description=description, - devices_response=[DEVICE_1], + hass, aioclient_mock, sites=site, devices_response=[DEVICE_1] ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index e19985aea3fb61..c5690ef5e92b16 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -441,7 +440,7 @@ async def test_browse_media_event_type( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 1, ) @@ -454,7 +453,7 @@ async def test_browse_media_event_type( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 2, ) @@ -513,7 +512,7 @@ async def test_browse_media_time( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 1, ) @@ -526,7 +525,7 @@ async def test_browse_media_time( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 2, ) diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index a7780f54f70b0e..73f98c9e2db06c 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -1,4 +1,5 @@ """The tests for the Update component.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest @@ -24,6 +25,7 @@ ATTR_TITLE, UpdateEntityFeature, ) +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, @@ -34,12 +36,24 @@ ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component -from tests.common import MockEntityPlatform, mock_restore_cache +from tests.common import ( + MockConfigEntry, + MockEntityPlatform, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, + mock_restore_cache, +) from tests.typing import WebSocketGenerator +TEST_DOMAIN = "test" + class MockUpdateEntity(UpdateEntity): """Mock UpdateEntity to use in tests.""" @@ -752,3 +766,101 @@ async def test_release_notes_entity_does_not_support_release_notes( result = await client.receive_json() assert result["error"]["code"] == "not_supported" assert result["error"]["message"] == "Entity does not support release notes" + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +async def test_name(hass: HomeAssistant) -> None: + """Test update name.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed update entity without device class -> no name + entity1 = UpdateEntity() + entity1.entity_id = "update.test1" + + # Unnamed update entity with device class but has_entity_name False -> no name + entity2 = UpdateEntity() + entity2.entity_id = "update.test2" + entity2._attr_device_class = UpdateDeviceClass.FIRMWARE + + # Unnamed update entity with device class and has_entity_name True -> named + entity3 = UpdateEntity() + entity3.entity_id = "update.test3" + entity3._attr_device_class = UpdateDeviceClass.FIRMWARE + entity3._attr_has_entity_name = True + + # Unnamed update entity with device class and has_entity_name True -> named + entity4 = UpdateEntity() + entity4.entity_id = "update.test4" + entity4.entity_description = UpdateEntityDescription( + "test", + UpdateDeviceClass.FIRMWARE, + has_entity_name=True, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test update platform via config entry.""" + async_add_entities([entity1, entity2, entity3, entity4]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + 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(entity1.entity_id) + assert state + assert "device_class" not in state.attributes + assert "friendly_name" not in state.attributes + + state = hass.states.get(entity2.entity_id) + assert state + assert state.attributes.get("device_class") == "firmware" + assert "friendly_name" not in state.attributes + + expected = { + "device_class": "firmware", + "friendly_name": "Firmware", + } + state = hass.states.get(entity3.entity_id) + assert state + assert expected.items() <= state.attributes.items() + + state = hass.states.get(entity4.entity_id) + assert state + assert expected.items() <= state.attributes.items() diff --git a/tests/components/workday/__init__.py b/tests/components/workday/__init__.py index 005a63397d98fd..f87328998e1acf 100644 --- a/tests/components/workday/__init__.py +++ b/tests/components/workday/__init__.py @@ -50,6 +50,15 @@ async def init_integration( "add_holidays": [], "remove_holidays": [], } +TEST_CONFIG_INCORRECT_COUNTRY = { + "name": DEFAULT_NAME, + "country": "ZZ", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": [], + "remove_holidays": [], +} TEST_CONFIG_INCORRECT_PROVINCE = { "name": DEFAULT_NAME, "country": "DE", diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 71dd23c19a31ee..a8cea01a864824 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -17,6 +17,7 @@ TEST_CONFIG_EXAMPLE_2, TEST_CONFIG_INCLUDE_HOLIDAY, TEST_CONFIG_INCORRECT_ADD_REMOVE, + TEST_CONFIG_INCORRECT_COUNTRY, TEST_CONFIG_INCORRECT_PROVINCE, TEST_CONFIG_NO_PROVINCE, TEST_CONFIG_NO_STATE, @@ -187,6 +188,21 @@ async def test_setup_day_after_tomorrow( assert state.state == "off" +async def test_setup_faulty_country( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup with faulty province.""" + freezer.move_to(datetime(2017, 1, 6, 12, tzinfo=UTC)) # Friday + await init_integration(hass, TEST_CONFIG_INCORRECT_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is None + + assert "There is no country" in caplog.text + + async def test_setup_faulty_province( hass: HomeAssistant, freezer: FrozenDateTimeFactory, diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py new file mode 100644 index 00000000000000..144a24a489789a --- /dev/null +++ b/tests/components/yale_smart_alarm/conftest.py @@ -0,0 +1,64 @@ +"""Fixtures for the Yale Smart Living integration.""" +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import patch + +import pytest +from yalesmartalarmclient.const import YALE_STATE_ARM_FULL + +from homeassistant.components.yale_smart_alarm.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + +ENTRY_CONFIG = { + "username": "test-username", + "password": "new-test-password", + "name": "Yale Smart Alarm", + "area_id": "1", +} +OPTIONS_CONFIG = {"lock_code_digits": 6} + + +@pytest.fixture +async def load_config_entry( + hass: HomeAssistant, load_json: dict[str, Any] +) -> MockConfigEntry: + """Set up the Yale Smart Living integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + unique_id="username", + version=1, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yale_smart_alarm.coordinator.YaleSmartAlarmClient", + autospec=True, + ) as mock_client_class: + client = mock_client_class.return_value + client.auth = None + client.lock_api = None + client.get_all.return_value = load_json + client.get_armed_status.return_value = YALE_STATE_ARM_FULL + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(name="load_json", scope="session") +def load_json_from_fixture() -> dict[str, Any]: + """Load fixture with json data and return.""" + + data_fixture = load_fixture("get_all.json", "yale_smart_alarm") + json_data: dict[str, Any] = json.loads(data_fixture) + return json_data diff --git a/tests/components/yale_smart_alarm/fixtures/get_all.json b/tests/components/yale_smart_alarm/fixtures/get_all.json new file mode 100644 index 00000000000000..08f60fafd3f2d3 --- /dev/null +++ b/tests/components/yale_smart_alarm/fixtures/get_all.json @@ -0,0 +1,331 @@ +{ + "DEVICES": [ + { + "area": "1", + "no": "1", + "rf": null, + "address": "123", + "type": "device_type.door_lock", + "name": "Device1", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:01", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "35", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "123", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + } + ], + "MODE": [ + { + "area": "1", + "mode": "disarm" + } + ], + "STATUS": { + "acfail": "main.normal", + "battery": "main.normal", + "tamper": "main.normal", + "jam": "main.normal", + "rssi": "1", + "gsm_rssi": "0", + "imei": "", + "imsi": "" + }, + "CYCLE": { + "model": [ + { + "area": "1", + "mode": "disarm" + } + ], + "panel_status": { + "warning_snd_mute": "0" + }, + "device_status": [ + { + "area": "1", + "no": "1", + "rf": null, + "address": "124", + "type": "device_type.door_lock", + "name": "Device2", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:02", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "35", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "124", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + } + ], + "capture_latest": null, + "report_event_latest": { + "utc_event_time": null, + "time": "1692271914", + "report_id": "1027299996", + "id": "9999", + "event_time": null, + "cid_code": "1807" + }, + "alarm_event_latest": null + }, + "ONLINE": "online", + "HISTORY": [ + { + "report_id": "1027299996", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:54", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027299889", + "cid": "18180201101", + "event_type": "1802", + "user": 101, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:43", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027299587", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:11", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027296099", + "cid": "18180101001", + "event_type": "1801", + "user": 1, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:24:52", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027273782", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 10:43:21", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027273230", + "cid": "18180201101", + "event_type": "1802", + "user": 101, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 10:42:09", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027100172", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 05:28:57", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027099978", + "cid": "18180101001", + "event_type": "1801", + "user": 1, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 05:28:39", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027093266", + "cid": "18160200000", + "event_type": "1602", + "user": "", + "area": 0, + "zone": 0, + "name": "", + "type": "", + "event_time": null, + "time": "2023/08/17 05:17:12", + "status_temp_format": "C", + "cid_source": "SYSTEM" + }, + { + "report_id": "1026912623", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/16 20:29:36", + "status_temp_format": "C", + "cid_source": "DEVICE" + } + ], + "PANEL INFO": { + "mac": "00:00:00:00:10", + "report_account": "username", + "xml_version": "2", + "version": "MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga", + "net_version": "MINIGW-MZ-1_G 1.0.1.29A", + "rf51_version": "", + "zb_version": "4.1.2.6.2", + "zw_version": "", + "SMS_Balance": "50", + "voice_balance": "0", + "name": "", + "contact": "", + "mail_address": "username@fake.com", + "phone": "UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036", + "service_time": "UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00", + "dealer_name": "Poland" + }, + "AUTH CHECK": { + "user_id": "username", + "id": "username", + "mail_address": "username@fake.com", + "mac": "00:00:00:00:20", + "is_auth": "1", + "master": "1", + "first_login": "1", + "name": "Device1", + "token_time": "2023-08-17 16:19:20", + "agent": false, + "xml_version": "2", + "dealer_id": "605", + "dealer_group": "yale" + } +} diff --git a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..faff1c5103ac74 --- /dev/null +++ b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr @@ -0,0 +1,344 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'AUTH CHECK': dict({ + 'agent': False, + 'dealer_group': 'yale', + 'dealer_id': '605', + 'first_login': '1', + 'id': '**REDACTED**', + 'is_auth': '1', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'master': '1', + 'name': '**REDACTED**', + 'token_time': '2023-08-17 16:19:20', + 'user_id': '**REDACTED**', + 'xml_version': '2', + }), + 'CYCLE': dict({ + 'alarm_event_latest': None, + 'capture_latest': None, + 'device_status': list([ + dict({ + '_state': 'locked', + '_state2': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '35', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '1', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + ]), + 'model': list([ + dict({ + 'area': '1', + 'mode': 'disarm', + }), + ]), + 'panel_status': dict({ + 'warning_snd_mute': '0', + }), + 'report_event_latest': dict({ + 'cid_code': '1807', + 'event_time': None, + 'id': '**REDACTED**', + 'report_id': '1027299996', + 'time': '1692271914', + 'utc_event_time': None, + }), + }), + 'DEVICES': list([ + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '35', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '1', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + ]), + 'HISTORY': list([ + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299996', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:54', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027299889', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:43', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299587', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:11', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027296099', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:24:52', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027273782', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:43:21', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027273230', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:42:09', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027100172', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:57', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027099978', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:39', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 0, + 'cid': '18160200000', + 'cid_source': 'SYSTEM', + 'event_time': None, + 'event_type': '1602', + 'name': '', + 'report_id': '1027093266', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:17:12', + 'type': '', + 'user': '', + 'zone': 0, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1026912623', + 'status_temp_format': 'C', + 'time': '2023/08/16 20:29:36', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + ]), + 'MODE': list([ + dict({ + 'area': '1', + 'mode': 'disarm', + }), + ]), + 'ONLINE': 'online', + 'PANEL INFO': dict({ + 'SMS_Balance': '50', + 'contact': '', + 'dealer_name': 'Poland', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'name': '', + 'net_version': 'MINIGW-MZ-1_G 1.0.1.29A', + 'phone': 'UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036', + 'report_account': '**REDACTED**', + 'rf51_version': '', + 'service_time': 'UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00', + 'version': 'MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga', + 'voice_balance': '0', + 'xml_version': '2', + 'zb_version': '4.1.2.6.2', + 'zw_version': '', + }), + 'STATUS': dict({ + 'acfail': 'main.normal', + 'battery': 'main.normal', + 'gsm_rssi': '0', + 'imei': '', + 'imsi': '', + 'jam': 'main.normal', + 'rssi': '1', + 'tamper': 'main.normal', + }), + }) +# --- diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 4553a1200602b6..90c0b78baf5c4a 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -121,6 +121,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "name": "Yale Smart Alarm", "area_id": "1", }, + version=2, ) entry.add_to_hass(hass) @@ -187,6 +188,7 @@ async def test_reauth_flow_error( "name": "Yale Smart Alarm", "area_id": "1", }, + version=2, ) entry.add_to_hass(hass) @@ -248,41 +250,20 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=DOMAIN, unique_id="test-username", - data={}, + data={ + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + version=2, ) entry.add_to_hass(hass) with patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", return_value=True, - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"code": "123456", "lock_code_digits": 6}, - ) - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == {"code": "123456", "lock_code_digits": 6} - - -async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: - """Test options config flow with a code format mismatch error.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="test-username", - data={}, - ) - entry.add_to_hass(hass) - - with patch( + ), patch( "homeassistant.components.yale_smart_alarm.async_setup_entry", return_value=True, ): @@ -293,21 +274,11 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - assert result["errors"] == {} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"code": "123", "lock_code_digits": 6}, - ) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": "code_format_mismatch"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"code": "123456", "lock_code_digits": 6}, + user_input={"lock_code_digits": 6}, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == {"code": "123456", "lock_code_digits": 6} + assert result["data"] == {"lock_code_digits": 6} diff --git a/tests/components/yale_smart_alarm/test_diagnostics.py b/tests/components/yale_smart_alarm/test_diagnostics.py new file mode 100644 index 00000000000000..8796eeb465b295 --- /dev/null +++ b/tests/components/yale_smart_alarm/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Test Yale Smart Living diagnostics.""" +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + load_config_entry: ConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a config entry.""" + entry = load_config_entry + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == snapshot diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 6161f66fdd1ccb..47dbd54baa9df8 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -181,7 +181,7 @@ async def _async_test_service( await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() - elif type(payload) == list: + elif isinstance(payload, list): mocked_method.assert_called_once_with(*payload) else: mocked_method.assert_called_once_with(**payload) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index fdbe9eb316b4e6..2512f426f13ddc 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -303,7 +303,7 @@ async def test_and_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "And Condition Shorthand" - assert "and" not in config.keys() + assert "and" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -345,7 +345,7 @@ async def test_and_condition_list_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "And Condition List Shorthand" - assert "and" not in config.keys() + assert "and" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -577,7 +577,7 @@ async def test_or_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "Or Condition Shorthand" - assert "or" not in config.keys() + assert "or" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -809,7 +809,7 @@ async def test_not_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "Not Condition Shorthand" - assert "not" not in config.keys() + assert "not" not in config hass.states.async_set("sensor.temperature", 101) assert test(hass) diff --git a/tests/helpers/test_issue_registry.py b/tests/helpers/test_issue_registry.py index d184ccf0a2b64f..88f97a6542193d 100644 --- a/tests/helpers/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -9,7 +9,7 @@ from tests.common import async_capture_events, flush_store -async def test_load_issues(hass: HomeAssistant) -> None: +async def test_load_save_issues(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" issues = [ { @@ -209,6 +209,77 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert issue4_registry2 == issue4 +@pytest.mark.parametrize("load_registries", [False]) +async def test_load_save_issues_read_only( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Make sure that we don't save data when opened in read-only mode.""" + hass_storage[ir.STORAGE_KEY] = { + "version": ir.STORAGE_VERSION_MAJOR, + "minor_version": ir.STORAGE_VERSION_MINOR, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "is_persistent": False, + "issue_id": "issue_1", + }, + ] + }, + } + + issues = [ + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "is_fixable": True, + "is_persistent": False, + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED) + await ir.async_load(hass, read_only=True) + + for issue in issues: + ir.async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_2", + } + + registry = ir.async_get(hass) + assert len(registry.issues) == 2 + + registry2 = ir.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert len(registry2.issues) == 1 + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_issues_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 81953c7d785b65..85aa4d2de0e8e1 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -60,6 +60,12 @@ def store_v_2_1(hass): ) +@pytest.fixture +def read_only_store(hass): + """Fixture of a read only store.""" + return storage.Store(hass, MOCK_VERSION, MOCK_KEY, read_only=True) + + async def test_loading(hass: HomeAssistant, store) -> None: """Test we can save and load data.""" await store.async_save(MOCK_DATA) @@ -703,3 +709,27 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: await store.async_load() await hass.async_stop(force=True) + + +async def test_read_only_store( + hass: HomeAssistant, read_only_store, hass_storage: dict[str, Any] +) -> None: + """Test store opened in read only mode does not save.""" + read_only_store.async_delay_save(lambda: MOCK_DATA, 1) + assert read_only_store.key not in hass_storage + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index 5d2292e924921a..36b4e7c692fe49 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -61,7 +61,7 @@ def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: if version is not None: self._values["installed_version"] = version - _LOGGER.info(f"Installed update with version: {version}") + _LOGGER.info("Installed update with version: %s", version) else: self._values["installed_version"] = self.latest_version _LOGGER.info("Installed latest update")