mirror of
https://github.com/home-assistant/core.git
synced 2026-05-30 20:53:11 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0547ac7ad3 | |||
| 8a0602cc4c | |||
| cf9518cb33 | |||
| 4183c55cc5 | |||
| 2e50868c4c | |||
| cc07ac58ab | |||
| 81004196a5 |
@@ -380,7 +380,7 @@ jobs:
|
||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
with:
|
||||
images: ${{ matrix.registry }}/home-assistant
|
||||
sep-tags: ","
|
||||
@@ -394,7 +394,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.7.1
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
|
||||
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import avea
|
||||
from bleak.exc import BleakError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
@@ -67,15 +66,6 @@ def _is_avea_discovery(discovery_info: BluetoothServiceInfoBleak) -> bool:
|
||||
return AVEA_SERVICE_UUID in discovery_info.service_uuids
|
||||
|
||||
|
||||
def _discovery_label(discovery_info: BluetoothServiceInfoBleak) -> str:
|
||||
"""Return a label for a discovered Avea bulb."""
|
||||
if (
|
||||
name := _normalize_name(discovery_info.name)
|
||||
) and name != discovery_info.address:
|
||||
return f"{name} ({discovery_info.address})"
|
||||
return discovery_info.address
|
||||
|
||||
|
||||
class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Avea."""
|
||||
|
||||
@@ -160,7 +150,6 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if discovery := self._discovery_info:
|
||||
self._discovered_devices[discovery.address] = discovery
|
||||
else:
|
||||
await bluetooth.async_request_active_scan(self.hass)
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
for discovery in async_discovered_service_info(self.hass):
|
||||
if (
|
||||
@@ -176,10 +165,11 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if self._discovery_info:
|
||||
disc = self._discovery_info
|
||||
label = f"{disc.name or disc.address} ({disc.address})"
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS, default=disc.address): vol.In(
|
||||
{disc.address: _discovery_label(disc)}
|
||||
{disc.address: label}
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -188,7 +178,10 @@ class AveaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
service_info.address: _discovery_label(service_info)
|
||||
service_info.address: (
|
||||
f"{service_info.name or service_info.address}"
|
||||
f" ({service_info.address})"
|
||||
)
|
||||
for service_info in self._discovered_devices.values()
|
||||
}
|
||||
),
|
||||
|
||||
@@ -27,7 +27,6 @@ from bluetooth_data_tools import monotonic_time_coarse as MONOTONIC_TIME
|
||||
from habluetooth import (
|
||||
BaseHaRemoteScanner,
|
||||
BaseHaScanner,
|
||||
BluetoothReachabilityIntent,
|
||||
BluetoothScannerDevice,
|
||||
BluetoothScanningMode,
|
||||
HaBluetoothConnector,
|
||||
@@ -56,7 +55,6 @@ from . import passive_update_processor, websocket_api
|
||||
from .api import (
|
||||
_get_manager,
|
||||
async_address_present,
|
||||
async_address_reachability_diagnostics,
|
||||
async_ble_device_from_address,
|
||||
async_clear_address_from_match_history,
|
||||
async_clear_advertisement_history,
|
||||
@@ -110,14 +108,12 @@ __all__ = [
|
||||
"BluetoothCallback",
|
||||
"BluetoothCallbackMatcher",
|
||||
"BluetoothChange",
|
||||
"BluetoothReachabilityIntent",
|
||||
"BluetoothScannerDevice",
|
||||
"BluetoothScanningMode",
|
||||
"BluetoothServiceInfo",
|
||||
"BluetoothServiceInfoBleak",
|
||||
"HaBluetoothConnector",
|
||||
"async_address_present",
|
||||
"async_address_reachability_diagnostics",
|
||||
"async_ble_device_from_address",
|
||||
"async_clear_address_from_match_history",
|
||||
"async_clear_advertisement_history",
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, cast
|
||||
from bleak import BleakScanner
|
||||
from habluetooth import (
|
||||
BaseHaScanner,
|
||||
BluetoothReachabilityIntent,
|
||||
BluetoothScannerDevice,
|
||||
BluetoothScanningMode,
|
||||
HaBleakScannerWrapper,
|
||||
@@ -109,14 +108,6 @@ def async_ble_device_from_address(
|
||||
return _get_manager(hass).async_ble_device_from_address(address, connectable)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_address_reachability_diagnostics(
|
||||
hass: HomeAssistant, address: str, intent: BluetoothReachabilityIntent
|
||||
) -> str:
|
||||
"""Return a human readable explanation of why an address may be unreachable."""
|
||||
return _get_manager(hass).async_address_reachability_diagnostics(address, intent)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_scanner_devices_by_address(
|
||||
hass: HomeAssistant, address: str, connectable: bool = True
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.16",
|
||||
"habluetooth==6.8.0"
|
||||
"habluetooth==6.7.9"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -284,19 +284,6 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||
UpdateDeviceClass, static_info.device_class
|
||||
)
|
||||
|
||||
def version_is_newer(self, latest_version: str, installed_version: str) -> bool:
|
||||
"""Return True if latest_version is newer than installed_version.
|
||||
|
||||
ESPHome project versions can carry a build suffix (e.g.
|
||||
2025.11.5_c51f7548) that AwesomeVersion cannot parse. Without stripping
|
||||
it the base comparison raises and the entity is forced on for every
|
||||
build mismatch. Drop the suffix so the versions compare cleanly and we
|
||||
only report genuinely newer firmware.
|
||||
"""
|
||||
return super().version_is_newer(
|
||||
latest_version.partition("_")[0], installed_version.partition("_")[0]
|
||||
)
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def installed_version(self) -> str:
|
||||
|
||||
@@ -4,7 +4,7 @@ from logging import getLogger
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.assets.models import ImmichAsset
|
||||
from aioimmich.exceptions import ImmichError, ImmichForbiddenError
|
||||
from aioimmich.exceptions import ImmichError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
@@ -79,7 +79,7 @@ class ImmichMediaSource(MediaSource):
|
||||
],
|
||||
)
|
||||
|
||||
async def _async_build_immich( # noqa: C901
|
||||
async def _async_build_immich(
|
||||
self, item: MediaSourceItem, entries: list[ConfigEntry]
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Handle browsing different immich instances."""
|
||||
@@ -137,12 +137,6 @@ class ImmichMediaSource(MediaSource):
|
||||
LOGGER.debug("Render all albums for %s", entry.title)
|
||||
try:
|
||||
albums = await immich_api.albums.async_get_all_albums()
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
@@ -164,12 +158,6 @@ class ImmichMediaSource(MediaSource):
|
||||
LOGGER.debug("Render all tags for %s", entry.title)
|
||||
try:
|
||||
tags = await immich_api.tags.async_get_all_tags()
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
@@ -190,12 +178,6 @@ class ImmichMediaSource(MediaSource):
|
||||
LOGGER.debug("Render all people for %s", entry.title)
|
||||
try:
|
||||
people = await immich_api.people.async_get_all_people()
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
@@ -229,12 +211,6 @@ class ImmichMediaSource(MediaSource):
|
||||
identifier.collection_id
|
||||
)
|
||||
assets = album_info.assets
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
@@ -247,12 +223,6 @@ class ImmichMediaSource(MediaSource):
|
||||
assets = await immich_api.search.async_get_all_by_tag_ids(
|
||||
[identifier.collection_id]
|
||||
)
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
@@ -265,24 +235,12 @@ class ImmichMediaSource(MediaSource):
|
||||
assets = await immich_api.search.async_get_all_by_person_ids(
|
||||
[identifier.collection_id]
|
||||
)
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
elif identifier.collection == "favorites":
|
||||
LOGGER.debug("Render all assets for favorites collection")
|
||||
try:
|
||||
assets = await immich_api.search.async_get_all_favorites()
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_api_permission",
|
||||
translation_placeholders={"msg": str(err)},
|
||||
) from err
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
"identifier_unresolvable": {
|
||||
"message": "Could not parse identifier: {identifier}"
|
||||
},
|
||||
"missing_api_permission": {
|
||||
"message": "Missing API permission ({msg})."
|
||||
},
|
||||
"not_configured": {
|
||||
"message": "Immich is not configured."
|
||||
},
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.2.2",
|
||||
"aiolifx==1.2.1",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==1.0.4"
|
||||
"aiolifx-themes==1.0.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DISCOVERY,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
@@ -51,7 +50,6 @@ from .client import (
|
||||
async_subscribe_internal,
|
||||
publish,
|
||||
subscribe,
|
||||
try_connection,
|
||||
)
|
||||
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA
|
||||
from .config_integration import CONFIG_SCHEMA_BASE
|
||||
@@ -81,15 +79,14 @@ from .const import (
|
||||
CONFIG_ENTRY_VERSION,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_PREFIX,
|
||||
DEFAULT_PROTOCOL,
|
||||
DEFAULT_QOS,
|
||||
DEFAULT_RETAIN,
|
||||
DOMAIN,
|
||||
ENTITY_PLATFORMS,
|
||||
ENTRY_OPTION_FIELDS,
|
||||
MQTT_CONNECTION_STATE,
|
||||
PROTOCOL_5,
|
||||
PROTOCOL_311,
|
||||
TEMPLATE_ERRORS,
|
||||
Platform,
|
||||
@@ -499,45 +496,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
mqtt_data: MqttData
|
||||
|
||||
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != PROTOCOL_5:
|
||||
# Automatically migrate the broker protocol to v5 if possible
|
||||
# Can be removed with HA Core 2027.1
|
||||
new_entry_data = entry.data.copy()
|
||||
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
|
||||
# Try the connection with protocol version 5
|
||||
# And update the protocol if successful
|
||||
if await hass.async_add_executor_job(
|
||||
try_connection,
|
||||
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
|
||||
):
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=new_entry_data,
|
||||
)
|
||||
ir.async_delete_issue(hass, DOMAIN, "protocol_5_migration")
|
||||
_LOGGER.info(
|
||||
"The MQTT protocol version was successfully updated to version 5"
|
||||
)
|
||||
else:
|
||||
broker: str = entry.data[CONF_BROKER]
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"protocol_5_migration",
|
||||
issue_domain=DOMAIN,
|
||||
is_fixable=False,
|
||||
breaks_in_ha_version="2027.1.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/mqtt/"
|
||||
"#mqtt-protocol",
|
||||
data={
|
||||
"entry_id": entry.entry_id,
|
||||
"broker": broker,
|
||||
"protocol": protocol,
|
||||
},
|
||||
translation_placeholders={"broker": broker, "protocol": protocol},
|
||||
translation_key="protocol_5_migration",
|
||||
)
|
||||
if (protocol := entry.data.get(CONF_PROTOCOL, PROTOCOL_311)) != DEFAULT_PROTOCOL:
|
||||
broker: str = entry.data[CONF_BROKER]
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"protocol_5_migration",
|
||||
issue_domain=DOMAIN,
|
||||
is_fixable=True,
|
||||
breaks_in_ha_version="2027.1.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/mqtt/#mqtt-protocol",
|
||||
data={
|
||||
"entry_id": entry.entry_id,
|
||||
"broker": broker,
|
||||
"protocol": protocol,
|
||||
},
|
||||
translation_placeholders={"broker": broker, "protocol": protocol},
|
||||
translation_key="protocol_5_migration",
|
||||
)
|
||||
|
||||
async def _setup_client() -> tuple[MqttData, dict[str, Any]]:
|
||||
"""Set up the MQTT client."""
|
||||
|
||||
@@ -9,7 +9,6 @@ from functools import lru_cache, partial
|
||||
from itertools import chain, groupby
|
||||
import logging
|
||||
from operator import attrgetter
|
||||
import queue
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
@@ -93,8 +92,6 @@ from .util import EnsureJobAfterCooldown, get_file_path, mqtt_config_entry_enabl
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MQTT_TIMEOUT = 5
|
||||
|
||||
MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails
|
||||
PREFERRED_BUFFER_SIZE = 8 * 1024 * 1024 # Set receive buffer size to 8MiB
|
||||
|
||||
@@ -436,40 +433,6 @@ class MqttClientSetup:
|
||||
return self._client
|
||||
|
||||
|
||||
def try_connection(
|
||||
user_input: dict[str, Any],
|
||||
) -> bool:
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
mqtt_client_setup = MqttClientSetup(user_input)
|
||||
mqtt_client_setup.setup()
|
||||
client = mqtt_client_setup.client
|
||||
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_connect(
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Handle connection result."""
|
||||
result.put(not reason_code.is_failure)
|
||||
|
||||
client.on_connect = on_connect
|
||||
|
||||
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
|
||||
client.loop_start()
|
||||
|
||||
try:
|
||||
return result.get(timeout=MQTT_TIMEOUT)
|
||||
except queue.Empty:
|
||||
return False
|
||||
finally:
|
||||
client.disconnect()
|
||||
client.loop_stop()
|
||||
|
||||
|
||||
class MQTT:
|
||||
"""Home Assistant MQTT client."""
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
@@ -21,6 +22,7 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
)
|
||||
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
|
||||
import paho.mqtt.client as mqtt
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
@@ -141,7 +143,7 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .addon import get_addon_manager
|
||||
from .client import try_connection
|
||||
from .client import MqttClientSetup
|
||||
from .const import (
|
||||
ALARM_CONTROL_PANEL_SUPPORTED_FEATURES,
|
||||
ATTR_PAYLOAD,
|
||||
@@ -442,6 +444,8 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 5
|
||||
|
||||
CONF_CLIENT_KEY_PASSWORD = "client_key_password"
|
||||
|
||||
MQTT_TIMEOUT = 5
|
||||
|
||||
ADVANCED_OPTIONS = "advanced_options"
|
||||
SET_CA_CERT = "set_ca_cert"
|
||||
SET_CLIENT_CERT = "set_client_cert"
|
||||
@@ -5577,6 +5581,40 @@ async def async_get_broker_settings(
|
||||
return False
|
||||
|
||||
|
||||
def try_connection(
|
||||
user_input: dict[str, Any],
|
||||
) -> bool:
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
mqtt_client_setup = MqttClientSetup(user_input)
|
||||
mqtt_client_setup.setup()
|
||||
client = mqtt_client_setup.client
|
||||
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_connect(
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Handle connection result."""
|
||||
result.put(not reason_code.is_failure)
|
||||
|
||||
client.on_connect = on_connect
|
||||
|
||||
client.connect_async(user_input[CONF_BROKER], user_input[CONF_PORT])
|
||||
client.loop_start()
|
||||
|
||||
try:
|
||||
return result.get(timeout=MQTT_TIMEOUT)
|
||||
except queue.Empty:
|
||||
return False
|
||||
finally:
|
||||
client.disconnect()
|
||||
client.loop_stop()
|
||||
|
||||
|
||||
def check_certicate_chain() -> str | None:
|
||||
"""Check the MQTT certificates."""
|
||||
if client_certificate := get_file_path(CONF_CLIENT_CERT):
|
||||
|
||||
@@ -5,10 +5,12 @@ from typing import TYPE_CHECKING
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
|
||||
from homeassistant.const import CONF_PORT, CONF_PROTOCOL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
from .config_flow import try_connection
|
||||
from .const import DEFAULT_PORT, DOMAIN, PROTOCOL_5
|
||||
|
||||
URL_MQTT_BROKER_CONFIGURATION = (
|
||||
"https://www.home-assistant.io/integrations/mqtt/#broker-configuration"
|
||||
@@ -53,6 +55,55 @@ class MQTTDeviceEntryMigration(RepairsFlow):
|
||||
)
|
||||
|
||||
|
||||
class MQTTProtocolV5Migration(RepairsFlow):
|
||||
"""Handler to migrate to MQTT protocol version 5."""
|
||||
|
||||
def __init__(self, entry_id: str, broker: str, protocol: str) -> None:
|
||||
"""Initialize the flow."""
|
||||
self.entry_id = entry_id
|
||||
self.broker = broker
|
||||
self.protocol = protocol
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None:
|
||||
entry = self.hass.config_entries.async_get_entry(self.entry_id)
|
||||
if TYPE_CHECKING:
|
||||
assert entry is not None
|
||||
new_entry_data = entry.data.copy()
|
||||
new_entry_data[CONF_PROTOCOL] = PROTOCOL_5
|
||||
# Try the connection with protocol version 5
|
||||
if await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
{CONF_PORT: DEFAULT_PORT} | new_entry_data,
|
||||
):
|
||||
self.hass.config_entries.async_update_entry(entry, data=new_entry_data)
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
return self.async_abort(
|
||||
reason="mqtt_broker_migration_to_v5_failed",
|
||||
description_placeholders={
|
||||
"broker": self.broker,
|
||||
"protocol": self.protocol,
|
||||
"url_mqtt_broker_configuration": URL_MQTT_BROKER_CONFIGURATION,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders={"broker": self.broker, "protocol": self.protocol},
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
@@ -62,6 +113,10 @@ async def async_create_fix_flow(
|
||||
if TYPE_CHECKING:
|
||||
assert data is not None
|
||||
entry_id: str = data["entry_id"] # type: ignore[assignment]
|
||||
if issue_id == "protocol_5_migration":
|
||||
broker: str = data["broker"] # type: ignore[assignment]
|
||||
protocol: str = data["protocol"] # type: ignore[assignment]
|
||||
return MQTTProtocolV5Migration(entry_id, broker, protocol)
|
||||
subentry_id: str = data["subentry_id"] # type: ignore[assignment]
|
||||
name: str = data["name"] # type: ignore[assignment]
|
||||
return MQTTDeviceEntryMigration(
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"keepalive": "A value less than 90 seconds is advised.",
|
||||
"password": "The password to log in to your MQTT broker.",
|
||||
"port": "The port your MQTT broker listens to. For example 1883.",
|
||||
"protocol": "The MQTT protocol version that is used. Note that Home Assistant will silently change to version 5 if your broker supports it.",
|
||||
"protocol": "The MQTT protocol your broker operates at. For example 3.1.1.",
|
||||
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT broker's certificate.",
|
||||
"set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.",
|
||||
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
|
||||
@@ -1134,8 +1134,18 @@
|
||||
"title": "Invalid config found for MQTT {domain} item"
|
||||
},
|
||||
"protocol_5_migration": {
|
||||
"description": "The automatic migration to MQTT protocol version 5 failed. The currently configured protocol version for MQTT broker {broker} is {protocol}, but this protocol version is deprecated, and support for it will be removed.\n\nMake sure your broker supports protocol version 5. Update your MQTT broker's connection settings, and restart Home Assistant to fix this issue.",
|
||||
"title": "MQTT protocol migration failed"
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"mqtt_broker_migration_to_v5_failed": "Migrating the broker ({broker}) protocol version from {protocol} to 5 failed, and the migration has been aborted.\n\nYour broker may not support MQTT protocol version 5.\n\nPlease [reconfigure your MQTT broker settings]({url_mqtt_broker_configuration}) or upgrade your broker to support MQTT protocol version 5 to fix this issue."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Home Assistant needs to migrate to MQTT protocol version 5. The currently configured protocol version for broker {broker} is {protocol}. This protocol version is deprecated, and support for it will be removed.\n\nSubmitting this form will attempt to migrate your MQTT broker configuration to use protocol version 5 to fix this issue. If the broker cannot be reached using MQTT protocol version 5, for example because it does not support it, the migration will be aborted.",
|
||||
"title": "MQTT protocol change required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Deprecated MQTT protocol {protocol} in use"
|
||||
},
|
||||
"subentry_migration_discovery": {
|
||||
"fix_flow": {
|
||||
|
||||
@@ -79,14 +79,14 @@ TOKEN_SCHEMA = vol.Schema(
|
||||
|
||||
def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Validate the user input and fetch data (sync, for executor)."""
|
||||
auth_kwargs = (
|
||||
{
|
||||
auth_kwargs = {
|
||||
"password": data.get(CONF_PASSWORD),
|
||||
}
|
||||
if data.get(CONF_TOKEN):
|
||||
auth_kwargs = {
|
||||
"token_name": data[CONF_TOKEN_ID],
|
||||
"token_value": data[CONF_TOKEN_SECRET],
|
||||
}
|
||||
if data.get(CONF_TOKEN)
|
||||
else {"password": data.get(CONF_PASSWORD)}
|
||||
)
|
||||
data = sanitize_config_entry(data)
|
||||
try:
|
||||
client = ProxmoxAPI(
|
||||
@@ -122,9 +122,6 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
raise ProxmoxConnectionError from err
|
||||
|
||||
if not nodes:
|
||||
raise ProxmoxNoNodesFound("No nodes found")
|
||||
|
||||
nodes_data: list[dict[str, Any]] = []
|
||||
for node in nodes:
|
||||
if node.get("status") != NODE_ONLINE:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyrisco"],
|
||||
"requirements": ["pyrisco==0.8.0"]
|
||||
"requirements": ["pyrisco==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.9.6"],
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
|
||||
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
|
||||
}
|
||||
|
||||
+22
-4
@@ -753,11 +753,29 @@ class UnitOfPrecipitationDepth(StrEnum):
|
||||
"""Derived from cm³/cm²"""
|
||||
|
||||
|
||||
class UnitOfDensity(StrEnum):
|
||||
"""Density units.
|
||||
|
||||
Ratio of a substance's mass to its volume.
|
||||
"""
|
||||
|
||||
GRAMS_PER_CUBIC_METER = "g/m³"
|
||||
MILLIGRAMS_PER_CUBIC_METER = "mg/m³"
|
||||
MICROGRAMS_PER_CUBIC_METER = "μg/m³"
|
||||
MICROGRAMS_PER_CUBIC_FOOT = "μg/ft³"
|
||||
|
||||
|
||||
# Concentration units
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³"
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³"
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = UnitOfDensity.GRAMS_PER_CUBIC_METER.value
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = (
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER.value
|
||||
)
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER.value
|
||||
)
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_FOOT.value
|
||||
)
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³"
|
||||
CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
|
||||
CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
|
||||
|
||||
@@ -35,7 +35,7 @@ file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.8.0
|
||||
habluetooth==6.7.9
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
|
||||
@@ -5,9 +5,6 @@ from functools import lru_cache
|
||||
from math import floor, log10
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
@@ -17,6 +14,7 @@ from homeassistant.const import (
|
||||
UnitOfBloodGlucoseConcentration,
|
||||
UnitOfConductivity,
|
||||
UnitOfDataRate,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -248,18 +246,18 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_PARTS_PER_MILLION: 1e6,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER: (
|
||||
_CARBON_MONOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e3
|
||||
),
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: (
|
||||
_CARBON_MONOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -494,14 +492,14 @@ class MassVolumeConcentrationConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "concentration"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1_000_000.0, # 1000 µg/m³ = 1 mg/m³
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1000.0, # 1000 mg/m³ = 1 g/m³
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: 1.0,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 1_000_000.0, # 1000 µg/m³ = 1 mg/m³
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER: 1000.0, # 1000 mg/m³ = 1 g/m³
|
||||
UnitOfDensity.GRAMS_PER_CUBIC_METER: 1.0,
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.GRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -512,14 +510,14 @@ class NitrogenDioxideConcentrationConverter(BaseUnitConverter):
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_PARTS_PER_MILLION: 1e6,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: (
|
||||
_NITROGEN_DIOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -529,13 +527,13 @@ class NitrogenMonoxideConcentrationConverter(BaseUnitConverter):
|
||||
UNIT_CLASS = "nitrogen_monoxide"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: (
|
||||
_NITROGEN_MONOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -546,14 +544,14 @@ class OzoneConcentrationConverter(BaseUnitConverter):
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_PARTS_PER_MILLION: 1e6,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: (
|
||||
_OZONE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
|
||||
),
|
||||
}
|
||||
VALID_UNITS = {
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -751,13 +749,13 @@ class SulphurDioxideConcentrationConverter(BaseUnitConverter):
|
||||
UNIT_CLASS = "sulphur_dioxide"
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
CONCENTRATION_PARTS_PER_BILLION: 1e9,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
|
||||
UnitOfDensity.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,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Generated
+6
-6
@@ -318,10 +318,10 @@ aiolichess==1.3.0
|
||||
aiolifx-effects==0.3.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-themes==1.0.4
|
||||
aiolifx-themes==1.0.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==1.2.2
|
||||
aiolifx==1.2.1
|
||||
|
||||
# homeassistant.components.lookin
|
||||
aiolookin==1.0.0
|
||||
@@ -1213,7 +1213,7 @@ ha-xthings-cloud==1.0.5
|
||||
habiticalib==0.4.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==6.8.0
|
||||
habluetooth==6.7.9
|
||||
|
||||
# homeassistant.components.hanna
|
||||
hanna-cloud==0.0.7
|
||||
@@ -2486,13 +2486,13 @@ pyrecswitch==1.0.2
|
||||
pyrepetierng==0.1.0
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.8.0
|
||||
pyrisco==0.7.0
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.7
|
||||
|
||||
# homeassistant.components.thread
|
||||
pyroute2==0.9.6
|
||||
pyroute2==0.7.5
|
||||
|
||||
# homeassistant.components.rympro
|
||||
pyrympro==0.0.9
|
||||
@@ -3403,7 +3403,7 @@ yalexs-ble==3.3.0
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
yalexs==9.2.7
|
||||
yalexs==9.2.1
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.16
|
||||
|
||||
@@ -6,10 +6,11 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
BASE_COMPONENT = "notify"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_log_level():
|
||||
@@ -25,25 +26,25 @@ async def test_apprise_config_load_fail01(hass: HomeAssistant) -> None:
|
||||
"""Test apprise configuration failures 1."""
|
||||
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {"name": "test", "platform": "apprise", "config": "/path/"}
|
||||
BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.apprise.notify.apprise.AppriseConfig.add",
|
||||
return_value=False,
|
||||
):
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test that our service failed to load
|
||||
assert not hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert not hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
|
||||
async def test_apprise_config_load_fail02(hass: HomeAssistant) -> None:
|
||||
"""Test apprise configuration failures 2."""
|
||||
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {"name": "test", "platform": "apprise", "config": "/path/"}
|
||||
BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"}
|
||||
}
|
||||
|
||||
with (
|
||||
@@ -56,11 +57,11 @@ async def test_apprise_config_load_fail02(hass: HomeAssistant) -> None:
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test that our service failed to load
|
||||
assert not hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert not hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
|
||||
async def test_apprise_config_load_okay(hass: HomeAssistant, tmp_path: Path) -> None:
|
||||
@@ -72,20 +73,20 @@ async def test_apprise_config_load_okay(hass: HomeAssistant, tmp_path: Path) ->
|
||||
f = d / "apprise"
|
||||
f.write_text("mailto://user:pass@example.com/")
|
||||
|
||||
config = {NOTIFY_DOMAIN: {"name": "test", "platform": "apprise", "config": str(f)}}
|
||||
config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}
|
||||
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Valid configuration was loaded; our service is good
|
||||
assert hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
|
||||
async def test_apprise_url_load_fail(hass: HomeAssistant) -> None:
|
||||
"""Test apprise url failure."""
|
||||
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {
|
||||
BASE_COMPONENT: {
|
||||
"name": "test",
|
||||
"platform": "apprise",
|
||||
"url": "mailto://user:pass@example.com",
|
||||
@@ -95,18 +96,18 @@ async def test_apprise_url_load_fail(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.apprise.notify.apprise.Apprise.add",
|
||||
return_value=False,
|
||||
):
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test that our service failed to load
|
||||
assert not hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert not hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
|
||||
async def test_apprise_notification(hass: HomeAssistant) -> None:
|
||||
"""Test apprise notification."""
|
||||
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {
|
||||
BASE_COMPONENT: {
|
||||
"name": "test",
|
||||
"platform": "apprise",
|
||||
"url": "mailto://user:pass@example.com",
|
||||
@@ -123,18 +124,18 @@ async def test_apprise_notification(hass: HomeAssistant) -> None:
|
||||
obj.add.return_value = True
|
||||
obj.notify.return_value = True
|
||||
mock_apprise.return_value = obj
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test the existence of our service
|
||||
assert hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
# Test the call to our underlining notify() call
|
||||
await hass.services.async_call(NOTIFY_DOMAIN, "test", data)
|
||||
await hass.services.async_call(BASE_COMPONENT, "test", data)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Validate calls were made under the hood correctly
|
||||
obj.add.assert_called_once_with(config[NOTIFY_DOMAIN]["url"])
|
||||
obj.add.assert_called_once_with(config[BASE_COMPONENT]["url"])
|
||||
obj.notify.assert_called_once_with(
|
||||
body=data["message"], title=data["title"], tag=None
|
||||
)
|
||||
@@ -144,7 +145,7 @@ async def test_apprise_multiple_notification(hass: HomeAssistant) -> None:
|
||||
"""Test apprise notification."""
|
||||
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {
|
||||
BASE_COMPONENT: {
|
||||
"name": "test",
|
||||
"platform": "apprise",
|
||||
"url": [
|
||||
@@ -164,14 +165,14 @@ async def test_apprise_multiple_notification(hass: HomeAssistant) -> None:
|
||||
obj.add.return_value = True
|
||||
obj.notify.return_value = True
|
||||
mock_apprise.return_value = obj
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test the existence of our service
|
||||
assert hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
# Test the call to our underlining notify() call
|
||||
await hass.services.async_call(NOTIFY_DOMAIN, "test", data)
|
||||
await hass.services.async_call(BASE_COMPONENT, "test", data)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Validate 2 calls were made under the hood
|
||||
@@ -195,7 +196,7 @@ async def test_apprise_notification_with_target(
|
||||
f.write_text("devops=mailto://user:pass@example.com/\r\n")
|
||||
f.write_text("system,alert=syslog://\r\n")
|
||||
|
||||
config = {NOTIFY_DOMAIN: {"name": "test", "platform": "apprise", "config": str(f)}}
|
||||
config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}}
|
||||
|
||||
# Our Message, only notify the services tagged with "devops"
|
||||
data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]}
|
||||
@@ -207,14 +208,14 @@ async def test_apprise_notification_with_target(
|
||||
apprise_obj.add.return_value = True
|
||||
apprise_obj.notify.return_value = True
|
||||
mock_apprise.return_value = apprise_obj
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test the existence of our service
|
||||
assert hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
# Test the call to our underlining notify() call
|
||||
await hass.services.async_call(NOTIFY_DOMAIN, "test", data)
|
||||
await hass.services.async_call(BASE_COMPONENT, "test", data)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Validate calls were made under the hood correctly
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.avea.const import AVEA_SERVICE_UUID, DOMAIN
|
||||
from homeassistant.components.avea.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -12,11 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import AVEA_DISCOVERY_INFO, NOT_AVEA_DISCOVERY_INFO
|
||||
|
||||
from tests.components.bluetooth import (
|
||||
generate_advertisement_data,
|
||||
generate_ble_device,
|
||||
inject_bluetooth_service_info,
|
||||
)
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("enable_bluetooth")
|
||||
|
||||
@@ -39,22 +35,13 @@ async def test_user_step_success(hass: HomeAssistant) -> None:
|
||||
inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
|
||||
) as mock_request_active_scan:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
assert result["data_schema"].schema[CONF_ADDRESS].container == {
|
||||
AVEA_DISCOVERY_INFO.address: (
|
||||
f"{AVEA_DISCOVERY_INFO.name} ({AVEA_DISCOVERY_INFO.address})"
|
||||
)
|
||||
}
|
||||
mock_request_active_scan.assert_awaited_once_with(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -80,62 +67,14 @@ async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
|
||||
inject_bluetooth_service_info(hass, NOT_AVEA_DISCOVERY_INFO)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
async def test_user_step_unnamed_device_label(hass: HomeAssistant) -> None:
|
||||
"""Test unnamed discovered devices are shown without duplicating the address."""
|
||||
discovery_info = type(AVEA_DISCOVERY_INFO)(
|
||||
name=AVEA_DISCOVERY_INFO.address,
|
||||
address=AVEA_DISCOVERY_INFO.address,
|
||||
rssi=-60,
|
||||
manufacturer_data={},
|
||||
service_uuids=[AVEA_SERVICE_UUID],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=generate_ble_device(
|
||||
address=AVEA_DISCOVERY_INFO.address, name=AVEA_DISCOVERY_INFO.address
|
||||
),
|
||||
advertisement=generate_advertisement_data(
|
||||
local_name=AVEA_DISCOVERY_INFO.address,
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
service_uuids=[AVEA_SERVICE_UUID],
|
||||
),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.avea.config_flow.async_discovered_service_info",
|
||||
return_value=[discovery_info],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.avea.config_flow.bluetooth.async_request_active_scan"
|
||||
) as mock_request_active_scan,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["data_schema"].schema[CONF_ADDRESS].container == {
|
||||
AVEA_DISCOVERY_INFO.address: AVEA_DISCOVERY_INFO.address
|
||||
}
|
||||
mock_request_active_scan.assert_awaited_once_with(hass)
|
||||
|
||||
|
||||
async def test_user_step_cannot_connect_recovers(hass: HomeAssistant) -> None:
|
||||
"""Test the user step recovers after a cannot connect error."""
|
||||
inject_bluetooth_service_info(hass, AVEA_DISCOVERY_INFO)
|
||||
|
||||
@@ -10,11 +10,9 @@ from homeassistant.components.bluetooth import (
|
||||
MONOTONIC_TIME,
|
||||
BaseHaRemoteScanner,
|
||||
BluetoothChange,
|
||||
BluetoothReachabilityIntent,
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfo,
|
||||
HaBluetoothConnector,
|
||||
async_address_reachability_diagnostics,
|
||||
async_clear_advertisement_history,
|
||||
async_request_active_scan,
|
||||
async_scanner_by_source,
|
||||
@@ -114,57 +112,6 @@ async def test_async_scanner_devices_by_address_connectable(
|
||||
cancel()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth")
|
||||
async def test_async_address_reachability_diagnostics(hass: HomeAssistant) -> None:
|
||||
"""Test the address reachability diagnostics passthrough."""
|
||||
# An address that was never seen reports as unknown.
|
||||
assert "unknown" in async_address_reachability_diagnostics(
|
||||
hass, "44:44:33:11:23:99", BluetoothReachabilityIntent.CONNECTION
|
||||
)
|
||||
|
||||
manager = _get_manager()
|
||||
|
||||
class FakeInjectableScanner(BaseHaRemoteScanner):
|
||||
def inject_advertisement(
|
||||
self, device: BLEDevice, advertisement_data: AdvertisementData
|
||||
) -> None:
|
||||
"""Inject an advertisement."""
|
||||
self._async_on_advertisement(
|
||||
device.address,
|
||||
advertisement_data.rssi,
|
||||
device.name,
|
||||
advertisement_data.service_uuids,
|
||||
advertisement_data.service_data,
|
||||
advertisement_data.manufacturer_data,
|
||||
advertisement_data.tx_power,
|
||||
{"scanner_specific_data": "test"},
|
||||
MONOTONIC_TIME(),
|
||||
)
|
||||
|
||||
connector = HaBluetoothConnector(MockBleakClient, "esp32", lambda: True)
|
||||
scanner = FakeInjectableScanner("esp32", "esp32", connector, True)
|
||||
unsetup = scanner.async_setup()
|
||||
cancel = manager.async_register_scanner(scanner)
|
||||
switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand", {})
|
||||
switchbot_device_adv = generate_advertisement_data(local_name="wohand", rssi=-80)
|
||||
scanner.inject_advertisement(switchbot_device, switchbot_device_adv)
|
||||
|
||||
connection_diag = async_address_reachability_diagnostics(
|
||||
hass, "44:44:33:11:23:45", BluetoothReachabilityIntent.CONNECTION
|
||||
)
|
||||
assert "in connectable history" in connection_diag
|
||||
assert "esp32" in connection_diag
|
||||
|
||||
# An advertisement intent does not report connectable paths or slots.
|
||||
advertisement_diag = async_address_reachability_diagnostics(
|
||||
hass, "44:44:33:11:23:45", BluetoothReachabilityIntent.PASSIVE_ADVERTISEMENT
|
||||
)
|
||||
assert "advertising" in advertisement_diag
|
||||
|
||||
unsetup()
|
||||
cancel()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth")
|
||||
async def test_async_scanner_devices_by_address_non_connectable(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -5,8 +5,6 @@ from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioesphomeapi import APIClient, UpdateCommand, UpdateInfo, UpdateState
|
||||
from awesomeversion import AwesomeVersion
|
||||
from awesomeversion.exceptions import AwesomeVersionCompareException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.esphome.dashboard import async_get_dashboard
|
||||
@@ -549,128 +547,6 @@ async def test_generic_device_update_entity_has_update(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_version", "latest_version"),
|
||||
[
|
||||
("2025.11.5_c51f7548", "2025.11.6_aabbccdd"),
|
||||
("2025.11.5_c51f7548", "2025.11.5_aabbccdd"),
|
||||
("2025.11.6_aabbccdd", "2025.11.5_c51f7548"),
|
||||
],
|
||||
ids=["newer_base", "same_base_new_build", "older_base"],
|
||||
)
|
||||
def test_awesomeversion_cannot_compare_project_versions(
|
||||
current_version: str, latest_version: str
|
||||
) -> None:
|
||||
"""Prove AwesomeVersion raises on ESPHome project versions.
|
||||
|
||||
ESPHome project versions carry a build suffix (e.g. 2025.11.5_c51f7548).
|
||||
AwesomeVersion cannot parse these, so the base UpdateEntity comparison would
|
||||
raise and force the entity on, which is why ESPHomeUpdateEntity mirrors the
|
||||
device by comparing with a plain string inequality instead.
|
||||
"""
|
||||
with pytest.raises(AwesomeVersionCompareException):
|
||||
assert AwesomeVersion(latest_version) > current_version
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_version", "latest_version", "expected_state"),
|
||||
[
|
||||
("2025.11.5_c51f7548", "2025.11.6_aabbccdd", STATE_ON),
|
||||
("2025.11.5_c51f7548", "2025.11.5_aabbccdd", STATE_OFF),
|
||||
("2025.11.6_aabbccdd", "2025.11.5_c51f7548", STATE_OFF),
|
||||
("2025.11.5_c51f7548", "2025.11.5_c51f7548", STATE_OFF),
|
||||
],
|
||||
ids=["newer_base", "same_base_new_build", "older_base", "identical"],
|
||||
)
|
||||
async def test_generic_device_update_entity_project_version(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_generic_device_entry: MockGenericDeviceEntryType,
|
||||
current_version: str,
|
||||
latest_version: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test version comparison for ESPHome project versions.
|
||||
|
||||
AwesomeVersion cannot parse the build suffix, so the entity strips it and
|
||||
compares the real versions: only a genuinely newer base version is offered;
|
||||
a different build of the same version or an older version is not.
|
||||
"""
|
||||
entity_info = [
|
||||
UpdateInfo(
|
||||
object_id="myupdate",
|
||||
key=1,
|
||||
name="my update",
|
||||
)
|
||||
]
|
||||
states = [
|
||||
UpdateState(
|
||||
key=1,
|
||||
current_version=current_version,
|
||||
latest_version=latest_version,
|
||||
title="ESPHome Project",
|
||||
release_summary=RELEASE_SUMMARY,
|
||||
release_url=RELEASE_URL,
|
||||
)
|
||||
]
|
||||
await mock_generic_device_entry(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
states=states,
|
||||
)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_generic_device_update_entity_clears_after_ota(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
) -> None:
|
||||
"""Test a project version update clears once the device runs the new build."""
|
||||
entity_info = [
|
||||
UpdateInfo(
|
||||
object_id="myupdate",
|
||||
key=1,
|
||||
name="my update",
|
||||
)
|
||||
]
|
||||
states = [
|
||||
UpdateState(
|
||||
key=1,
|
||||
current_version="2025.11.5_c51f7548",
|
||||
latest_version="2025.11.6_aabbccdd",
|
||||
title="ESPHome Project",
|
||||
release_summary=RELEASE_SUMMARY,
|
||||
release_url=RELEASE_URL,
|
||||
)
|
||||
]
|
||||
mock_device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
states=states,
|
||||
)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
mock_device.set_state(
|
||||
UpdateState(
|
||||
key=1,
|
||||
current_version="2025.11.6_aabbccdd",
|
||||
latest_version="2025.11.6_aabbccdd",
|
||||
title="ESPHome Project",
|
||||
release_summary=RELEASE_SUMMARY,
|
||||
release_url=RELEASE_URL,
|
||||
)
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_update_entity_release_notes(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
|
||||
@@ -5,7 +5,7 @@ import tempfile
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import web
|
||||
from aioimmich.exceptions import ImmichError, ImmichForbiddenError
|
||||
from aioimmich.exceptions import ImmichError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.immich.const import DOMAIN
|
||||
@@ -252,12 +252,6 @@ async def test_browse_media_collections_error(
|
||||
with patch("homeassistant.components.immich.PLATFORMS", []):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
item = MediaSourceItem(
|
||||
hass, DOMAIN, f"{mock_config_entry.unique_id}|{collection}", None
|
||||
)
|
||||
source = await async_get_media_source(hass)
|
||||
|
||||
# test generic ImmichError
|
||||
getattr(
|
||||
getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1]
|
||||
).side_effect = ImmichError(
|
||||
@@ -269,26 +263,17 @@ async def test_browse_media_collections_error(
|
||||
}
|
||||
)
|
||||
|
||||
source = await async_get_media_source(hass)
|
||||
|
||||
item = MediaSourceItem(
|
||||
hass, DOMAIN, f"{mock_config_entry.unique_id}|{collection}", None
|
||||
)
|
||||
result = await source.async_browse_media(item)
|
||||
|
||||
assert result
|
||||
assert result.identifier is None
|
||||
assert len(result.children) == 0
|
||||
|
||||
# test specific ImmichForbiddenError
|
||||
getattr(
|
||||
getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1]
|
||||
).side_effect = ImmichForbiddenError(
|
||||
{
|
||||
"message": "Missing required permission: asset.read",
|
||||
"error": "Forbidden",
|
||||
"statusCode": 403,
|
||||
"correlationId": "e0hlizyl",
|
||||
}
|
||||
)
|
||||
with pytest.raises(BrowseError, match="Missing API permission"):
|
||||
await source.async_browse_media(item)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("collection", "mocked_get_fn"),
|
||||
@@ -312,15 +297,8 @@ async def test_browse_media_collection_items_error(
|
||||
with patch("homeassistant.components.immich.PLATFORMS", []):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
item = MediaSourceItem(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{mock_config_entry.unique_id}|{collection}|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
|
||||
None,
|
||||
)
|
||||
source = await async_get_media_source(hass)
|
||||
|
||||
# test generic ImmichError
|
||||
getattr(
|
||||
getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1]
|
||||
).side_effect = ImmichError(
|
||||
@@ -331,27 +309,18 @@ async def test_browse_media_collection_items_error(
|
||||
"correlationId": "e0hlizyl",
|
||||
}
|
||||
)
|
||||
|
||||
item = MediaSourceItem(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{mock_config_entry.unique_id}|{collection}|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
|
||||
None,
|
||||
)
|
||||
result = await source.async_browse_media(item)
|
||||
|
||||
assert result
|
||||
assert result.identifier is None
|
||||
assert len(result.children) == 0
|
||||
|
||||
# test specific ImmichForbiddenError
|
||||
getattr(
|
||||
getattr(mock_immich, mocked_get_fn[0]), mocked_get_fn[1]
|
||||
).side_effect = ImmichForbiddenError(
|
||||
{
|
||||
"message": "Missing required permission: asset.read",
|
||||
"error": "Forbidden",
|
||||
"statusCode": 403,
|
||||
"correlationId": "e0hlizyl",
|
||||
}
|
||||
)
|
||||
with pytest.raises(BrowseError, match="Missing API permission"):
|
||||
await source.async_browse_media(item)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("collection", "collection_id", "children"),
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.indevolt.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE
|
||||
from homeassistant.components.number import SERVICE_SET_VALUE
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -87,7 +87,7 @@ async def test_number_set_values(
|
||||
|
||||
# Call the service to set the value
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
Platform.NUMBER,
|
||||
SERVICE_SET_VALUE,
|
||||
{"entity_id": entity_id, "value": test_value},
|
||||
blocking=True,
|
||||
@@ -116,7 +116,7 @@ async def test_number_set_value_error(
|
||||
# Attempt to set value
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
Platform.NUMBER,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
"entity_id": "number.cms_sf2000_discharge_limit",
|
||||
|
||||
@@ -9,10 +9,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.indevolt.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.components.select import SERVICE_SELECT_OPTION
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -69,7 +66,7 @@ async def test_select_option(
|
||||
|
||||
# Attempt to change option
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
Platform.SELECT,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{"entity_id": "select.cms_sf2000_energy_mode", "option": option},
|
||||
blocking=True,
|
||||
@@ -100,7 +97,7 @@ async def test_select_set_option_error(
|
||||
# Attempt to change option
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
Platform.SELECT,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
"entity_id": "select.cms_sf2000_energy_mode",
|
||||
|
||||
@@ -9,11 +9,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.indevolt.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -88,7 +84,7 @@ async def test_switch_turn_on(
|
||||
|
||||
# Call the service to turn on
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{"entity_id": entity_id},
|
||||
blocking=True,
|
||||
@@ -147,7 +143,7 @@ async def test_switch_turn_off(
|
||||
|
||||
# Call the service to turn off
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_OFF,
|
||||
{"entity_id": entity_id},
|
||||
blocking=True,
|
||||
@@ -176,7 +172,7 @@ async def test_switch_set_value_error(
|
||||
# Attempt to switch on
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{"entity_id": "switch.cms_sf2000_allow_grid_charging"},
|
||||
blocking=True,
|
||||
|
||||
@@ -1488,6 +1488,11 @@
|
||||
'infrared': False,
|
||||
'matrix': False,
|
||||
'max_kelvin': 9000,
|
||||
'min_ext_mz_firmware': 1532997580,
|
||||
'min_ext_mz_firmware_components': list([
|
||||
2,
|
||||
77,
|
||||
]),
|
||||
'min_kelvin': 1500,
|
||||
'multizone': True,
|
||||
'relays': False,
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import AsyncGenerator, Generator
|
||||
from pathlib import Path
|
||||
from random import getrandbits
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -33,24 +33,6 @@ def patch_hass_config(mock_hass_config: None) -> None:
|
||||
"""Patch configuration.yaml."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_v5_protocol_check() -> bool:
|
||||
"""Fixture to mock a v5 protocol test result."""
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_try_connection_protocol_check(
|
||||
hass: HomeAssistant, mock_v5_protocol_check: bool
|
||||
) -> Generator[MagicMock]:
|
||||
"""Patch try_connection."""
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.try_connection",
|
||||
return_value=mock_v5_protocol_check,
|
||||
) as mock_try_connection:
|
||||
yield mock_try_connection
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir_prefix() -> str:
|
||||
"""Set an alternate temp dir prefix."""
|
||||
|
||||
@@ -230,7 +230,6 @@ async def test_publish(
|
||||
assert publish_mock.call_args[0][4].json() == {"MessageExpiryInterval": 60}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_v5_protocol_check", [False])
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_options", "mqtt_config_entry_data", "protocol"),
|
||||
[
|
||||
@@ -1258,12 +1257,7 @@ async def test_restore_subscriptions_on_reconnect(
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "mqtt_config_entry_options"),
|
||||
[
|
||||
(
|
||||
{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_PROTOCOL: "5"},
|
||||
{mqtt.CONF_DISCOVERY: False},
|
||||
)
|
||||
],
|
||||
[({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_DISCOVERY: False})],
|
||||
)
|
||||
async def test_restore_all_active_subscriptions_on_reconnect(
|
||||
hass: HomeAssistant,
|
||||
@@ -1283,7 +1277,7 @@ async def test_restore_all_active_subscriptions_on_reconnect(
|
||||
|
||||
# the subscription with the highest QoS should survive
|
||||
expected = [
|
||||
call([("test/state", 2)], properties=ANY),
|
||||
call([("test/state", 2)], properties=None),
|
||||
]
|
||||
assert mqtt_client_mock.subscribe.mock_calls == expected
|
||||
|
||||
@@ -1297,7 +1291,7 @@ async def test_restore_all_active_subscriptions_on_reconnect(
|
||||
# wait for cooldown
|
||||
await mock_debouncer.wait()
|
||||
|
||||
expected.append(call([("test/state", 1)], properties=ANY))
|
||||
expected.append(call([("test/state", 1)], properties=None))
|
||||
for expected_call in expected:
|
||||
assert mqtt_client_mock.subscribe.hass_call(expected_call)
|
||||
|
||||
@@ -1555,7 +1549,6 @@ async def test_handle_message_callback(
|
||||
assert callbacks[0].payload == "test-payload"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_v5_protocol_check", [False])
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "protocol", "clean_session"),
|
||||
[
|
||||
@@ -1589,6 +1582,7 @@ async def test_handle_message_callback(
|
||||
async def test_setup_mqtt_client_clean_session_and_protocol(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
protocol: int,
|
||||
clean_session: bool | None,
|
||||
) -> None:
|
||||
@@ -1603,7 +1597,6 @@ async def test_setup_mqtt_client_clean_session_and_protocol(
|
||||
assert mock_client.call_args[1]["protocol"] == protocol
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_v5_protocol_check", [False])
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "connect_args"),
|
||||
[
|
||||
|
||||
@@ -130,7 +130,6 @@ MOCK_ENCRYPTED_CLIENT_KEY_DER = b"## mock DER formatted encrypted key file ##\n"
|
||||
|
||||
MOCK_ENTRY_DATA = {
|
||||
mqtt.CONF_BROKER: "test-broker",
|
||||
CONF_PROTOCOL: "5",
|
||||
CONF_PORT: 1234,
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
@@ -274,7 +273,7 @@ def mock_try_connection_time_out() -> Generator[MagicMock]:
|
||||
# Patch prevent waiting 5 sec for a timeout
|
||||
with (
|
||||
patch("homeassistant.components.mqtt.client.AsyncMQTTClient") as mock_client,
|
||||
patch("homeassistant.components.mqtt.client.MQTT_TIMEOUT", 0),
|
||||
patch("homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0),
|
||||
):
|
||||
mock_client().loop_start = lambda *args: 1
|
||||
yield mock_client()
|
||||
@@ -1757,7 +1756,6 @@ async def test_step_hassio_reauth(
|
||||
mock_try_connection.assert_called_once_with(
|
||||
{
|
||||
"broker": "core-mosquitto",
|
||||
CONF_PROTOCOL: "5",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
|
||||
@@ -5,7 +5,6 @@ from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, TypedDict
|
||||
from unittest.mock import ANY, MagicMock, Mock, mock_open, patch
|
||||
@@ -29,7 +28,6 @@ from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
SERVICE_RELOAD,
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -55,7 +53,6 @@ from tests.common import (
|
||||
MockEntity,
|
||||
MockEntityPlatform,
|
||||
MockMqttReasonCode,
|
||||
async_capture_events,
|
||||
async_fire_mqtt_message,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache,
|
||||
@@ -2473,125 +2470,3 @@ async def test_yaml_config_with_active_mqtt_config_entry(
|
||||
state = hass.states.get("sensor.mqtt_sensor")
|
||||
assert state is not None
|
||||
assert issue is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mqtt_config_entry_data",
|
||||
[
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
},
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
],
|
||||
ids=[
|
||||
"entry_without_protocol_without_port",
|
||||
"entry_without_protocol_with_port",
|
||||
"entry_with_protocol_3.1.1",
|
||||
"entry_with_protocol_3.1",
|
||||
],
|
||||
)
|
||||
async def test_mqtt_protocol_successful_migration_to_v5(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test the silent MQTT protocol migration is successful."""
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
await mqtt_mock_entry()
|
||||
assert len(events) == 0
|
||||
assert (
|
||||
"The MQTT protocol version was successfully updated to version 5"
|
||||
in caplog.text
|
||||
)
|
||||
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
assert entry.data[mqtt.CONF_PROTOCOL] == mqtt.PROTOCOL_5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_v5_protocol_check", [False])
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "current_protocol"),
|
||||
[
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"entry_without_protocol_without_port",
|
||||
"entry_without_protocol_with_port",
|
||||
"entry_with_protocol_3.1.1",
|
||||
"entry_with_protocol_3.1",
|
||||
],
|
||||
)
|
||||
async def test_mqtt_protocol_failed_migration_to_v5(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
current_protocol: str,
|
||||
) -> None:
|
||||
"""Test failed silent MQTT protocol migration creates a repair issue."""
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||
|
||||
await mqtt_mock_entry()
|
||||
assert len(events) == 1
|
||||
assert events[0].data["issue_id"] == "protocol_5_migration"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(mqtt.DOMAIN, "protocol_5_migration")
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "protocol_5_migration"
|
||||
assert issue.translation_placeholders == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
}
|
||||
assert (
|
||||
"The MQTT protocol version was successfully updated to version 5"
|
||||
not in caplog.text
|
||||
)
|
||||
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
assert entry.data.get(mqtt.CONF_PROTOCOL, mqtt.PROTOCOL_311) == current_protocol
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
"""Test repairs for MQTT."""
|
||||
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Coroutine, Generator
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigSubentry, ConfigSubentryData
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.const import CONF_PORT, CONF_PROTOCOL, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.yaml import parse_yaml
|
||||
|
||||
from .common import MOCK_NOTIFY_SUBENTRY_DATA_MULTI, async_fire_mqtt_message
|
||||
@@ -27,6 +28,13 @@ from tests.conftest import ClientSessionGenerator
|
||||
from tests.typing import MqttMockHAClientGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_try_connection() -> Generator[MagicMock]:
|
||||
"""Mock the try connection method."""
|
||||
with patch("homeassistant.components.mqtt.repairs.try_connection") as mock_try:
|
||||
yield mock_try
|
||||
|
||||
|
||||
async def help_setup_yaml(hass: HomeAssistant, config: dict[str, str]) -> None:
|
||||
"""Help to set up an exported MQTT device via YAML."""
|
||||
with patch(
|
||||
@@ -177,3 +185,183 @@ async def test_subentry_reconfigure_export_settings(
|
||||
device = device_registry.async_get_device(identifiers={(mqtt.DOMAIN, subentry_id)})
|
||||
assert device.config_entries_subentries[config_entry.entry_id] == {None}
|
||||
assert device is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "current_protocol"),
|
||||
[
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"entry_without_protocol_without_port",
|
||||
"entry_without_protocol_with_port",
|
||||
"entry_with_protocol_3.1.1",
|
||||
"entry_with_protocol_3.1",
|
||||
],
|
||||
)
|
||||
async def test_mqtt_protocol_successful_migration_to_v5(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mqtt_config_entry_data: dict[str, Any],
|
||||
current_protocol: str,
|
||||
mock_try_connection: MagicMock,
|
||||
) -> None:
|
||||
"""Test the MQTT protocol migration repair flow is successful."""
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||
|
||||
await mqtt_mock_entry()
|
||||
assert len(events) == 1
|
||||
assert events[0].data["issue_id"] == "protocol_5_migration"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(mqtt.DOMAIN, "protocol_5_migration")
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "protocol_5_migration"
|
||||
assert issue.translation_placeholders == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
}
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
data = await start_repair_fix_flow(client, mqtt.DOMAIN, "protocol_5_migration")
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["description_placeholders"] == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
}
|
||||
assert data["step_id"] == "confirm"
|
||||
|
||||
mock_try_connection.side_effect = lambda x: True
|
||||
data = await process_repair_fix_flow(client, flow_id)
|
||||
assert data["type"] == "create_entry"
|
||||
expected_entry_data: dict[str, Any] = mqtt_config_entry_data | {CONF_PROTOCOL: "5"}
|
||||
mock_try_connection.assert_called_once_with(expected_entry_data | {CONF_PORT: 1883})
|
||||
entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
assert entry.data == expected_entry_data
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config_entry_data", "current_protocol"),
|
||||
[
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1.1",
|
||||
),
|
||||
(
|
||||
{
|
||||
mqtt.CONF_BROKER: "mock-broker",
|
||||
CONF_PROTOCOL: "3.1",
|
||||
CONF_PORT: 1883,
|
||||
},
|
||||
"3.1",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"entry_without_protocol_without_port",
|
||||
"entry_without_protocol_with_port",
|
||||
"entry_with_protocol_3.1.1",
|
||||
"entry_with_protocol_3.1",
|
||||
],
|
||||
)
|
||||
async def test_mqtt_protocol_failed_migration_to_v5(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
hass_client: ClientSessionGenerator,
|
||||
current_protocol: str,
|
||||
mock_try_connection: MagicMock,
|
||||
) -> None:
|
||||
"""Test the MQTT protocol migration repair flow fails."""
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||
|
||||
await mqtt_mock_entry()
|
||||
assert len(events) == 1
|
||||
assert events[0].data["issue_id"] == "protocol_5_migration"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(mqtt.DOMAIN, "protocol_5_migration")
|
||||
assert issue is not None
|
||||
assert issue.translation_key == "protocol_5_migration"
|
||||
assert issue.translation_placeholders == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
}
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
data = await start_repair_fix_flow(client, mqtt.DOMAIN, "protocol_5_migration")
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["description_placeholders"] == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
}
|
||||
assert data["step_id"] == "confirm"
|
||||
|
||||
mock_try_connection.side_effect = lambda x: False
|
||||
data = await process_repair_fix_flow(client, flow_id)
|
||||
assert data["type"] == "abort"
|
||||
assert data["reason"] == "mqtt_broker_migration_to_v5_failed"
|
||||
assert data["description_placeholders"] == {
|
||||
"broker": "mock-broker",
|
||||
"protocol": current_protocol,
|
||||
"url_mqtt_broker_configuration": "https://www.home-assistant.io/integrations/mqtt/#broker-configuration",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -274,7 +274,9 @@ async def test_yaml_import(
|
||||
mocked_projector: MagicMock,
|
||||
) -> None:
|
||||
"""Test a YAML media player is imported and becomes an operational config entry."""
|
||||
assert await async_setup_component(hass, media_player.DOMAIN, _EXAMPLE_YAML_CONFIG)
|
||||
assert await async_setup_component(
|
||||
hass, Platform.MEDIA_PLAYER, _EXAMPLE_YAML_CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the config entry was created
|
||||
@@ -306,7 +308,7 @@ async def test_failed_yaml_import(
|
||||
|
||||
with patch("pypjlink.Projector.from_address", side_effect=side_effect):
|
||||
assert await async_setup_component(
|
||||
hass, media_player.DOMAIN, _EXAMPLE_YAML_CONFIG
|
||||
hass, Platform.MEDIA_PLAYER, _EXAMPLE_YAML_CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -367,33 +367,6 @@ async def test_form_no_nodes_exception(
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_form_no_nodes_empty_list(
|
||||
hass: HomeAssistant,
|
||||
mock_proxmox_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test we handle no nodes found exception when empty list is returned."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
mock_proxmox_client.nodes.get.return_value = []
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_USER_STEP
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user_auth"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=MOCK_USER_AUTH_STEP_PASSWORD
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_nodes_found"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -12,7 +12,6 @@ import pytest
|
||||
from requests_mock.mocker import Mocker
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
@@ -26,11 +25,13 @@ from .conftest import (
|
||||
SignalNotificationService,
|
||||
)
|
||||
|
||||
BASE_COMPONENT = "notify"
|
||||
|
||||
|
||||
async def test_signal_messenger_init(hass: HomeAssistant) -> None:
|
||||
"""Test that service loads successfully."""
|
||||
config = {
|
||||
NOTIFY_DOMAIN: {
|
||||
BASE_COMPONENT: {
|
||||
"name": "test",
|
||||
"platform": "signal_messenger",
|
||||
"url": "http://127.0.0.1:8080",
|
||||
@@ -40,10 +41,10 @@ async def test_signal_messenger_init(hass: HomeAssistant) -> None:
|
||||
}
|
||||
|
||||
with patch("pysignalclirestapi.SignalCliRestApi.send_message", return_value=None):
|
||||
assert await async_setup_component(hass, NOTIFY_DOMAIN, config)
|
||||
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.services.has_service(NOTIFY_DOMAIN, "test")
|
||||
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||
|
||||
|
||||
def test_send_message(
|
||||
|
||||
@@ -14,7 +14,6 @@ from unifi_access_api import (
|
||||
UnifiAccessError,
|
||||
)
|
||||
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.components.unifi_access.const import DOMAIN
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -94,7 +93,7 @@ async def test_select_option_calls_api(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
Platform.SELECT,
|
||||
"select_option",
|
||||
{"entity_id": FRONT_DOOR_LOCK_RULE_SELECT_ENTITY, "option": "keep_lock"},
|
||||
blocking=True,
|
||||
@@ -117,7 +116,7 @@ async def test_select_schedule_option_does_not_call_api(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
Platform.SELECT,
|
||||
"select_option",
|
||||
{"entity_id": FRONT_DOOR_LOCK_RULE_SELECT_ENTITY, "option": "schedule"},
|
||||
blocking=True,
|
||||
@@ -230,7 +229,7 @@ async def test_select_option_raises_on_api_error(
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
Platform.SELECT,
|
||||
"select_option",
|
||||
{
|
||||
"entity_id": FRONT_DOOR_LOCK_RULE_SELECT_ENTITY,
|
||||
|
||||
@@ -13,7 +13,6 @@ from uiprotect.data import (
|
||||
from uiprotect.exceptions import ClientError, NotAuthorized
|
||||
from uiprotect.websocket import WebsocketState
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -22,6 +21,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -167,7 +167,7 @@ async def test_relay_switch_turn_on_off(
|
||||
await init_entry(hass, ufp, [])
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -176,7 +176,7 @@ async def test_relay_switch_turn_on_off(
|
||||
relay.activate_output.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -245,7 +245,7 @@ async def test_relay_switch_command_error_raises(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -264,7 +264,7 @@ async def test_relay_switch_client_error_raises(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -284,7 +284,7 @@ async def test_relay_switch_command_when_relay_gone(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -303,7 +303,7 @@ async def test_relay_switch_command_when_bootstrap_unavailable(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -456,7 +456,7 @@ async def test_relay_switch_command_when_output_gone(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
Platform.SWITCH,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SWITCH_ENTITY_ID},
|
||||
blocking=True,
|
||||
|
||||
@@ -14,11 +14,7 @@ from uiprotect.data import (
|
||||
from uiprotect.exceptions import ClientError, NotAuthorized
|
||||
from uiprotect.websocket import WebsocketState
|
||||
|
||||
from homeassistant.components.siren import (
|
||||
ATTR_DURATION,
|
||||
ATTR_VOLUME_LEVEL,
|
||||
DOMAIN as SIREN_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.siren import ATTR_DURATION, ATTR_VOLUME_LEVEL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -159,7 +155,7 @@ async def test_siren_turn_on(
|
||||
await init_entry(hass, ufp_with_siren, [])
|
||||
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -187,7 +183,7 @@ async def test_siren_turn_on_with_duration(
|
||||
await init_entry(hass, ufp_with_siren, [])
|
||||
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID, ATTR_DURATION: seconds},
|
||||
blocking=True,
|
||||
@@ -205,7 +201,7 @@ async def test_siren_turn_on_invalid_duration(
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID, ATTR_DURATION: 15},
|
||||
blocking=True,
|
||||
@@ -227,7 +223,7 @@ async def test_siren_turn_on_invalid_duration_does_not_set_volume(
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: SIREN_ENTITY_ID,
|
||||
@@ -249,7 +245,7 @@ async def test_siren_turn_on_with_volume(
|
||||
await init_entry(hass, ufp_with_siren, [])
|
||||
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID, ATTR_VOLUME_LEVEL: 0.75},
|
||||
blocking=True,
|
||||
@@ -272,7 +268,7 @@ async def test_siren_turn_off(
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -305,7 +301,7 @@ async def test_siren_turn_on_api_error(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -323,7 +319,7 @@ async def test_siren_turn_on_when_siren_gone(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -341,7 +337,7 @@ async def test_siren_turn_off_when_bootstrap_unavailable(
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
@@ -510,7 +506,7 @@ async def test_siren_turn_off_cancels_scheduled_timer(
|
||||
|
||||
# Manually turn off — must cancel the scheduled timer.
|
||||
await hass.services.async_call(
|
||||
SIREN_DOMAIN,
|
||||
Platform.SIREN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: SIREN_ENTITY_ID},
|
||||
blocking=True,
|
||||
|
||||
@@ -6,9 +6,6 @@ from itertools import chain
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
@@ -17,6 +14,7 @@ from homeassistant.const import (
|
||||
UnitOfBloodGlucoseConcentration,
|
||||
UnitOfConductivity,
|
||||
UnitOfDataRate,
|
||||
UnitOfDensity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -128,7 +126,7 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
|
||||
18.016,
|
||||
),
|
||||
CarbonMonoxideConcentrationConverter: (
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
1.16441,
|
||||
),
|
||||
@@ -164,22 +162,22 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
|
||||
InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8),
|
||||
MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473),
|
||||
MassVolumeConcentrationConverter: (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
1000,
|
||||
),
|
||||
NitrogenDioxideConcentrationConverter: (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.912503,
|
||||
),
|
||||
NitrogenMonoxideConcentrationConverter: (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.247389,
|
||||
),
|
||||
OzoneConcentrationConverter: (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.995417,
|
||||
),
|
||||
@@ -201,7 +199,7 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
|
||||
1.609343,
|
||||
),
|
||||
SulphurDioxideConcentrationConverter: (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
2.6633,
|
||||
),
|
||||
@@ -336,13 +334,13 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.16441,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
0.00116441,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
# PPM to other units
|
||||
(
|
||||
@@ -355,51 +353,51 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
1.16441,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
1164.41,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
# MICROGRAMS_PER_CUBIC_METER to other units
|
||||
(
|
||||
120000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
103056.5,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
(
|
||||
120000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
103.0565,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
(
|
||||
120000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
120,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
# MILLIGRAMS_PER_CUBIC_METER to other units
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
103056.5,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
103.0565,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
120000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
],
|
||||
NitrogenDioxideConcentrationConverter: [
|
||||
@@ -407,11 +405,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.912503,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
62.744976,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
@@ -419,11 +417,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
1912.503,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
0.062744976,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
@@ -445,11 +443,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.247389,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
96.200906,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
@@ -803,11 +801,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
1.995417,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
60.1378,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
@@ -815,11 +813,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
1995.417,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
0.0601378,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
@@ -1005,11 +1003,11 @@ _CONVERTED_VALUE: dict[
|
||||
1,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
2.6633,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
(
|
||||
120,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
45.056879,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
),
|
||||
@@ -1060,23 +1058,23 @@ _CONVERTED_VALUE: dict[
|
||||
# 1000 µg/m³ = 1 mg/m³
|
||||
(
|
||||
1000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
1,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
# 2 mg/m³ = 2000 µg/m³
|
||||
(
|
||||
2,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
2000,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
# 3 g/m³ = 3000 mg/m³
|
||||
(
|
||||
3,
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.GRAMS_PER_CUBIC_METER,
|
||||
3000,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
],
|
||||
VolumeConverter: [
|
||||
|
||||
Reference in New Issue
Block a user