Compare commits

..

5 Commits

Author SHA1 Message Date
Paulus Schoutsen
362769d64d Use new app panel instead of ingress page 2026-01-19 23:00:29 -05:00
Thomas55555
48852bab7a Add ppb as a valid UOM for sensor/number SO2 device class (#159431) 2026-01-19 23:32:45 +00:00
Andres Ruiz
7d370f4513 Bump waterfurnace to 1.4.0 (#161244) 2026-01-19 20:33:29 +01:00
Aleksandr Oleinikov
9d97791faf Change default model for Ollama to qwen3:4b-instruct (#161202)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-19 12:56:18 -05:00
Norbert Rittel
4fe8982b68 Clarify description of lawn_mower.docked trigger (#161238) 2026-01-19 17:48:40 +00:00
21 changed files with 156 additions and 158 deletions

View File

@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
if self._entry.source == SOURCE_HASSIO:
config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
config_url = "homeassistant://app/a0d7b954_adguard"
elif self.adguard.tls:
config_url = f"https://{self.adguard.host}:{self.adguard.port}"
else:

View File

@@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://hassio/ingress/core_deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://app/core_deconz"
CONF_BRIDGE_ID = "bridgeid"
CONF_GROUP_ID_BASE = "group_id_base"

View File

@@ -1034,7 +1034,7 @@ def _async_setup_device_registry(
and dashboard.data
and dashboard.data.get(device_info.name)
):
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
configuration_url = f"homeassistant://app/{dashboard.addon_slug}"
manufacturer = "espressif"
if device_info.manufacturer:

View File

@@ -41,7 +41,7 @@
"title": "Lawn mower",
"triggers": {
"docked": {
"description": "Triggers after one or more lawn mowers return to dock.",
"description": "Triggers after one or more lawn mowers have returned to dock.",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",

View File

@@ -50,7 +50,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Check connection to the Mealie API."""
assert self.host is not None
if "/hassio/ingress/" in self.host:
if "/app/" in self.host:
return {"base": "ingress_url"}, None
client = MealieClient(

View File

@@ -373,7 +373,7 @@ class NumberDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `μg/m³`
Unit of measurement: `ppb` (parts per billion), `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -545,7 +545,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
},
NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature),
NumberDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

View File

@@ -158,7 +158,7 @@ MODEL_NAMES = [ # https://ollama.com/library
"yi",
"zephyr",
]
DEFAULT_MODEL = "qwen3:4b"
DEFAULT_MODEL = "qwen3:4b-instruct"
DEFAULT_CONVERSATION_NAME = "Ollama Conversation"
DEFAULT_AI_TASK_NAME = "Ollama AI Task"

View File

@@ -64,6 +64,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -225,6 +226,7 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
_SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
CarbonMonoxideConcentrationConverter,
TemperatureDeltaConverter,
SulphurDioxideConcentrationConverter,
]
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {

View File

@@ -38,6 +38,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -94,6 +95,9 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("sulphur_dioxide"): vol.In(
SulphurDioxideConcentrationConverter.VALID_UNITS
),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("temperature_delta"): vol.In(
TemperatureDeltaConverter.VALID_UNITS

View File

@@ -68,6 +68,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -409,7 +410,7 @@ class SensorDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `μg/m³`
Unit of measurement: `ppb` (parts per billion), `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -569,6 +570,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.PRESSURE: PressureConverter,
SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter,
SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter,
SensorDeviceClass.SULPHUR_DIOXIDE: SulphurDioxideConcentrationConverter,
SensorDeviceClass.SPEED: SpeedConverter,
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
SensorDeviceClass.TEMPERATURE_DELTA: TemperatureDeltaConverter,
@@ -657,7 +659,10 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
},
SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature),
SensorDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "legacy",
"requirements": ["waterfurnace==1.2.0"]
"requirements": ["waterfurnace==1.4.0"]
}

View File

@@ -103,6 +103,7 @@ _AMBIENT_IDEAL_GAS_MOLAR_VOLUME = ( # m3⋅mol⁻¹
)
# Molar masses in g⋅mol⁻¹
_CARBON_MONOXIDE_MOLAR_MASS = 28.01
_SULPHUR_DIOXIDE_MOLAR_MASS = 64.066
class BaseUnitConverter:
@@ -208,6 +209,22 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
}
class SulphurDioxideConcentrationConverter(BaseUnitConverter):
"""Convert sulphur dioxide ratio to mass per volume."""
UNIT_CLASS = "sulphur_dioxide"
_UNIT_CONVERSION: dict[str | None, float] = {
CONCENTRATION_PARTS_PER_BILLION: 1e9,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
_SULPHUR_DIOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
),
}
VALID_UNITS = {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
class DataRateConverter(BaseUnitConverter):
"""Utility to convert data rate values."""

2
requirements_all.txt generated
View File

@@ -3170,7 +3170,7 @@ wallbox==0.9.0
watchdog==6.0.0
# homeassistant.components.waterfurnace
waterfurnace==1.2.0
waterfurnace==1.4.0
# homeassistant.components.watergate
watergate-local-api==2025.1.0

View File

@@ -100,13 +100,6 @@ async def target_entities(
suggested_object_id=f"device_{domain}",
device_id=device.id,
)
entity_reg.async_get_or_create(
domain=domain,
platform="test",
unique_id=f"{domain}_device2",
suggested_object_id=f"device2_{domain}",
device_id=device.id,
)
entity_reg.async_get_or_create(
domain=domain,
platform="test",
@@ -137,11 +130,9 @@ async def target_entities(
return {
"included": [
f"{domain}.standalone_{domain}",
f"{domain}.standalone2_{domain}",
f"{domain}.label_{domain}",
f"{domain}.area_{domain}",
f"{domain}.device_{domain}",
f"{domain}.device2_{domain}",
],
"excluded": [
f"{domain}.standalone_{domain}_excluded",
@@ -159,22 +150,17 @@ def parametrize_target_entities(domain: str) -> list[tuple[dict, str, int]]:
"""
return [
(
{
CONF_ENTITY_ID: [
f"{domain}.standalone_{domain}",
f"{domain}.standalone2_{domain}",
]
},
{CONF_ENTITY_ID: f"{domain}.standalone_{domain}"},
f"{domain}.standalone_{domain}",
2,
1,
),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 3),
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 3),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 3),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 3),
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 3),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 3),
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 2),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 2),
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 2),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 2),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 2),
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 2),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 2),
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 1),
]
@@ -198,19 +184,18 @@ class ConditionStateDescription(TypedDict):
included: _StateDescription # State for entities meant to be targeted
excluded: _StateDescription # State for entities not meant to be targeted
state_valid: bool # False if the state of the included entities is missing (None), unavailable or unknown
condition_true: bool # If the condition is expected to evaluate to true
condition_true_first_entity: bool # If the condition is expected to evaluate to true for the first targeted entity
def _parametrize_condition_states(
def parametrize_condition_states(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None,
condition_true_if_invalid: bool,
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
@@ -227,7 +212,7 @@ def _parametrize_condition_states(
def state_with_attributes(
state: str | None | tuple[str | None, dict],
condition_true: bool,
condition_true_first_entity: bool,
state_valid: bool,
) -> ConditionStateDescription:
"""Return ConditionStateDescription dict."""
if isinstance(state, str) or state is None:
@@ -241,7 +226,7 @@ def _parametrize_condition_states(
"attributes": {},
},
"condition_true": condition_true,
"condition_true_first_entity": condition_true_first_entity,
"state_valid": state_valid,
}
return {
"included": {
@@ -253,7 +238,7 @@ def _parametrize_condition_states(
"attributes": state[1],
},
"condition_true": condition_true,
"condition_true_first_entity": condition_true_first_entity,
"state_valid": state_valid,
}
return [
@@ -262,19 +247,11 @@ def _parametrize_condition_states(
condition_options,
list(
itertools.chain(
(state_with_attributes(None, condition_true_if_invalid, True),),
(state_with_attributes(None, False, False),),
(state_with_attributes(STATE_UNAVAILABLE, False, False),),
(state_with_attributes(STATE_UNKNOWN, False, False),),
(
state_with_attributes(
STATE_UNAVAILABLE, condition_true_if_invalid, True
),
),
(
state_with_attributes(
STATE_UNKNOWN, condition_true_if_invalid, True
),
),
(
state_with_attributes(other_state, False, False)
state_with_attributes(other_state, False, True)
for other_state in other_states
),
),
@@ -286,8 +263,8 @@ def _parametrize_condition_states(
condition,
condition_options,
[
state_with_attributes(other_states[0], False, False),
state_with_attributes(target_state, True, False),
state_with_attributes(other_states[0], False, True),
state_with_attributes(target_state, True, True),
],
)
for target_state in target_states
@@ -295,60 +272,6 @@ def _parametrize_condition_states(
]
def parametrize_condition_states_any(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
The target_states and other_states iterables are either iterables of
states or iterables of (state, attributes) tuples.
Returns a list of tuples with (condition, condition options, list of states),
where states is a list of ConditionStateDescription dicts.
"""
return _parametrize_condition_states(
condition=condition,
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
additional_attributes=additional_attributes,
condition_true_if_invalid=False,
)
def parametrize_condition_states_all(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
The target_states and other_states iterables are either iterables of
states or iterables of (state, attributes) tuples.
Returns a list of tuples with (condition, condition options, list of states),
where states is a list of ConditionStateDescription dicts.
"""
return _parametrize_condition_states(
condition=condition,
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
additional_attributes=additional_attributes,
condition_true_if_invalid=True,
)
def parametrize_trigger_states(
*,
trigger: str,

View File

@@ -16,8 +16,7 @@ from tests.components import (
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_condition_states,
parametrize_target_entities,
set_or_remove_state,
target_entities,
@@ -62,7 +61,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed",
target_states=[
AlarmControlPanelState.ARMED_AWAY,
@@ -79,7 +78,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
AlarmControlPanelState.TRIGGERED,
],
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
@@ -87,7 +86,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
@@ -95,7 +94,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
@@ -103,7 +102,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
@@ -111,12 +110,12 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_disarmed",
target_states=[AlarmControlPanelState.DISARMED],
other_states=other_states(AlarmControlPanelState.DISARMED),
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="alarm_control_panel.is_triggered",
target_states=[AlarmControlPanelState.TRIGGERED],
other_states=other_states(AlarmControlPanelState.TRIGGERED),
@@ -169,7 +168,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed",
target_states=[
AlarmControlPanelState.ARMED_AWAY,
@@ -186,7 +185,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
AlarmControlPanelState.TRIGGERED,
],
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
@@ -194,7 +193,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
@@ -202,7 +201,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
@@ -210,7 +209,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
@@ -218,12 +217,12 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_disarmed",
target_states=[AlarmControlPanelState.DISARMED],
other_states=other_states(AlarmControlPanelState.DISARMED),
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="alarm_control_panel.is_triggered",
target_states=[AlarmControlPanelState.TRIGGERED],
other_states=other_states(AlarmControlPanelState.TRIGGERED),
@@ -260,10 +259,17 @@ async def test_alarm_control_panel_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)

View File

@@ -12,8 +12,7 @@ from tests.components import (
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_condition_states,
parametrize_target_entities,
set_or_remove_state,
target_entities,
@@ -55,22 +54,22 @@ async def test_assist_satellite_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="assist_satellite.is_idle",
target_states=[AssistSatelliteState.IDLE],
other_states=other_states(AssistSatelliteState.IDLE),
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="assist_satellite.is_listening",
target_states=[AssistSatelliteState.LISTENING],
other_states=other_states(AssistSatelliteState.LISTENING),
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="assist_satellite.is_processing",
target_states=[AssistSatelliteState.PROCESSING],
other_states=other_states(AssistSatelliteState.PROCESSING),
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="assist_satellite.is_responding",
target_states=[AssistSatelliteState.RESPONDING],
other_states=other_states(AssistSatelliteState.RESPONDING),
@@ -123,22 +122,22 @@ async def test_assist_satellite_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="assist_satellite.is_idle",
target_states=[AssistSatelliteState.IDLE],
other_states=other_states(AssistSatelliteState.IDLE),
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="assist_satellite.is_listening",
target_states=[AssistSatelliteState.LISTENING],
other_states=other_states(AssistSatelliteState.LISTENING),
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="assist_satellite.is_processing",
target_states=[AssistSatelliteState.PROCESSING],
other_states=other_states(AssistSatelliteState.PROCESSING),
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="assist_satellite.is_responding",
target_states=[AssistSatelliteState.RESPONDING],
other_states=other_states(AssistSatelliteState.RESPONDING),
@@ -175,10 +174,17 @@ async def test_assist_satellite_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)

View File

@@ -11,8 +11,7 @@ from tests.components import (
ConditionStateDescription,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_condition_states,
parametrize_target_entities,
set_or_remove_state,
target_entities,
@@ -58,12 +57,12 @@ async def test_fan_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="fan.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="fan.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -124,12 +123,12 @@ async def test_fan_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="fan.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="fan.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -170,10 +169,17 @@ async def test_fan_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)

View File

@@ -11,8 +11,7 @@ from tests.components import (
ConditionStateDescription,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_condition_states,
parametrize_target_entities,
set_or_remove_state,
target_entities,
@@ -58,12 +57,12 @@ async def test_light_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="light.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states_any(
*parametrize_condition_states(
condition="light.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -124,12 +123,12 @@ async def test_light_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="light.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states_all(
*parametrize_condition_states(
condition="light.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -170,10 +169,17 @@ async def test_light_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true_first_entity"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert condition(hass) == state["condition_true"]
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)

View File

@@ -103,7 +103,7 @@ async def test_ingress_host(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "http://homeassistant/hassio/ingress/db21ed7f_mealie",
CONF_HOST: "http://homeassistant/app/db21ed7f_mealie",
CONF_API_TOKEN: "token",
},
)

View File

@@ -3118,7 +3118,6 @@ def test_device_class_converters_are_complete() -> None:
SensorDeviceClass.PM4,
SensorDeviceClass.SIGNAL_STRENGTH,
SensorDeviceClass.SOUND_PRESSURE,
SensorDeviceClass.SULPHUR_DIOXIDE,
SensorDeviceClass.TIMESTAMP,
SensorDeviceClass.WIND_DIRECTION,
}

View File

@@ -61,6 +61,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -102,6 +103,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
EnergyDistanceConverter,
VolumeConverter,
VolumeFlowRateConverter,
SulphurDioxideConcentrationConverter,
)
}
@@ -175,6 +177,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
UnitOfSpeed.MILES_PER_HOUR,
1.609343,
),
SulphurDioxideConcentrationConverter: (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
2.6633,
),
TemperatureConverter: (
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
@@ -846,6 +853,20 @@ _CONVERTED_VALUE: dict[
# float(round(((20.7 m/s / 0.836) ** 2) ** (1 / 3))) = 8.0Bft
(20.7, UnitOfSpeed.METERS_PER_SECOND, 8.0, UnitOfSpeed.BEAUFORT),
],
SulphurDioxideConcentrationConverter: [
(
1,
CONCENTRATION_PARTS_PER_BILLION,
2.6633,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
(
120,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
45.056879,
CONCENTRATION_PARTS_PER_BILLION,
),
],
TemperatureConverter: [
(100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT),
(100, UnitOfTemperature.CELSIUS, 373.15, UnitOfTemperature.KELVIN),