diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 3e87cc55902662..43514727e971de 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "platinum", - "requirements": ["aioamazondevices==6.4.3"] + "requirements": ["aioamazondevices==6.4.4"] } diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 313373e3181b88..1ab688f457952e 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401 +from homeassistant.const import STATE_HOME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .config_entry import ( # noqa: F401 +from .config_entry import ( ScannerEntity, ScannerEntityDescription, TrackerEntity, @@ -15,7 +15,7 @@ async_setup_entry, async_unload_entry, ) -from .const import ( # noqa: F401 +from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, ATTR_DEV_ID, @@ -37,7 +37,7 @@ SCAN_INTERVAL, SourceType, ) -from .legacy import ( # noqa: F401 +from .legacy import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, SERVICE_SEE, @@ -61,3 +61,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the device tracker.""" async_setup_legacy_integration(hass, config) return True + + +__all__ = ( + "ATTR_ATTRIBUTES", + "ATTR_BATTERY", + "ATTR_DEV_ID", + "ATTR_GPS", + "ATTR_HOST_NAME", + "ATTR_IP", + "ATTR_LOCATION_NAME", + "ATTR_MAC", + "ATTR_SOURCE_TYPE", + "CONF_CONSIDER_HOME", + "CONF_NEW_DEVICE_DEFAULTS", + "CONF_SCAN_INTERVAL", + "CONF_TRACK_NEW", + "CONNECTED_DEVICE_REGISTERED", + "DEFAULT_CONSIDER_HOME", + "DEFAULT_TRACK_NEW", + "DOMAIN", + "ENTITY_ID_FORMAT", + "PLATFORM_SCHEMA", + "PLATFORM_SCHEMA_BASE", + "SCAN_INTERVAL", + "SERVICE_SEE", + "SERVICE_SEE_PAYLOAD_SCHEMA", + "SOURCE_TYPES", + "AsyncSeeCallback", + "DeviceScanner", + "ScannerEntity", + "ScannerEntityDescription", + "SeeCallback", + "SourceType", + "TrackerEntity", + "TrackerEntityDescription", + "async_setup", + "async_setup_entry", + "async_unload_entry", + "is_on", + "see", +) diff --git a/homeassistant/components/improv_ble/__init__.py b/homeassistant/components/improv_ble/__init__.py index 15f0ef4082f5af..d0526ad7150181 100644 --- a/homeassistant/components/improv_ble/__init__.py +++ b/homeassistant/components/improv_ble/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import logging from homeassistant.core import HomeAssistant, callback @@ -15,7 +16,9 @@ @callback -def async_get_provisioning_futures(hass: HomeAssistant) -> dict: +def async_get_provisioning_futures( + hass: HomeAssistant, +) -> dict[str, asyncio.Future[str]]: """Get the provisioning futures registry, creating it if needed. This is a helper function for internal use and testing. diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index 17168ee332fc6b..0df566066c1e37 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -296,7 +296,7 @@ async def async_step_identify( @asynccontextmanager async def _async_provision_context( self, ble_mac: str - ) -> AsyncIterator[asyncio.Future[str | None]]: + ) -> AsyncIterator[asyncio.Future[str]]: """Context manager to register and cleanup provisioning future.""" future = self.hass.loop.create_future() provisioning_futures = async_get_provisioning_futures(self.hass) diff --git a/homeassistant/components/improv_ble/const.py b/homeassistant/components/improv_ble/const.py index a55826d73a2c5e..c06e7c667a5d96 100644 --- a/homeassistant/components/improv_ble/const.py +++ b/homeassistant/components/improv_ble/const.py @@ -8,7 +8,7 @@ DOMAIN = "improv_ble" -PROVISIONING_FUTURES: HassKey[dict[str, asyncio.Future[str | None]]] = HassKey(DOMAIN) +PROVISIONING_FUTURES: HassKey[dict[str, asyncio.Future[str]]] = HassKey(DOMAIN) # Timeout in seconds to wait for another integration to register a next flow # after successful provisioning (e.g., ESPHome discovering the device) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 707a0215f2f606..2719a4a14c0700 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -5,7 +5,6 @@ from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, - ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, TrackerEntity, ) @@ -13,6 +12,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_ID, + ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 125e4d27247ce3..c852409b3b7761 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -28,7 +28,6 @@ from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, - ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON @@ -38,6 +37,7 @@ from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, + ATTR_GPS_ACCURACY, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 47d2908f5cfbb0..eb5c6d65c0e65e 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -479,6 +479,8 @@ INTEGRATION_URL = f"{USER_DOCUMENTATION_URL}integrations/{DOMAIN}/" TEMPLATING_URL = f"{USER_DOCUMENTATION_URL}docs/configuration/templating/" +COMMAND_TEMPLATING_URL = f"{TEMPLATING_URL}#using-command-templates-with-mqtt" +VALUE_TEMPLATING_URL = f"{TEMPLATING_URL}#using-value-templates-with-mqtt" AVAILABLE_STATE_CLASSES_URL = ( f"{DEVELOPER_DOCUMENTATION_URL}docs/core/entity/sensor/#available-state-classes" ) @@ -488,7 +490,8 @@ ) TRANSLATION_DESCRIPTION_PLACEHOLDERS = { - "templating_url": TEMPLATING_URL, + "command_templating_url": COMMAND_TEMPLATING_URL, + "value_templating_url": VALUE_TEMPLATING_URL, "available_state_classes_url": AVAILABLE_STATE_CLASSES_URL, "naming_entities_url": NAMING_ENTITIES_URL, "registry_properties_url": REGISTRY_PROPERTIES_URL, diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index b7540e7159c693..a9ef3907e44497 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -153,7 +153,7 @@ }, "data_description": { "availability_topic": "Topic to receive the availability payload on", - "availability_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-templates-with-the-mqtt-integration) to render the availability payload received on the availability topic", + "availability_template": "A [template]({value_templating_url}) to render the availability payload received on the availability topic", "payload_available": "The payload that indicates the device is available (defaults to 'online')", "payload_not_available": "The payload that indicates the device is not available (defaults to 'offline')" } @@ -221,7 +221,7 @@ }, "data_description": { "platform": "The type of the entity to configure.", - "name": "The name of the entity. Leave empty to set it to `None` to [mark it as main feature of the MQTT device](https://www.home-assistant.io/integrations/mqtt/#naming-of-mqtt-entities).", + "name": "The name of the entity. Leave empty to set it to `None` to [mark it as main feature of the MQTT device]({naming_entities_url}).", "entity_picture": "An URL to a picture to be assigned." } }, @@ -288,7 +288,7 @@ "climate_feature_target_temperature": "The climate supports setting the target temperature.", "climate_feature_target_humidity": "The climate supports setting the target humidity.", "device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)", - "entity_category": "Allows marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)", + "entity_category": "Allows marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configuration class. [Learn more.]({registry_properties_url})", "fan_feature_speed": "The fan supports multiple speeds.", "fan_feature_preset_modes": "The fan supports preset modes.", "fan_feature_oscillation": "The fan supports oscillation.", @@ -296,7 +296,7 @@ "image_processing_mode": "Select how the image data is received.", "options": "Options for allowed sensor state values. The sensor’s Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement.", "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", - "state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)", + "state_class": "The [State class]({available_state_classes_url}) of the sensor. [Learn more.]({url}#state_class)", "suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)", "supported_features": "The features that the entity supports.", "temperature_unit": "This determines the native unit of measurement the MQTT climate device works with.", @@ -370,32 +370,32 @@ }, "data_description": { "available_tones": "The siren supports tones. The `tone` variable will become available for use in the \"Command template\" setting. [Learn more.]({url}#available_tones)", - "blue_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", - "brightness_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", + "blue_template": "[Template]({value_templating_url}) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", + "brightness_template": "[Template]({value_templating_url}) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", "code": "Specifies a code to enable or disable the alarm in the frontend. Note that this blocks sending MQTT message commands to the remote device if the code validation fails. [Learn more.]({url}#code)", "code_format": "A regular expression to validate a supplied code when it is set during the action to open, lock or unlock the MQTT lock. [Learn more.]({url}#code_format)", "code_arm_required": "If set, the code is required to arm the alarm. If not set, the code is not validated.", "code_disarm_required": "If set, the code is required to disarm the alarm. If not set, the code is not validated.", "code_trigger_required": "If set, the code is required to manually trigger the alarm. If not set, the code is not validated.", - "color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.", - "command_off_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"off\" state changes. Available variables are: `state` and `transition`.", - "command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", - "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic. [Learn more.]({url}#command_template)", + "color_temp_template": "[Template]({value_templating_url}) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.", + "command_off_template": "The [template]({command_templating_url}) for \"off\" state changes. Available variables are: `state` and `transition`.", + "command_on_template": "The [template]({command_templating_url}) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", + "command_template": "A [template]({command_templating_url}) to render the payload to be published at the command topic. [Learn more.]({url}#command_template)", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "content_type": "The content type or the image data that is received at the image topic.", "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", - "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", + "green_template": "[Template]({value_templating_url}) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "image_encoding": "Select the encoding of the received image data", "image_topic": "The MQTT topic subscribed to receive messages containing the image data. [Learn more.]({url}#image_topic)", - "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", + "last_reset_value_template": "Defines a [template]({value_templating_url}) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "max": "Maximum value. [Learn more.]({url}#max)", "min": "Minimum value. [Learn more.]({url}#min)", "mode": "Control how the number should be displayed in the UI. [Learn more.]({url}#mode)", "modes": "A list of supported operation modes. [Learn more.]({url}#modes)", "mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)", - "mode_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)", + "mode_command_template": "[Template]({command_templating_url}) to define the operation mode to be sent to the operation mode command topic. [Learn more.]({url}#mode_command_template)", "mode_state_topic": "The MQTT topic subscribed to receive operation mode state messages. [Learn more.]({url}#mode_state_topic)", - "mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)", + "mode_state_template": "Defines a [template]({value_templating_url}) to extract the operation mode state. [Learn more.]({url}#mode_state_template)", "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "options": "List of options that can be selected.", @@ -404,19 +404,19 @@ "payload_press": "The payload to send when the button is triggered.", "payload_reset": "The payload received at the state topic that resets the entity to an unknown state.", "qos": "The QoS value a {platform} entity should use.", - "red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", + "red_template": "[Template]({value_templating_url}) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "state_off": "The incoming payload that represents the \"off\" state. Use only when the value that represents \"off\" state in the state topic is different from value that should be sent to the command topic to turn the device off.", "state_on": "The incoming payload that represents the \"on\" state. Use only when the value that represents \"on\" state in the state topic is different from value that should be sent to the command topic to turn the device on.", - "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", + "state_template": "[Template]({value_templating_url}) to extract state from the state payload value.", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", "step": "Step value. Smallest value 0.001.", "support_duration": "The siren supports setting a duration in second. The `duration` variable will become available for use in the \"Command template\" setting. [Learn more.]({url}#support_duration)", "support_volume_set": "The siren supports setting a volume. The `volume_level` variable will become available for use in the \"Command template\" setting. [Learn more.]({url}#support_volume_set)", "supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", - "url_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)", + "url_template": "[Template]({value_templating_url}) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)", "url_topic": "The MQTT topic subscribed to receive messages containing the image URL. [Learn more.]({url}#url_topic)", - "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)" + "value_template": "Defines a [template]({value_templating_url}) to extract the {platform} entity value. [Learn more.]({url}#value_template)" }, "sections": { "advanced_settings": { @@ -470,7 +470,7 @@ "action_topic": "Action topic" }, "data_description": { - "action_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the action topic with.", + "action_template": "A [template]({value_templating_url}) to render the value received on the action topic with.", "action_topic": "The MQTT topic to subscribe for changes of the current action. If this is set, the climate graph uses the value received as data source. A \"None\" payload resets the current action state. An empty payload is ignored. Valid action values are: \"off\", \"heating\", \"cooling\", \"drying\", \"idle\" and \"fan\". [Learn more.]({url}#action_topic)" } }, @@ -486,9 +486,9 @@ "data_description": { "fan_modes": "List of fan modes this climate is capable of running at. Common fan modes that offer translations are `off`, `on`, `auto`, `low`, `medium`, `high`, `middle`, `focus` and `diffuse`.", "fan_mode_command_topic": "The MQTT topic to publish commands to change the climate fan mode. [Learn more.]({url}#fan_mode_command_topic)", - "fan_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the fan mode command topic.", + "fan_mode_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the fan mode command topic.", "fan_mode_state_topic": "The MQTT topic subscribed to receive the climate fan mode. [Learn more.]({url}#fan_mode_state_topic)", - "fan_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate fan mode value." + "fan_mode_state_template": "Defines a [template]({value_templating_url}) to extract the climate fan mode value." } }, "climate_power_settings": { @@ -502,7 +502,7 @@ "data_description": { "payload_off": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_off%]", "payload_on": "[%key:component::mqtt::config_subentries::device::step::mqtt_platform_config::data_description::payload_on%]", - "power_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", + "power_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the power command topic. The `value` parameter is the payload set for payload \"on\" or payload \"off\".", "power_command_topic": "The MQTT topic to publish commands to change the climate power state. Sends the payload configured with payload \"on\" or payload \"off\". [Learn more.]({url}#power_command_topic)" } }, @@ -535,9 +535,9 @@ "data_description": { "swing_horizontal_modes": "List of horizontal swing modes this climate is capable of running at. Common horizontal swing modes that offer translations are `off` and `on`.", "swing_horizontal_mode_command_topic": "The MQTT topic to publish commands to change the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_command_topic)", - "swing_horizontal_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the horizontal swing mode command topic.", + "swing_horizontal_mode_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the horizontal swing mode command topic.", "swing_horizontal_mode_state_topic": "The MQTT topic subscribed to receive the climate horizontal swing mode. [Learn more.]({url}#swing_horizontal_mode_state_topic)", - "swing_horizontal_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate horizontal swing mode value." + "swing_horizontal_mode_state_template": "Defines a [template]({value_templating_url}) to extract the climate horizontal swing mode value." } }, "climate_swing_mode_settings": { @@ -552,9 +552,9 @@ "data_description": { "swing_modes": "List of swing modes this climate is capable of running at. Common swing modes that offer translations are `off`, `on`, `vertical`, `horizontal` and `both`.", "swing_mode_command_topic": "The MQTT topic to publish commands to change the climate swing mode. [Learn more.]({url}#swing_mode_command_topic)", - "swing_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the swing mode command topic.", + "swing_mode_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the swing mode command topic.", "swing_mode_state_topic": "The MQTT topic subscribed to receive the climate swing mode. [Learn more.]({url}#swing_mode_state_topic)", - "swing_mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the climate swing mode value." + "swing_mode_state_template": "Defines a [template]({value_templating_url}) to extract the climate swing mode value." } }, "cover_payload_settings": { @@ -595,9 +595,9 @@ "data_description": { "position_closed": "Number which represents \"closed\" position.", "position_open": "Number which represents \"open\" position.", - "position_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the payload for the position topic. Within the template the following variables are also available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#position_template)", + "position_template": "Defines a [template]({value_templating_url}) to extract the payload for the position topic. Within the template the following variables are also available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#position_template)", "position_topic": "The MQTT topic subscribed to receive cover position state messages. [Learn more.]({url}#position_topic)", - "set_position_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the set position topic. Within the template the following variables are available: `value` (the scaled target position), `entity_id`, `position` (the target position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#set_position_template)", + "set_position_template": "[Template]({command_templating_url}) to define the position to be sent to the set position topic. Within the template the following variables are available: `value` (the scaled target position), `entity_id`, `position` (the target position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#set_position_template)", "set_position_topic": "The MQTT topic to publish position commands to. You need to use the set position topic as well if you want to use the position topic. Use template if position topic wants different values than within range \"position closed\" - \"position_open\". If template is not defined and position \"closed\" != 100 and position \"open\" != 0 then proper position value is calculated from percentage position. [Learn more.]({url}#set_position_topic)" } }, @@ -616,12 +616,12 @@ }, "data_description": { "tilt_closed_value": "The value that will be sent to the \"tilt command topic\" when the cover tilt is closed.", - "tilt_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the tilt command topic. Within the template the following variables are available: `entity_id`, `tilt_position` (the target tilt position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_command_template)", + "tilt_command_template": "[Template]({command_templating_url}) to define the position to be sent to the tilt command topic. Within the template the following variables are available: `entity_id`, `tilt_position` (the target tilt position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_command_template)", "tilt_command_topic": "The MQTT topic to publish commands to control the cover tilt. [Learn more.]({url}#tilt_command_topic)", "tilt_max": "The maximum tilt value.", "tilt_min": "The minimum tilt value.", "tilt_opened_value": "The value that will be sent to the \"tilt command topic\" when the cover tilt is opened.", - "tilt_status_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the payload for the tilt status topic. Within the template the following variables are available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_status_template)", + "tilt_status_template": "Defines a [template]({value_templating_url}) to extract the payload for the tilt status topic. Within the template the following variables are available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_status_template)", "tilt_status_topic": "The MQTT topic subscribed to receive tilt status update values. [Learn more.]({url}#tilt_status_topic)", "tilt_optimistic": "Flag that defines if tilt works in optimistic mode. If tilt status topic is not defined, tilt works in optimistic mode by default. [Learn more.]({url}#tilt_optimistic)" } @@ -633,7 +633,7 @@ "current_humidity_topic": "Current humidity topic" }, "data_description": { - "current_humidity_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current humidity value. [Learn more.]({url}#current_humidity_template)", + "current_humidity_template": "Defines a [template]({value_templating_url}) to extract the current humidity value. [Learn more.]({url}#current_humidity_template)", "current_humidity_topic": "The MQTT topic subscribed to receive current humidity update values. [Learn more.]({url}#current_humidity_topic)" } }, @@ -644,7 +644,7 @@ "current_temperature_topic": "Current temperature topic" }, "data_description": { - "current_temperature_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the current temperature value. [Learn more.]({url}#current_temperature_template)", + "current_temperature_template": "Defines a [template]({value_templating_url}) to extract the current temperature value. [Learn more.]({url}#current_temperature_template)", "current_temperature_topic": "The MQTT topic subscribed to receive current temperature update values. [Learn more.]({url}#current_temperature_topic)" } }, @@ -660,11 +660,11 @@ }, "data_description": { "brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.", - "brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.", + "brightness_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the brightness command topic.", "brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)", "brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.", "brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)", - "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." + "brightness_value_template": "Defines a [template]({value_templating_url}) to extract the brightness value." } }, "lock_payload_settings": { @@ -702,9 +702,9 @@ }, "data_description": { "direction_command_topic": "The MQTT topic to publish commands to change the fan direction. The payload will be either `forward` or `reverse` and can be customized using the direction command template. [Learn more.]({url}#direction_command_topic)", - "direction_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the direction command topic. The template variable `value` will be either `forward` or `reverse`.", + "direction_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the direction command topic. The template variable `value` will be either `forward` or `reverse`.", "direction_state_topic": "The MQTT topic subscribed to receive fan direction state. Accepted state payloads are `forward` or `reverse`. [Learn more.]({url}#direction_state_topic)", - "direction_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan direction state value. The template should return either `forward` or `reverse`. When the template returns an empty string, the direction will be ignored." + "direction_value_template": "Defines a [template]({value_templating_url}) to extract fan direction state value. The template should return either `forward` or `reverse`. When the template returns an empty string, the direction will be ignored." } }, "fan_oscillation_settings": { @@ -719,9 +719,9 @@ }, "data_description": { "oscillation_command_topic": "The MQTT topic to publish commands to change the fan oscillation state. [Learn more.]({url}#oscillation_command_topic)", - "oscillation_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the oscillation command topic.", + "oscillation_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the oscillation command topic.", "oscillation_state_topic": "The MQTT topic subscribed to receive fan oscillation state. [Learn more.]({url}#oscillation_state_topic)", - "oscillation_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan oscillation state value.", + "oscillation_value_template": "Defines a [template]({value_templating_url}) to extract fan oscillation state value.", "payload_oscillation_off": "The payload that represents the oscillation \"off\" state.", "payload_oscillation_on": "The payload that represents the oscillation \"on\" state." } @@ -740,9 +740,9 @@ "payload_reset_preset_mode": "A special payload that resets the fan preset mode state attribute to unknown when received at the preset mode state topic.", "preset_modes": "List of preset modes this fan is capable of running at. Common examples include auto, smart, whoosh, eco and breeze.", "preset_mode_command_topic": "The MQTT topic to publish commands to change the fan preset mode. [Learn more.]({url}#preset_mode_command_topic)", - "preset_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the preset mode command topic.", + "preset_mode_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the preset mode command topic.", "preset_mode_state_topic": "The MQTT topic subscribed to receive fan preset mode. [Learn more.]({url}#preset_mode_state_topic)", - "preset_mode_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan preset mode value." + "preset_mode_value_template": "Defines a [template]({value_templating_url}) to extract fan preset mode value." } }, "fan_speed_settings": { @@ -759,9 +759,9 @@ "data_description": { "payload_reset_percentage": "A special payload that resets the fan speed percentage state attribute to unknown when received at the percentage state topic.", "percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage. [Learn more.]({url}#percentage_command_topic)", - "percentage_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the percentage command topic.", + "percentage_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the percentage command topic.", "percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)", - "percentage_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the speed percentage value.", + "percentage_value_template": "Defines a [template]({value_templating_url}) to extract the speed percentage value.", "speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The percentage step is 100 / the number of speeds within the \"speed range\".", "speed_range_max": "The maximum of numeric output range (representing 100 %). The percentage step is 100 / number of speeds within the \"speed range\"." } @@ -774,7 +774,7 @@ }, "data_description": { "color_mode_state_topic": "The MQTT topic subscribed to receive color mode updates. If this is not configured, the color mode will be automatically set according to the last received valid color or color temperature.", - "color_mode_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the color mode value." + "color_mode_value_template": "Defines a [template]({value_templating_url}) to extract the color mode value." } }, "light_color_temp_settings": { @@ -786,10 +786,10 @@ "color_temp_value_template": "Color temperature value template" }, "data_description": { - "color_temp_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the color temperature command topic.", + "color_temp_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the color temperature command topic.", "color_temp_command_topic": "The publishing topic that will be used to control the color temperature. [Learn more.]({url}#color_temp_command_topic)", "color_temp_state_topic": "The MQTT topic subscribed to receive color temperature state updates. [Learn more.]({url}#color_temp_state_topic)", - "color_temp_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the color temperature value." + "color_temp_value_template": "Defines a [template]({value_templating_url}) to extract the color temperature value." } }, "light_effect_settings": { @@ -805,7 +805,7 @@ }, "data_description": { "effect": "Flag that defines if the light supports effects.", - "effect_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the effect command topic.", + "effect_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the effect command topic.", "effect_command_topic": "The publishing topic that will be used to control the light's effect state. [Learn more.]({url}#effect_command_topic)", "effect_list": "The list of effects the light supports.", "effect_state_topic": "The MQTT topic subscribed to receive effect state updates. [Learn more.]({url}#effect_state_topic)" @@ -820,10 +820,10 @@ "hs_value_template": "HS value template" }, "data_description": { - "hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.", + "hs_command_template": "Defines a [template]({command_templating_url}) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.", "hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)", "hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)", - "hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value." + "hs_value_template": "Defines a [template]({value_templating_url}) to extract the HS value." } }, "light_rgb_settings": { @@ -835,10 +835,10 @@ "rgb_value_template": "RGB value template" }, "data_description": { - "rgb_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to RGB command topic. Available variables: `red`, `green` and `blue`.", + "rgb_command_template": "Defines a [template]({command_templating_url}) to compose message which will be sent to RGB command topic. Available variables: `red`, `green` and `blue`.", "rgb_command_topic": "The MQTT topic to publish commands to change the light’s RGB state. [Learn more.]({url}#rgb_command_topic)", "rgb_state_topic": "The MQTT topic subscribed to receive RGB state updates. The expected payload is the RGB values separated by commas, for example, `255,0,127`. [Learn more.]({url}#rgb_state_topic)", - "rgb_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the RGB value." + "rgb_value_template": "Defines a [template]({value_templating_url}) to extract the RGB value." } }, "light_rgbw_settings": { @@ -850,10 +850,10 @@ "rgbw_value_template": "RGBW value template" }, "data_description": { - "rgbw_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to RGBW command topic. Available variables: `red`, `green`, `blue` and `white`.", + "rgbw_command_template": "Defines a [template]({command_templating_url}) to compose message which will be sent to RGBW command topic. Available variables: `red`, `green`, `blue` and `white`.", "rgbw_command_topic": "The MQTT topic to publish commands to change the light’s RGBW state. [Learn more.]({url}#rgbw_command_topic)", "rgbw_state_topic": "The MQTT topic subscribed to receive RGBW state updates. The expected payload is the RGBW values separated by commas, for example, `255,0,127,64`. [Learn more.]({url}#rgbw_state_topic)", - "rgbw_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the RGBW value." + "rgbw_value_template": "Defines a [template]({value_templating_url}) to extract the RGBW value." } }, "light_rgbww_settings": { @@ -865,10 +865,10 @@ "rgbww_value_template": "RGBWW value template" }, "data_description": { - "rgbww_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to RGBWW command topic. Available variables: `red`, `green`, `blue`, `cold_white` and `warm_white`.", + "rgbww_command_template": "Defines a [template]({command_templating_url}) to compose message which will be sent to RGBWW command topic. Available variables: `red`, `green`, `blue`, `cold_white` and `warm_white`.", "rgbww_command_topic": "The MQTT topic to publish commands to change the light’s RGBWW state. [Learn more.]({url}#rgbww_command_topic)", "rgbww_state_topic": "The MQTT topic subscribed to receive RGBWW state updates. The expected payload is the RGBWW values separated by commas, for example, `255,0,127,64,32`. [Learn more.]({url}#rgbww_state_topic)", - "rgbww_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the RGBWW value." + "rgbww_value_template": "Defines a [template]({value_templating_url}) to extract the RGBWW value." } }, "light_white_settings": { @@ -891,10 +891,10 @@ "xy_value_template": "XY value template" }, "data_description": { - "xy_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to XY command topic. Available variables: `x` and `y`.", + "xy_command_template": "Defines a [template]({command_templating_url}) to compose message which will be sent to XY command topic. Available variables: `x` and `y`.", "xy_command_topic": "The MQTT topic to publish commands to change the light’s XY state. [Learn more.]({url}#xy_command_topic)", "xy_state_topic": "The MQTT topic subscribed to receive XY state updates. The expected payload is the X and Y color values separated by commas, for example, `0.675,0.322`. [Learn more.]({url}#xy_state_topic)", - "xy_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the XY value." + "xy_value_template": "Defines a [template]({value_templating_url}) to extract the XY value." } }, "target_humidity_settings": { @@ -910,9 +910,9 @@ "data_description": { "max_humidity": "The maximum target humidity that can be set.", "min_humidity": "The minimum target humidity that can be set.", - "target_humidity_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the target humidity command topic.", + "target_humidity_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the target humidity command topic.", "target_humidity_command_topic": "The MQTT topic to publish commands to change the climate target humidity. [Learn more.]({url}#humidity_command_topic)", - "target_humidity_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the target humidity state topic with.", + "target_humidity_state_template": "A [template]({value_templating_url}) to render the value received on the target humidity state topic with.", "target_humidity_state_topic": "The MQTT topic to subscribe for changes of the target humidity. [Learn more.]({url}#humidity_state_topic)" } }, @@ -922,7 +922,7 @@ "command_off_template": "Command \"off\" template" }, "data_description": { - "command_off_template": "The [template]({templating_url}#using-command-templates-with-mqtt) for \"off\" state changes. By default the \"[Command template]({url}#command_template)\" will be used. [Learn more.]({url}#command_off_template)" + "command_off_template": "The [template]({command_templating_url}) for \"off\" state changes. By default the \"[Command template]({url}#command_template)\" will be used. [Learn more.]({url}#command_off_template)" } }, "target_temperature_settings": { @@ -952,17 +952,17 @@ "min_temp": "The minimum target temperature that can be set.", "precision": "The precision in degrees the thermostat is working at.", "temp_step": "The target temperature step in degrees Celsius or Fahrenheit.", - "temperature_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the temperature command topic.", + "temperature_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the temperature command topic.", "temperature_command_topic": "The MQTT topic to publish commands to change the climate target temperature. [Learn more.]({url}#temperature_command_topic)", - "temperature_high_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the upper temperature command topic.", + "temperature_high_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the upper temperature command topic.", "temperature_high_command_topic": "The MQTT topic to publish commands to change the climate upper target temperature. [Learn more.]({url}#temperature_high_command_topic)", - "temperature_low_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the lower temperature command topic.", + "temperature_low_command_template": "A [template]({command_templating_url}) to compose the payload to be published at the lower temperature command topic.", "temperature_low_command_topic": "The MQTT topic to publish commands to change the climate lower target temperature. [Learn more.]({url}#temperature_low_command_topic)", - "temperature_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the temperature state topic with.", + "temperature_state_template": "A [template]({value_templating_url}) to render the value received on the temperature state topic with.", "temperature_state_topic": "The MQTT topic to subscribe for changes of the target temperature. [Learn more.]({url}#temperature_state_topic)", - "temperature_high_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the upper temperature state topic with.", + "temperature_high_state_template": "A [template]({value_templating_url}) to render the value received on the upper temperature state topic with.", "temperature_high_state_topic": "The MQTT topic to subscribe for changes of the upper target temperature. [Learn more.]({url}#temperature_high_state_topic)", - "temperature_low_state_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to render the value received on the lower temperature state topic with.", + "temperature_low_state_template": "A [template]({value_templating_url}) to render the value received on the lower temperature state topic with.", "temperature_low_state_topic": "The MQTT topic to subscribe for changes of the lower target temperature. [Learn more.]({url}#temperature_low_state_topic)" } } diff --git a/homeassistant/components/nintendo_parental_controls/manifest.json b/homeassistant/components/nintendo_parental_controls/manifest.json index 99a717f95898af..973da73bd252bf 100644 --- a/homeassistant/components/nintendo_parental_controls/manifest.json +++ b/homeassistant/components/nintendo_parental_controls/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pynintendoparental"], "quality_scale": "bronze", - "requirements": ["pynintendoparental==1.1.1"] + "requirements": ["pynintendoparental==1.1.2"] } diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 9df044ebc8bbad..55b8335acc5cc7 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -50,6 +50,7 @@ BaseUnitConverter, ReactiveEnergyConverter, TemperatureConverter, + TemperatureDeltaConverter, VolumeFlowRateConverter, ) @@ -381,6 +382,12 @@ class NumberDeviceClass(StrEnum): Unit of measurement: `°C`, `°F`, `K` """ + TEMPERATURE_DELTA = "temperature_delta" + """Difference of temperatures - Temperature range. + + Unit of measurement: `°C`, `°F`, `K` + """ + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. @@ -540,6 +547,7 @@ class NumberDeviceClass(StrEnum): NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux}, NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature), + NumberDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature), NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, @@ -568,6 +576,7 @@ class NumberDeviceClass(StrEnum): UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = { NumberDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter, NumberDeviceClass.TEMPERATURE: TemperatureConverter, + NumberDeviceClass.TEMPERATURE_DELTA: TemperatureDeltaConverter, NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, } diff --git a/homeassistant/components/number/icons.json b/homeassistant/components/number/icons.json index 9d75e09a72ddb1..b02583815d3b26 100644 --- a/homeassistant/components/number/icons.json +++ b/homeassistant/components/number/icons.json @@ -135,6 +135,9 @@ "temperature": { "default": "mdi:thermometer" }, + "temperature_delta": { + "default": "mdi:thermometer" + }, "volatile_organic_compounds": { "default": "mdi:molecule" }, diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py index e8cdd78e3215db..c8a3a1d7270d08 100644 --- a/homeassistant/components/number/significant_change.py +++ b/homeassistant/components/number/significant_change.py @@ -47,7 +47,10 @@ def async_check_significant_change( percentage_change: float | None = None # special for temperature - if device_class == NumberDeviceClass.TEMPERATURE: + if device_class in ( + NumberDeviceClass.TEMPERATURE, + NumberDeviceClass.TEMPERATURE_DELTA, + ): if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.FAHRENHEIT: absolute_change = 1.0 else: diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index b3cc5f7c814c7c..e9e313b1bba5b6 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -157,6 +157,9 @@ "temperature": { "name": "[%key:component::sensor::entity_component::temperature::name%]" }, + "temperature_delta": { + "name": "[%key:component::sensor::entity_component::temperature_delta::name%]" + }, "volatile_organic_compounds": { "name": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]" }, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 647053fe9c2f4c..1b3841fc107187 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -65,6 +65,7 @@ ReactivePowerConverter, SpeedConverter, TemperatureConverter, + TemperatureDeltaConverter, UnitlessRatioConverter, VolumeConverter, VolumeFlowRateConverter, @@ -223,6 +224,7 @@ def query_circular_mean(table: type[StatisticsBase]) -> tuple[Label, Label]: _SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [ CarbonMonoxideConcentrationConverter, + TemperatureDeltaConverter, ] STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 6aa8c44ad4a12e..09d820485dcabf 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -39,6 +39,7 @@ ReactivePowerConverter, SpeedConverter, TemperatureConverter, + TemperatureDeltaConverter, UnitlessRatioConverter, VolumeConverter, VolumeFlowRateConverter, @@ -94,6 +95,9 @@ vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS), vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("temperature_delta"): vol.In( + TemperatureDeltaConverter.VALID_UNITS + ), vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), vol.Optional("volume_flow_rate"): vol.In(VolumeFlowRateConverter.VALID_UNITS), diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 1e716b193c1e1e..af9391badda751 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -76,6 +76,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: SENSOR_DESCRIPTIONS = [ RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, key="main_brush_time_left", device_class=SensorDeviceClass.DURATION, translation_key="main_brush_time_left", @@ -85,6 +86,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: ), RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, key="side_brush_time_left", device_class=SensorDeviceClass.DURATION, translation_key="side_brush_time_left", @@ -94,6 +96,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: ), RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, key="filter_time_left", device_class=SensorDeviceClass.DURATION, translation_key="filter_time_left", @@ -121,6 +124,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: ), RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, key="sensor_time_left", device_class=SensorDeviceClass.DURATION, translation_key="sensor_time_left", @@ -129,6 +133,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: ), RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.MINUTES, key="cleaning_time", translation_key="cleaning_time", device_class=SensorDeviceClass.DURATION, @@ -137,6 +142,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: ), RoborockSensorDescription( native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, key="total_cleaning_time", translation_key="total_cleaning_time", device_class=SensorDeviceClass.DURATION, @@ -257,6 +263,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: RoborockSensorDescriptionA01( key="filter_time_left", data_protocol=RoborockDyadDataProtocol.MESH_LEFT, + suggested_unit_of_measurement=UnitOfTime.HOURS, native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, translation_key="filter_time_left", @@ -266,6 +273,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: key="brush_remaining", data_protocol=RoborockDyadDataProtocol.BRUSH_LEFT, native_unit_of_measurement=UnitOfTime.SECONDS, + suggested_unit_of_measurement=UnitOfTime.HOURS, device_class=SensorDeviceClass.DURATION, translation_key="brush_remaining", entity_category=EntityCategory.DIAGNOSTIC, @@ -281,6 +289,7 @@ def _dock_error_value_fn(properties: DeviceProp) -> str | None: RoborockSensorDescriptionA01( key="total_cleaning_time", native_unit_of_measurement=UnitOfTime.MINUTES, + suggested_unit_of_measurement=UnitOfTime.HOURS, data_protocol=RoborockDyadDataProtocol.TOTAL_RUN_TIME, device_class=SensorDeviceClass.DURATION, translation_key="total_cleaning_time", diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index b1b35385495d81..5973d1ccea6722 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.8.2"], + "requirements": ["aiorussound==4.9.0"], "zeroconf": ["_rio._tcp.local."] } diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 86af837e2b3cac..2731e9f6b03f58 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -69,6 +69,7 @@ ReactivePowerConverter, SpeedConverter, TemperatureConverter, + TemperatureDeltaConverter, UnitlessRatioConverter, VolumeConverter, VolumeFlowRateConverter, @@ -417,6 +418,12 @@ class SensorDeviceClass(StrEnum): Unit of measurement: `°C`, `°F`, `K` """ + TEMPERATURE_DELTA = "temperature_delta" + """Difference of temperatures - Temperature range. + + Unit of measurement: `°C`, `°F`, `K` + """ + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. @@ -564,6 +571,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.TEMPERATURE_DELTA: TemperatureDeltaConverter, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: MassVolumeConcentrationConverter, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: UnitlessRatioConverter, SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, @@ -651,6 +659,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux}, SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature), + SensorDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature), SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, @@ -715,6 +724,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.SOUND_PRESSURE: (UnitOfSoundPressure.DECIBEL, 0), SensorDeviceClass.SPEED: (UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), SensorDeviceClass.TEMPERATURE: (UnitOfTemperature.KELVIN, 1), + SensorDeviceClass.TEMPERATURE_DELTA: (UnitOfTemperature.KELVIN, 1), SensorDeviceClass.VOLTAGE: (UnitOfElectricPotential.VOLT, 0), SensorDeviceClass.VOLUME: (UnitOfVolume.MILLILITERS, 0), SensorDeviceClass.VOLUME_FLOW_RATE: (UnitOfVolumeFlowRate.LITERS_PER_SECOND, 0), @@ -779,6 +789,7 @@ class SensorStateClass(StrEnum): SensorDeviceClass.SPEED: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.SULPHUR_DIOXIDE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.TEMPERATURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.TEMPERATURE_DELTA: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.TIMESTAMP: set(), SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: {SensorStateClass.MEASUREMENT}, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index e238b1d9a9b0b3..ba5eb1fae2ae60 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -78,6 +78,7 @@ CONF_IS_SOUND_PRESSURE = "is_sound_pressure" CONF_IS_SULPHUR_DIOXIDE = "is_sulphur_dioxide" CONF_IS_TEMPERATURE = "is_temperature" +CONF_IS_TEMPERATURE_DELTA = "is_temperature_delta" CONF_IS_VALUE = "is_value" CONF_IS_VOLATILE_ORGANIC_COMPOUNDS = "is_volatile_organic_compounds" CONF_IS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "is_volatile_organic_compounds_parts" @@ -140,6 +141,7 @@ SensorDeviceClass.SPEED: [{CONF_TYPE: CONF_IS_SPEED}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_IS_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], + SensorDeviceClass.TEMPERATURE_DELTA: [{CONF_TYPE: CONF_IS_TEMPERATURE_DELTA}], SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ {CONF_TYPE: CONF_IS_VOLATILE_ORGANIC_COMPOUNDS} ], @@ -208,6 +210,7 @@ CONF_IS_SPEED, CONF_IS_SULPHUR_DIOXIDE, CONF_IS_TEMPERATURE, + CONF_IS_TEMPERATURE_DELTA, CONF_IS_VOLATILE_ORGANIC_COMPOUNDS, CONF_IS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, CONF_IS_VOLTAGE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1aacdbf507f935..b0babe30312968 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -77,6 +77,7 @@ CONF_SPEED = "speed" CONF_SULPHUR_DIOXIDE = "sulphur_dioxide" CONF_TEMPERATURE = "temperature" +CONF_TEMPERATURE_DELTA = "temperature_delta" CONF_VALUE = "value" CONF_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" CONF_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" @@ -139,6 +140,7 @@ SensorDeviceClass.SPEED: [{CONF_TYPE: CONF_SPEED}], SensorDeviceClass.SULPHUR_DIOXIDE: [{CONF_TYPE: CONF_SULPHUR_DIOXIDE}], SensorDeviceClass.TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + SensorDeviceClass.TEMPERATURE_DELTA: [{CONF_TYPE: CONF_TEMPERATURE_DELTA}], SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: [ {CONF_TYPE: CONF_VOLATILE_ORGANIC_COMPOUNDS} ], @@ -208,6 +210,7 @@ CONF_SPEED, CONF_SULPHUR_DIOXIDE, CONF_TEMPERATURE, + CONF_TEMPERATURE_DELTA, CONF_VOLATILE_ORGANIC_COMPOUNDS, CONF_VOLATILE_ORGANIC_COMPOUNDS_PARTS, CONF_VOLTAGE, diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index 740b2df7e5b37f..fac1a681b4424f 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -154,6 +154,9 @@ "temperature": { "default": "mdi:thermometer" }, + "temperature_delta": { + "default": "mdi:thermometer" + }, "timestamp": { "default": "mdi:clock" }, diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 00de36fc67cd23..06598a1d0a058e 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -45,16 +45,19 @@ def async_check_significant_change( absolute_change: float | None = None percentage_change: float | None = None - if device_class == SensorDeviceClass.TEMPERATURE: + if device_class in ( + SensorDeviceClass.TEMPERATURE, + SensorDeviceClass.TEMPERATURE_DELTA, + ): if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.FAHRENHEIT: absolute_change = 1.0 else: absolute_change = 0.5 - if device_class in (SensorDeviceClass.BATTERY, SensorDeviceClass.HUMIDITY): + elif device_class in (SensorDeviceClass.BATTERY, SensorDeviceClass.HUMIDITY): absolute_change = 1.0 - if device_class in ( + elif device_class in ( SensorDeviceClass.AQI, SensorDeviceClass.CO, SensorDeviceClass.CO2, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 81a67b78adad28..2c02bb73bdd68d 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -47,6 +47,7 @@ "is_speed": "Current {entity_name} speed", "is_sulphur_dioxide": "Current {entity_name} sulphur dioxide concentration level", "is_temperature": "Current {entity_name} temperature", + "is_temperature_delta": "Current {entity_name} temperature interval", "is_value": "Current {entity_name} value", "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level", "is_volatile_organic_compounds_parts": "[%key:component::sensor::device_automation::condition_type::is_volatile_organic_compounds%]", @@ -104,6 +105,7 @@ "speed": "{entity_name} speed changes", "sulphur_dioxide": "{entity_name} sulphur dioxide concentration changes", "temperature": "{entity_name} temperature changes", + "temperature_delta": "{entity_name} temperature interval changes", "value": "{entity_name} value changes", "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes", "volatile_organic_compounds_parts": "[%key:component::sensor::device_automation::trigger_type::volatile_organic_compounds%]", @@ -290,6 +292,9 @@ "temperature": { "name": "Temperature" }, + "temperature_delta": { + "name": "Temperature delta" + }, "timestamp": { "name": "Timestamp" }, diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c726921fc9023e..649fcb90fa879f 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping, Sequence +from copy import deepcopy from enum import StrEnum from functools import cache import importlib @@ -1153,7 +1154,7 @@ class ObjectSelectorField(TypedDict, total=False): label: str required: bool - selector: Required[dict[str, Any]] + selector: Required[Selector | dict[str, Any]] class ObjectSelectorConfig(BaseSelectorConfig, total=False): @@ -1176,7 +1177,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]): { vol.Optional("fields"): { str: { - vol.Required("selector"): validate_selector, + vol.Required("selector"): vol.Any(Selector, validate_selector), vol.Optional("required"): bool, vol.Optional("label"): str, } @@ -1192,6 +1193,21 @@ def __init__(self, config: ObjectSelectorConfig | None = None) -> None: """Instantiate a selector.""" super().__init__(config) + def serialize(self) -> dict[str, dict[str, ObjectSelectorConfig]]: + """Serialize ObjectSelector for voluptuous_serialize.""" + _config = deepcopy(self.config) + if "fields" in _config: + for field_items in _config["fields"].values(): + if isinstance(field_items["selector"], ObjectSelector): + field_items["selector"] = field_items["selector"].serialize() + elif isinstance(field_items["selector"], Selector): + field_items["selector"] = { + field_items["selector"].selector_type: field_items[ + "selector" + ].config + } + return {"selector": {self.selector_type: _config}} + def __call__(self, data: Any) -> Any: """Validate the passed selection.""" if "fields" not in self.config: diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index c3deae749a9edd..a03eeb5ddb81bc 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -750,6 +750,25 @@ def _celsius_to_kelvin(cls, celsius: float) -> float: return celsius + 273.15 +class TemperatureDeltaConverter(BaseUnitConverter): + """Utility to convert temperature intervals. + + eg. a 10°C interval (10°C to 20°C) will return a 18°F (50°F to 68°F) interval + """ + + UNIT_CLASS = "temperature_delta" + VALID_UNITS = { + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.KELVIN, + } + _UNIT_CONVERSION = { + UnitOfTemperature.CELSIUS: 1.0, + UnitOfTemperature.FAHRENHEIT: 1.8, + UnitOfTemperature.KELVIN: 1.0, + } + + class UnitlessRatioConverter(BaseUnitConverter): """Utility to convert unitless ratios.""" diff --git a/requirements_all.txt b/requirements_all.txt index 4399edbfb8143a..399383d681af0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.1 # homeassistant.components.alexa_devices -aioamazondevices==6.4.3 +aioamazondevices==6.4.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -381,7 +381,7 @@ aioridwell==2025.09.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.2 +aiorussound==4.9.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -2219,7 +2219,7 @@ pynetio==0.1.9.1 pynina==0.3.6 # homeassistant.components.nintendo_parental_controls -pynintendoparental==1.1.1 +pynintendoparental==1.1.2 # homeassistant.components.nobo_hub pynobo==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67393aecea63cc..b4600989f1a6eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.1 # homeassistant.components.alexa_devices -aioamazondevices==6.4.3 +aioamazondevices==6.4.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station @@ -363,7 +363,7 @@ aioridwell==2025.09.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.8.2 +aiorussound==4.9.0 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -1855,7 +1855,7 @@ pynetgear==0.10.10 pynina==0.3.6 # homeassistant.components.nintendo_parental_controls -pynintendoparental==1.1.1 +pynintendoparental==1.1.2 # homeassistant.components.nobo_hub pynobo==1.8.1 diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 4842a91ce422aa..1c296732523536 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -9,7 +9,6 @@ ATTR_BATTERY, ATTR_DEV_ID, ATTR_GPS, - ATTR_GPS_ACCURACY, ATTR_HOST_NAME, ATTR_LOCATION_NAME, ATTR_MAC, @@ -19,6 +18,7 @@ ScannerEntity, SourceType, ) +from homeassistant.const import ATTR_GPS_ACCURACY from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import ConfigType, GPSType from homeassistant.loader import bind_hass diff --git a/tests/components/number/test_significant_change.py b/tests/components/number/test_significant_change.py index e0e02fc1d35e62..9fe54b449449dd 100644 --- a/tests/components/number/test_significant_change.py +++ b/tests/components/number/test_significant_change.py @@ -37,6 +37,14 @@ ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, } +TEMP_DELTA_CELSIUS_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE_DELTA, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, +} +TEMP_DELTA_FAHRENHEIT_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE_DELTA, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, +} VOLATILE_ORGANIC_COMPOUNDS_ATTRS = { ATTR_DEVICE_CLASS: NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS } @@ -81,6 +89,13 @@ ("70", "70.5", TEMP_FREEDOM_ATTRS, False), ("fail", "70", TEMP_FREEDOM_ATTRS, True), ("70", "fail", TEMP_FREEDOM_ATTRS, False), + ("1", "1", TEMP_DELTA_CELSIUS_ATTRS, False), + ("12", "13", TEMP_DELTA_CELSIUS_ATTRS, True), + ("12.1", "12.2", TEMP_DELTA_CELSIUS_ATTRS, False), + ("10", "11", TEMP_DELTA_FAHRENHEIT_ATTRS, True), + ("10", "10.5", TEMP_DELTA_FAHRENHEIT_ATTRS, False), + ("fail", "0", TEMP_DELTA_FAHRENHEIT_ATTRS, True), + ("10", "fail", TEMP_DELTA_FAHRENHEIT_ATTRS, False), ("0", "1", VOLATILE_ORGANIC_COMPOUNDS_ATTRS, True), ("0.1", "0.5", VOLATILE_ORGANIC_COMPOUNDS_ATTRS, False), ], diff --git a/tests/components/number/test_websocket_api.py b/tests/components/number/test_websocket_api.py index a405ef8c2fcd62..8fbc79f2c1fac3 100644 --- a/tests/components/number/test_websocket_api.py +++ b/tests/components/number/test_websocket_api.py @@ -26,6 +26,17 @@ async def test_device_class_units( assert msg["success"] assert msg["result"] == {"units": ["K", "°C", "°F"]} + # Check also TEMPERATURE_DELTA + await client.send_json_auto_id( + { + "type": "number/device_class_convertible_units", + "device_class": "temperature_delta", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": ["K", "°C", "°F"]} + # Device class with units which number doesn't allow customizing & converting await client.send_json_auto_id( { diff --git a/tests/components/roborock/snapshots/test_sensor.ambr b/tests/components/roborock/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..7d853caaa7310f --- /dev/null +++ b/tests/components/roborock/snapshots/test_sensor.ambr @@ -0,0 +1,882 @@ +# serializer version: 1 +# name: test_sensors + list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Main brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_main_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '279.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Side brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_side_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '179.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Filter time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_filter_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '129.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 MaxV Dock Maintenance brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_dock_maintenance_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '235', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 MaxV Dock Strainer time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_dock_strainer_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Sensor time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_sensor_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.33833333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Total cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.6616666666667', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Total cleaning count', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_total_cleaning_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Status', + 'options': list([ + 'unknown', + 'starting', + 'charger_disconnected', + 'idle', + 'remote_control_active', + 'cleaning', + 'returning_home', + 'manual_mode', + 'charging', + 'charging_problem', + 'paused', + 'spot_cleaning', + 'error', + 'shutting_down', + 'updating', + 'docking', + 'going_to_target', + 'zoned_cleaning', + 'segment_cleaning', + 'emptying_the_bin', + 'washing_the_mop', + 'washing_the_mop_2', + 'going_to_wash_the_mop', + 'in_call', + 'mapping', + 'egg_attack', + 'patrol', + 'attaching_the_mop', + 'detaching_the_mop', + 'charging_complete', + 'device_offline', + 'locked', + 'air_drying_stopping', + 'robot_status_mopping', + 'clean_mop_cleaning', + 'clean_mop_mopping', + 'segment_mopping', + 'segment_clean_mop_cleaning', + 'segment_clean_mop_mopping', + 'zoned_mopping', + 'zoned_clean_mop_cleaning', + 'zoned_clean_mop_mopping', + 'back_to_dock_washing_duster', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Cleaning area', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Total cleaning area', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_total_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1159.2', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Vacuum error', + 'options': list([ + 'none', + 'lidar_blocked', + 'bumper_stuck', + 'wheels_suspended', + 'cliff_sensor_error', + 'main_brush_jammed', + 'side_brush_jammed', + 'wheels_jammed', + 'robot_trapped', + 'no_dustbin', + 'strainer_error', + 'compass_error', + 'low_battery', + 'charging_error', + 'battery_error', + 'wall_sensor_dirty', + 'robot_tilted', + 'side_brush_error', + 'fan_error', + 'dock', + 'optical_flow_sensor_dirt', + 'vertical_bumper_pressed', + 'dock_locator_error', + 'return_to_dock_fail', + 'nogo_zone_detected', + 'visual_sensor', + 'light_touch', + 'vibrarise_jammed', + 'robot_on_carpet', + 'filter_blocked', + 'invisible_wall_detected', + 'cannot_cross_carpet', + 'internal_error', + 'collect_dust_error_3', + 'collect_dust_error_4', + 'mopping_roller_1', + 'mopping_roller_error_2', + 'clear_water_box_hoare', + 'dirty_water_box_hoare', + 'sink_strainer_hoare', + 'clear_water_box_exception', + 'clear_brush_exception', + 'clear_brush_exception_2', + 'filter_screen_exception', + 'mopping_roller_2', + 'up_water_exception', + 'drain_water_exception', + 'temperature_protection', + 'clean_carousel_exception', + 'clean_carousel_water_full', + 'water_carriage_drop', + 'check_clean_carouse', + 'audio_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_vacuum_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Roborock S7 2 Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Roborock S7 2 Last clean begin', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_last_clean_begin', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-01-01T03:22:10+00:00', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Roborock S7 2 Last clean end', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_last_clean_end', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-01-01T03:43:58+00:00', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 MaxV Dock Dock error', + 'options': list([ + 'ok', + 'duct_blockage', + 'water_empty', + 'waste_water_tank_full', + 'maintenance_brush_jammed', + 'dirty_tank_latch_open', + 'no_dustbin', + 'cleaning_tank_full_or_blocked', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_dock_dock_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Main brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_main_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '279.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Side brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_side_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '179.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Filter time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_filter_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '129.338333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Dock Maintenance brush time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_dock_maintenance_brush_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '235', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Dock Strainer time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_dock_strainer_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '85', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Sensor time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_sensor_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.33833333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '19.6', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Roborock S7 2 Total cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.6616666666667', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Total cleaning count', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_total_cleaning_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Status', + 'options': list([ + 'unknown', + 'starting', + 'charger_disconnected', + 'idle', + 'remote_control_active', + 'cleaning', + 'returning_home', + 'manual_mode', + 'charging', + 'charging_problem', + 'paused', + 'spot_cleaning', + 'error', + 'shutting_down', + 'updating', + 'docking', + 'going_to_target', + 'zoned_cleaning', + 'segment_cleaning', + 'emptying_the_bin', + 'washing_the_mop', + 'washing_the_mop_2', + 'going_to_wash_the_mop', + 'in_call', + 'mapping', + 'egg_attack', + 'patrol', + 'attaching_the_mop', + 'detaching_the_mop', + 'charging_complete', + 'device_offline', + 'locked', + 'air_drying_stopping', + 'robot_status_mopping', + 'clean_mop_cleaning', + 'clean_mop_mopping', + 'segment_mopping', + 'segment_clean_mop_cleaning', + 'segment_clean_mop_mopping', + 'zoned_mopping', + 'zoned_clean_mop_cleaning', + 'zoned_clean_mop_mopping', + 'back_to_dock_washing_duster', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Cleaning area', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Total cleaning area', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_total_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1159.2', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Vacuum error', + 'options': list([ + 'none', + 'lidar_blocked', + 'bumper_stuck', + 'wheels_suspended', + 'cliff_sensor_error', + 'main_brush_jammed', + 'side_brush_jammed', + 'wheels_jammed', + 'robot_trapped', + 'no_dustbin', + 'strainer_error', + 'compass_error', + 'low_battery', + 'charging_error', + 'battery_error', + 'wall_sensor_dirty', + 'robot_tilted', + 'side_brush_error', + 'fan_error', + 'dock', + 'optical_flow_sensor_dirt', + 'vertical_bumper_pressed', + 'dock_locator_error', + 'return_to_dock_fail', + 'nogo_zone_detected', + 'visual_sensor', + 'light_touch', + 'vibrarise_jammed', + 'robot_on_carpet', + 'filter_blocked', + 'invisible_wall_detected', + 'cannot_cross_carpet', + 'internal_error', + 'collect_dust_error_3', + 'collect_dust_error_4', + 'mopping_roller_1', + 'mopping_roller_error_2', + 'clear_water_box_hoare', + 'dirty_water_box_hoare', + 'sink_strainer_hoare', + 'clear_water_box_exception', + 'clear_brush_exception', + 'clear_brush_exception_2', + 'filter_screen_exception', + 'mopping_roller_2', + 'up_water_exception', + 'drain_water_exception', + 'temperature_protection', + 'clean_carousel_exception', + 'clean_carousel_water_full', + 'water_carriage_drop', + 'check_clean_carouse', + 'audio_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_vacuum_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Roborock S7 2 Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Roborock S7 2 Last clean begin', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_last_clean_begin', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-01-01T03:22:10+00:00', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Roborock S7 2 Last clean end', + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_last_clean_end', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-01-01T03:43:58+00:00', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Dock Dock error', + 'options': list([ + 'ok', + 'duct_blockage', + 'water_empty', + 'waste_water_tank_full', + 'maintenance_brush_jammed', + 'dirty_tank_latch_open', + 'no_dustbin', + 'cleaning_tank_full_or_blocked', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_dock_dock_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Current room', + 'options': list([ + 'Example room 1', + 'Example room 2', + 'Example room 3', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_maxv_current_room', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Example room 2', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Roborock S7 2 Current room', + 'options': list([ + 'Example room 1', + 'Example room 2', + 'Example room 3', + ]), + }), + 'context': , + 'entity_id': 'sensor.roborock_s7_2_current_room', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Example room 2', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dyad Pro Status', + 'options': list([ + 'unknown', + 'fetching', + 'fetch_failed', + 'updating', + 'washing', + 'ready', + 'charging', + 'mop_washing', + 'self_clean_cleaning', + 'self_clean_deep_cleaning', + 'self_clean_rinsing', + 'self_clean_dehydrating', + 'drying', + 'ventilating', + 'reserving', + 'mop_washing_paused', + 'dusting_mode', + ]), + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'drying', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Dyad Pro Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Dyad Pro Filter time left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_filter_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0308333333333333', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Dyad Pro Roller left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_roller_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0616666666666667', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dyad Pro Error', + 'options': list([ + 'none', + 'dirty_tank_full', + 'water_level_sensor_stuck', + 'clean_tank_empty', + 'clean_head_entangled', + 'clean_head_too_hot', + 'fan_protection_e5', + 'cleaning_head_blocked', + 'temperature_protection', + 'fan_protection_e4', + 'fan_protection_e9', + 'battery_temperature_protection_e0', + 'battery_temperature_protection', + 'battery_temperature_protection_2', + 'power_adapter_error', + 'dirty_charging_contacts', + 'low_battery', + 'battery_under_10', + ]), + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Dyad Pro Total cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dyad_pro_total_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.55', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Zeo One State', + 'options': list([ + 'standby', + 'weighing', + 'soaking', + 'washing', + 'rinsing', + 'spinning', + 'drying', + 'cooling', + 'under_delay_start', + 'done', + ]), + }), + 'context': , + 'entity_id': 'sensor.zeo_one_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'drying', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Zeo One Countdown', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zeo_one_countdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Zeo One Washing left', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zeo_one_washing_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '253', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Zeo One Error', + 'options': list([ + 'none', + 'refill_error', + 'drain_error', + 'door_lock_error', + 'water_level_error', + 'inverter_error', + 'heating_error', + 'temperature_error', + 'communication_error', + 'drying_error', + 'drying_error_e_12', + 'drying_error_e_13', + 'drying_error_e_14', + 'drying_error_e_15', + 'drying_error_e_16', + 'drying_error_water_flow', + 'drying_error_restart', + 'spin_error', + ]), + }), + 'context': , + 'entity_id': 'sensor.zeo_one_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }), + ]) +# --- diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 623fde93b1f251..847623e2ba7345 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -4,16 +4,9 @@ import pytest from roborock import DeviceData, HomeDataDevice -from roborock.const import ( - CLEANING_BRUSH_REPLACE_TIME, - FILTER_REPLACE_TIME, - MAIN_BRUSH_REPLACE_TIME, - SENSOR_DIRTY_REPLACE_TIME, - SIDE_BRUSH_REPLACE_TIME, - STRAINER_REPLACE_TIME, -) from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.version_1_apis import RoborockMqttClientV1 +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -29,63 +22,13 @@ def platforms() -> list[Platform]: return [Platform.SENSOR] -async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: +async def test_sensors( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 46 - assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( - MAIN_BRUSH_REPLACE_TIME - 74382 - ) - assert hass.states.get("sensor.roborock_s7_maxv_side_brush_time_left").state == str( - SIDE_BRUSH_REPLACE_TIME - 74382 - ) - assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( - FILTER_REPLACE_TIME - 74382 - ) - assert hass.states.get("sensor.roborock_s7_maxv_sensor_time_left").state == str( - SENSOR_DIRTY_REPLACE_TIME - 74382 - ) - assert hass.states.get( - "sensor.roborock_s7_2_dock_maintenance_brush_time_left" - ).state == str(CLEANING_BRUSH_REPLACE_TIME - 65) - assert hass.states.get("sensor.roborock_s7_2_dock_strainer_time_left").state == str( - STRAINER_REPLACE_TIME - 65 - ) - - assert hass.states.get("sensor.roborock_s7_maxv_cleaning_time").state == "1176" - assert ( - hass.states.get("sensor.roborock_s7_maxv_total_cleaning_time").state == "74382" - ) - assert hass.states.get("sensor.roborock_s7_maxv_status").state == "charging" - assert ( - hass.states.get("sensor.roborock_s7_maxv_total_cleaning_area").state == "1159.2" - ) - assert hass.states.get("sensor.roborock_s7_maxv_cleaning_area").state == "21.0" - assert hass.states.get("sensor.roborock_s7_maxv_vacuum_error").state == "none" - assert hass.states.get("sensor.roborock_s7_maxv_battery").state == "100" - assert hass.states.get("sensor.roborock_s7_maxv_dock_dock_error").state == "ok" - assert hass.states.get("sensor.roborock_s7_maxv_total_cleaning_count").state == "31" - assert ( - hass.states.get("sensor.roborock_s7_maxv_last_clean_begin").state - == "2023-01-01T03:22:10+00:00" - ) - assert ( - hass.states.get("sensor.roborock_s7_maxv_last_clean_end").state - == "2023-01-01T03:43:58+00:00" - ) - assert ( - hass.states.get("sensor.roborock_s7_maxv_current_room").state - == "Example room 2" - ) - assert hass.states.get("sensor.dyad_pro_status").state == "drying" - assert hass.states.get("sensor.dyad_pro_battery").state == "100" - assert hass.states.get("sensor.dyad_pro_filter_time_left").state == "111" - assert hass.states.get("sensor.dyad_pro_roller_left").state == "222" - assert hass.states.get("sensor.dyad_pro_error").state == "none" - assert hass.states.get("sensor.dyad_pro_total_cleaning_time").state == "213" - assert hass.states.get("sensor.zeo_one_state").state == "drying" - assert hass.states.get("sensor.zeo_one_countdown").state == "0" - assert hass.states.get("sensor.zeo_one_washing_left").state == "253" - assert hass.states.get("sensor.zeo_one_error").state == "none" + assert snapshot == hass.states.async_all("sensor") async def test_listener_update( @@ -109,8 +52,9 @@ async def test_listener_update( ] ) # Test consumable - assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( - FILTER_REPLACE_TIME - 74382 + assert ( + hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state + == "129.338333333333" ) with patch("roborock.version_1_apis.AttributeCache.value", CONSUMABLE.as_dict()): client.on_message_received( @@ -122,6 +66,7 @@ async def test_listener_update( ] ) await hass.async_block_till_done() - assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( - FILTER_REPLACE_TIME - 743 + assert ( + hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state + == "149.793611111111" ) diff --git a/tests/components/sensor/common.py b/tests/components/sensor/common.py index ea5f6db0bf6167..4dedababad1ce9 100644 --- a/tests/components/sensor/common.py +++ b/tests/components/sensor/common.py @@ -93,6 +93,7 @@ SensorDeviceClass.SPEED: UnitOfSpeed.METERS_PER_SECOND, SensorDeviceClass.SULPHUR_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SensorDeviceClass.TEMPERATURE: UnitOfTemperature.CELSIUS, + SensorDeviceClass.TEMPERATURE_DELTA: UnitOfTemperature.CELSIUS, SensorDeviceClass.TIMESTAMP: None, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: CONCENTRATION_PARTS_PER_MILLION, diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index a0e97ac9e0d22d..e33dd0ffb520bf 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -125,7 +125,7 @@ async def test_get_conditions( conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id ) - assert len(conditions) == 56 + assert len(conditions) == 57 assert conditions == unordered(expected_conditions) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1034f3473db583..43476a6a43b223 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -126,7 +126,7 @@ async def test_get_triggers( triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) - assert len(triggers) == 56 + assert len(triggers) == 57 assert triggers == unordered(expected_triggers) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 4b0ac44c14f85d..90f564464d2a4a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -791,6 +791,26 @@ async def test_unit_translation_key_without_platform_raises( "37.8", 1, ), + ( + SensorDeviceClass.TEMPERATURE_DELTA, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + 2, + 3.6, + "3.6", + 1, + ), + ( + SensorDeviceClass.TEMPERATURE_DELTA, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, + 1, + pytest.approx(0.555556), + "0.6", + 1, + ), ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, UnitOfPressure.INHG, @@ -1164,6 +1184,15 @@ async def test_custom_unit( 100, SensorDeviceClass.WEIGHT, ), + ( + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + 10, + 10, + 18, + SensorDeviceClass.TEMPERATURE_DELTA, + ), ], ) async def test_custom_unit_change( @@ -1814,6 +1843,7 @@ async def test_unit_conversion_priority_suggested_unit_change_2( (SensorDeviceClass.SOUND_PRESSURE, UnitOfSoundPressure.DECIBEL, 0), (SensorDeviceClass.SPEED, UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), (SensorDeviceClass.TEMPERATURE, UnitOfTemperature.KELVIN, 1), + (SensorDeviceClass.TEMPERATURE_DELTA, UnitOfTemperature.KELVIN, 1), (SensorDeviceClass.VOLTAGE, UnitOfElectricPotential.VOLT, 0), (SensorDeviceClass.VOLUME, UnitOfVolume.MILLILITERS, 0), (SensorDeviceClass.VOLUME_FLOW_RATE, UnitOfVolumeFlowRate.LITERS_PER_SECOND, 0), @@ -2229,6 +2259,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement( SensorDeviceClass.SPEED, SensorDeviceClass.SULPHUR_DIOXIDE, SensorDeviceClass.TEMPERATURE, + SensorDeviceClass.TEMPERATURE_DELTA, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, SensorDeviceClass.VOLTAGE, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index cfb74b563a844b..b2cedfbe4b7ae7 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -273,6 +273,26 @@ async def assert_validation_result( ("speed", "mph", "mph", "mph", "speed", 13.050847, -10, 30), ("temperature", "°C", "°C", "°C", "temperature", 13.050847, -10, 30), ("temperature", "°F", "°F", "°F", "temperature", 13.050847, -10, 30), + ( + "temperature_delta", + "°C", + "°C", + "°C", + "temperature_delta", + 13.050847, + -10, + 30, + ), + ( + "temperature_delta", + "°F", + "°F", + "°F", + "temperature_delta", + 13.050847, + -10, + 30, + ), ("volume", "m³", "m³", "m³", "volume", 13.050847, -10, 30), ("volume", "ft³", "ft³", "ft³", "volume", 13.050847, -10, 30), ("weight", "g", "g", "g", "mass", 13.050847, -10, 30), @@ -3138,6 +3158,24 @@ async def test_compile_hourly_statistics_fails( "temperature", StatisticMeanType.ARITHMETIC, ), + ( + "measurement", + "temperature_delta", + "°C", + "°C", + "°C", + "temperature_delta", + StatisticMeanType.ARITHMETIC, + ), + ( + "measurement", + "temperature_delta", + "°F", + "°F", + "°F", + "temperature_delta", + StatisticMeanType.ARITHMETIC, + ), ( "measurement", "volume", diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index e1a2325cd11a4d..e81fa98362fed3 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -31,6 +31,16 @@ ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, } +TEMP_DELTA_CELSIUS_ATTRS = { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE_DELTA, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, +} + +TEMP_DELTA_FAHRENHEIT_ATTRS = { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE_DELTA, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, +} + @pytest.mark.parametrize( ("old_state", "new_state", "attrs", "result"), @@ -54,6 +64,11 @@ ("70", "70.5", TEMP_FREEDOM_ATTRS, False), ("fail", "70", TEMP_FREEDOM_ATTRS, True), ("70", "fail", TEMP_FREEDOM_ATTRS, False), + ("12", "12", TEMP_DELTA_CELSIUS_ATTRS, False), + ("12", "13", TEMP_DELTA_CELSIUS_ATTRS, True), + ("12.1", "12.2", TEMP_DELTA_CELSIUS_ATTRS, False), + ("7", "8", TEMP_DELTA_FAHRENHEIT_ATTRS, True), + ("7", "7.5", TEMP_DELTA_FAHRENHEIT_ATTRS, False), ], ) async def test_significant_change_temperature( diff --git a/tests/helpers/snapshots/test_selector.ambr b/tests/helpers/snapshots/test_selector.ambr new file mode 100644 index 00000000000000..e3af84e6621118 --- /dev/null +++ b/tests/helpers/snapshots/test_selector.ambr @@ -0,0 +1,118 @@ +# serializer version: 1 +# name: test_nested_object_selectors + dict({ + 'selector': dict({ + 'object': dict({ + 'description_field': 'percentage', + 'fields': dict({ + 'name': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + 'object': dict({ + 'selector': dict({ + 'selector': dict({ + 'object': dict({ + 'description_field': 'other_name', + 'fields': dict({ + 'new_object': dict({ + 'required': True, + 'selector': dict({ + 'selector': dict({ + 'object': dict({ + 'description_field': 'description', + 'fields': dict({ + 'description': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + 'title': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + }), + 'label_field': 'title', + 'multiple': False, + }), + }), + }), + }), + 'no_name': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + 'other_name': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + }), + 'label_field': 'no_name', + 'multiple': False, + }), + }), + }), + }), + }), + 'label_field': 'name', + 'multiple': True, + }), + }), + }) +# --- +# name: test_object_selector_uses_selectors + dict({ + 'selector': dict({ + 'object': dict({ + 'description_field': 'percentage', + 'fields': dict({ + 'name': dict({ + 'required': True, + 'selector': dict({ + 'text': dict({ + 'multiline': False, + 'multiple': False, + }), + }), + }), + 'percentage': dict({ + 'selector': dict({ + 'number': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': 'slider', + 'step': 1.0, + }), + }), + }), + }), + 'label_field': 'name', + 'multiple': True, + }), + }), + }) +# --- diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 2a0df008e9b501..73db8af126eedc 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -6,6 +6,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant.helpers import selector @@ -722,6 +723,114 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> _test_selector("object", schema, valid_selections, invalid_selections) +def test_object_selector_uses_selectors(snapshot: SnapshotAssertion) -> None: + """Test ObjectSelector custom serializer for using Selector in ObjectSelectorField.""" + + selector_type = "object" + schema = { + "fields": { + "name": { + "required": True, + "selector": selector.TextSelector(), + }, + "percentage": { + "selector": selector.NumberSelector( + selector.NumberSelectorConfig(min=0, max=100) + ), + }, + }, + "multiple": True, + "label_field": "name", + "description_field": "percentage", + } + + # Validate selector configuration + config = {selector_type: schema} + selector.validate_selector(config) + selector_instance = selector.selector(config) + + # Serialize selector + selector_instance = selector.selector({selector_type: schema}) + assert selector_instance.serialize() != { + "selector": {selector_type: selector_instance.config} + } + assert selector_instance.serialize() == snapshot() + + # Test serialized selector can be dumped to YAML + yaml_util.dump(selector_instance.serialize()) + + +def test_nested_object_selectors(snapshot: SnapshotAssertion) -> None: + """Test ObjectSelector custom serializer with nested ObjectSelectors.""" + + selector_type = "object" + schema = { + "fields": { + "name": { + "required": True, + "selector": selector.TextSelector(), + }, + "object": { + "selector": selector.ObjectSelector( + selector.ObjectSelectorConfig( + fields={ + "no_name": { + "required": True, + "selector": selector.TextSelector(), + }, + "other_name": { + "required": True, + "selector": selector.TextSelector(), + }, + "new_object": { + "required": True, + "selector": selector.ObjectSelector( + selector.ObjectSelectorConfig( + fields={ + "title": { + "required": True, + "selector": selector.TextSelector(), + }, + "description": { + "required": True, + "selector": selector.TextSelector(), + }, + }, + multiple=False, + label_field="title", + description_field="description", + ) + ), + }, + }, + multiple=False, + label_field="no_name", + description_field="other_name", + ) + ), + }, + }, + "multiple": True, + "label_field": "name", + "description_field": "percentage", + } + + # Validate selector configuration + config = {selector_type: schema} + selector.validate_selector(config) + selector_instance = selector.selector(config) + + # Serialize selector + selector_instance = selector.selector({selector_type: schema}) + assert selector_instance.serialize() != { + "selector": {selector_type: selector_instance.config} + } + assert selector_instance.serialize() == snapshot() + + # Test serialized selector can be dumped to YAML + yaml_util.dump(selector_instance.serialize()) + + @pytest.mark.parametrize( ("schema", "raises"), [ @@ -759,7 +868,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> "label_field": "name", "description_field": "percentage", }, - pytest.raises(vol.Invalid), + does_not_raise(), ), ( { diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 26a6b1805201b4..6d119b137e2894 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -62,6 +62,7 @@ ReactivePowerConverter, SpeedConverter, TemperatureConverter, + TemperatureDeltaConverter, UnitlessRatioConverter, VolumeConverter, VolumeFlowRateConverter, @@ -96,6 +97,7 @@ ReactivePowerConverter, SpeedConverter, TemperatureConverter, + TemperatureDeltaConverter, UnitlessRatioConverter, EnergyDistanceConverter, VolumeConverter, @@ -178,6 +180,11 @@ UnitOfTemperature.FAHRENHEIT, 0.555556, ), + TemperatureDeltaConverter: ( + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + 0.555556, + ), UnitlessRatioConverter: (PERCENTAGE, None, 100), VolumeConverter: (UnitOfVolume.GALLONS, UnitOfVolume.LITERS, 0.264172), VolumeFlowRateConverter: ( @@ -847,6 +854,34 @@ (100, UnitOfTemperature.KELVIN, -173.15, UnitOfTemperature.CELSIUS), (100, UnitOfTemperature.KELVIN, -279.6699, UnitOfTemperature.FAHRENHEIT), ], + TemperatureDeltaConverter: [ + ( + 100, + UnitOfTemperature.CELSIUS, + 180, + UnitOfTemperature.FAHRENHEIT, + ), + (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), + ( + 100, + UnitOfTemperature.FAHRENHEIT, + 55.5556, + UnitOfTemperature.CELSIUS, + ), + ( + 100, + UnitOfTemperature.FAHRENHEIT, + 55.5556, + UnitOfTemperature.KELVIN, + ), + (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), + ( + 100, + UnitOfTemperature.KELVIN, + 180, + UnitOfTemperature.FAHRENHEIT, + ), + ], UnitlessRatioConverter: [ (5, None, 500, PERCENTAGE), (5, None, 5000000000, CONCENTRATION_PARTS_PER_BILLION), @@ -1211,6 +1246,18 @@ def test_unit_conversion_factory_allow_none_with_none() -> None: )(None) is None ) + assert ( + TemperatureDeltaConverter.converter_factory_allow_none( + UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS + )(1) + == 1 + ) + assert ( + TemperatureDeltaConverter.converter_factory_allow_none( + UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS + )(None) + is None + ) @pytest.mark.parametrize( @@ -1260,3 +1307,42 @@ def test_temperature_convert_with_interval( """Test conversion to other units.""" expected = pytest.approx(expected) assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected + + +@pytest.mark.parametrize( + ("value", "from_unit", "expected", "to_unit"), + [ + ( + 100, + UnitOfTemperature.CELSIUS, + 180, + UnitOfTemperature.FAHRENHEIT, + ), + (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), + ( + 100, + UnitOfTemperature.FAHRENHEIT, + 55.5556, + UnitOfTemperature.CELSIUS, + ), + ( + 100, + UnitOfTemperature.FAHRENHEIT, + 55.5556, + UnitOfTemperature.KELVIN, + ), + (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), + ( + 100, + UnitOfTemperature.KELVIN, + 180, + UnitOfTemperature.FAHRENHEIT, + ), + ], +) +def test_temperature_delta_convert( + value: float, from_unit: str, expected: float, to_unit: str +) -> None: + """Test conversion to other units.""" + expected = pytest.approx(expected) + assert TemperatureDeltaConverter.convert(value, from_unit, to_unit) == expected