mirror of
https://github.com/home-assistant/core.git
synced 2026-02-07 23:56:36 +01:00
Compare commits
36 Commits
2022.11.0b
...
2022.11.0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3409dea28c | ||
|
|
06d22d8249 | ||
|
|
a6e745b687 | ||
|
|
f6c094b017 | ||
|
|
9f54e332ec | ||
|
|
3aca376374 | ||
|
|
a5f209b219 | ||
|
|
0dbf0504ff | ||
|
|
95ce20638a | ||
|
|
b9132e78b4 | ||
|
|
e8f93d9c7f | ||
|
|
1efec8323a | ||
|
|
8c63a9ce5e | ||
|
|
9dff7ab6b9 | ||
|
|
d0ddbb5f58 | ||
|
|
f265c160d1 | ||
|
|
a2d432dfd6 | ||
|
|
c4bb225060 | ||
|
|
473490aee7 | ||
|
|
f9493bc313 | ||
|
|
1cc85f77e3 | ||
|
|
c2c57712d2 | ||
|
|
4684101a85 | ||
|
|
9b87f7f6f9 | ||
|
|
8965a1322c | ||
|
|
dfe399e370 | ||
|
|
0ac0e9c0d5 | ||
|
|
941512641b | ||
|
|
599c23c1d7 | ||
|
|
882ad31a99 | ||
|
|
356953c8bc | ||
|
|
19a5c87da6 | ||
|
|
d7e76fdf3a | ||
|
|
9b4f2df8f3 | ||
|
|
7046f5f19e | ||
|
|
3ddcc637da |
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
||||
image: homeassistant/{arch}-homeassistant
|
||||
shadow_repository: ghcr.io/home-assistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -127,6 +127,7 @@ class BluetoothManager:
|
||||
self._non_connectable_scanners: list[BaseHaScanner] = []
|
||||
self._connectable_scanners: list[BaseHaScanner] = []
|
||||
self._adapters: dict[str, AdapterDetails] = {}
|
||||
self._sources: set[str] = set()
|
||||
|
||||
@property
|
||||
def supports_passive_scan(self) -> bool:
|
||||
@@ -379,6 +380,7 @@ class BluetoothManager:
|
||||
if (
|
||||
(old_service_info := all_history.get(address))
|
||||
and source != old_service_info.source
|
||||
and old_service_info.source in self._sources
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_service_info, service_info
|
||||
)
|
||||
@@ -398,6 +400,7 @@ class BluetoothManager:
|
||||
# the old connectable advertisement
|
||||
or (
|
||||
source != old_connectable_service_info.source
|
||||
and old_connectable_service_info.source in self._sources
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_connectable_service_info, service_info
|
||||
)
|
||||
@@ -597,8 +600,10 @@ class BluetoothManager:
|
||||
def _unregister_scanner() -> None:
|
||||
self._advertisement_tracker.async_remove_source(scanner.source)
|
||||
scanners.remove(scanner)
|
||||
self._sources.remove(scanner.source)
|
||||
|
||||
scanners.append(scanner)
|
||||
self._sources.add(scanner.source)
|
||||
return _unregister_scanner
|
||||
|
||||
@hass_callback
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.19.1",
|
||||
"bleak-retry-connector==2.8.1",
|
||||
"bleak-retry-connector==2.8.2",
|
||||
"bluetooth-adapters==0.6.0",
|
||||
"bluetooth-auto-recovery==0.3.6",
|
||||
"dbus-fast==1.60.0"
|
||||
"dbus-fast==1.61.1"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
@@ -22,6 +23,7 @@ from homeassistant.const import (
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
@@ -34,29 +36,35 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import unit_conversion
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import EnergyManager, async_get_manager
|
||||
|
||||
SUPPORTED_STATE_CLASSES = [
|
||||
SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.MEASUREMENT,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
]
|
||||
VALID_ENERGY_UNITS = [
|
||||
}
|
||||
VALID_ENERGY_UNITS: set[str] = {
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
]
|
||||
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
|
||||
VALID_VOLUME_UNITS_WATER = [
|
||||
}
|
||||
VALID_ENERGY_UNITS_GAS = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
*VALID_ENERGY_UNITS,
|
||||
}
|
||||
VALID_VOLUME_UNITS_WATER = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
]
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -252,8 +260,24 @@ class EnergyCostSensor(SensorEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_cost(self) -> None: # noqa: C901
|
||||
def _update_cost(self) -> None:
|
||||
"""Update incurred costs."""
|
||||
if self._adapter.source_type == "grid":
|
||||
valid_units = VALID_ENERGY_UNITS
|
||||
default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
valid_units = VALID_ENERGY_UNITS_GAS
|
||||
# No conversion for gas.
|
||||
default_price_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
valid_units = VALID_VOLUME_UNITS_WATER
|
||||
if self.hass.config.units is METRIC_SYSTEM:
|
||||
default_price_unit = UnitOfVolume.CUBIC_METERS
|
||||
else:
|
||||
default_price_unit = UnitOfVolume.GALLONS
|
||||
|
||||
energy_state = self.hass.states.get(
|
||||
cast(str, self._config[self._adapter.stat_energy_key])
|
||||
)
|
||||
@@ -298,52 +322,27 @@ class EnergyCostSensor(SensorEntity):
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.WATT_HOUR}"
|
||||
):
|
||||
energy_price *= 1000.0
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.MEGA_WATT_HOUR}"
|
||||
):
|
||||
energy_price /= 1000.0
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.GIGA_JOULE}"
|
||||
):
|
||||
energy_price /= 1000 / 3.6
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
else:
|
||||
energy_price_state = None
|
||||
energy_price = cast(float, self._config["number_energy_price"])
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if self._adapter.source_type == "grid":
|
||||
if energy_unit not in VALID_ENERGY_UNITS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
if energy_unit not in VALID_ENERGY_UNITS_GAS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
if energy_unit not in VALID_VOLUME_UNITS_WATER:
|
||||
energy_unit = None
|
||||
|
||||
if energy_unit == UnitOfEnergy.WATT_HOUR:
|
||||
energy_price /= 1000
|
||||
elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR:
|
||||
energy_price *= 1000
|
||||
elif energy_unit == UnitOfEnergy.GIGA_JOULE:
|
||||
energy_price *= 1000 / 3.6
|
||||
|
||||
if energy_unit is None:
|
||||
if energy_unit is None or energy_unit not in valid_units:
|
||||
if not self._wrong_unit_reported:
|
||||
self._wrong_unit_reported = True
|
||||
_LOGGER.warning(
|
||||
@@ -373,10 +372,30 @@ class EnergyCostSensor(SensorEntity):
|
||||
energy_state_copy = copy.copy(energy_state)
|
||||
energy_state_copy.state = "0.0"
|
||||
self._reset(energy_state_copy)
|
||||
|
||||
# Update with newly incurred cost
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
cur_value = cast(float, self._attr_native_value)
|
||||
self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price
|
||||
|
||||
if energy_price_unit is None:
|
||||
converted_energy_price = energy_price
|
||||
else:
|
||||
if self._adapter.source_type == "grid":
|
||||
converter: Callable[
|
||||
[float, str, str], float
|
||||
] = unit_conversion.EnergyConverter.convert
|
||||
elif self._adapter.source_type in ("gas", "water"):
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
converted_energy_price = converter(
|
||||
energy_price,
|
||||
energy_unit,
|
||||
energy_price_unit,
|
||||
)
|
||||
|
||||
self._attr_native_value = (
|
||||
cur_value + (energy - old_energy_value) * converted_energy_price
|
||||
)
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ import logging
|
||||
from typing import Any, TypeVar, cast
|
||||
import uuid
|
||||
|
||||
from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError
|
||||
from aioesphomeapi import (
|
||||
ESP_CONNECTION_ERROR_DESCRIPTION,
|
||||
ESPHOME_GATT_ERRORS,
|
||||
BLEConnectionError,
|
||||
)
|
||||
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
||||
import async_timeout
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
@@ -207,7 +211,9 @@ class ESPHomeClient(BaseBleakClient):
|
||||
human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error]
|
||||
except (KeyError, ValueError):
|
||||
ble_connection_error_name = str(error)
|
||||
human_error = f"Unknown error code {error}"
|
||||
human_error = ESPHOME_GATT_ERRORS.get(
|
||||
error, f"Unknown error code {error}"
|
||||
)
|
||||
connected_future.set_exception(
|
||||
BleakError(
|
||||
f"Error {ble_connection_error_name} while connecting: {human_error}"
|
||||
|
||||
@@ -6,6 +6,7 @@ import datetime
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
from aioesphomeapi import BluetoothLEAdvertisement
|
||||
from bleak.backends.device import BLEDevice
|
||||
@@ -23,6 +24,15 @@ from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
TWO_CHAR = re.compile("..")
|
||||
|
||||
# The maximum time between advertisements for a device to be considered
|
||||
# stale when the advertisement tracker can determine the interval for
|
||||
# connectable devices.
|
||||
#
|
||||
# BlueZ uses 180 seconds by default but we give it a bit more time
|
||||
# to account for the esp32's bluetooth stack being a bit slower
|
||||
# than BlueZ's.
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195
|
||||
|
||||
|
||||
class ESPHomeScanner(BaseHaScanner):
|
||||
"""Scanner for esphome."""
|
||||
@@ -45,8 +55,12 @@ class ESPHomeScanner(BaseHaScanner):
|
||||
self._connector = connector
|
||||
self._connectable = connectable
|
||||
self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id}
|
||||
self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
if connectable:
|
||||
self._details["connector"] = connector
|
||||
self._fallback_seconds = (
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_setup(self) -> CALLBACK_TYPE:
|
||||
@@ -61,7 +75,7 @@ class ESPHomeScanner(BaseHaScanner):
|
||||
expired = [
|
||||
address
|
||||
for address, timestamp in self._discovered_device_timestamps.items()
|
||||
if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
if now - timestamp > self._fallback_seconds
|
||||
]
|
||||
for address in expired:
|
||||
del self._discovered_device_advertisement_datas[address]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==11.4.1"],
|
||||
"requirements": ["aioesphomeapi==11.4.2"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
||||
@@ -79,6 +79,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity):
|
||||
"type",
|
||||
"responder_mode",
|
||||
"can_respond_until",
|
||||
"task_ids",
|
||||
):
|
||||
if data.get(value):
|
||||
attr[value] = data[value]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20221027.0"],
|
||||
"requirements": ["home-assistant-frontend==20221102.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==2.2.0", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==2.2.2", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -77,6 +77,7 @@ from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F4
|
||||
from .handler import HassIO, HassioAPIError, api_data
|
||||
from .http import HassIOView
|
||||
from .ingress import async_setup_ingress_view
|
||||
from .repairs import SupervisorRepairs
|
||||
from .websocket_api import async_load_websocket_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -103,6 +104,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_SUPERVISOR_REPAIRS = "supervisor_repairs"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
@@ -758,6 +760,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||
)
|
||||
|
||||
# Start listening for problems with supervisor and making repairs
|
||||
hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio)
|
||||
await repairs.setup()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -11,19 +11,26 @@ ATTR_CONFIG = "config"
|
||||
ATTR_DATA = "data"
|
||||
ATTR_DISCOVERY = "discovery"
|
||||
ATTR_ENABLE = "enable"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_FOLDERS = "folders"
|
||||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_HOMEASSISTANT = "homeassistant"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_PANELS = "panels"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_TITLE = "title"
|
||||
ATTR_UNHEALTHY = "unhealthy"
|
||||
ATTR_UNHEALTHY_REASONS = "unhealthy_reasons"
|
||||
ATTR_UNSUPPORTED = "unsupported"
|
||||
ATTR_UNSUPPORTED_REASONS = "unsupported_reasons"
|
||||
ATTR_UPDATE_KEY = "update_key"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_WS_EVENT = "event"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
|
||||
X_AUTH_TOKEN = "X-Supervisor-Token"
|
||||
X_INGRESS_PATH = "X-Ingress-Path"
|
||||
@@ -38,6 +45,11 @@ WS_TYPE_EVENT = "supervisor/event"
|
||||
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||
EVENT_SUPERVISOR_UPDATE = "supervisor_update"
|
||||
EVENT_HEALTH_CHANGED = "health_changed"
|
||||
EVENT_SUPPORTED_CHANGED = "supported_changed"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
@@ -51,7 +63,6 @@ ATTR_STARTED = "started"
|
||||
ATTR_URL = "url"
|
||||
ATTR_REPOSITORY = "repository"
|
||||
|
||||
|
||||
DATA_KEY_ADDONS = "addons"
|
||||
DATA_KEY_OS = "os"
|
||||
DATA_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
@@ -190,6 +190,14 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command(f"/discovery/{uuid}", method="get")
|
||||
|
||||
@api_data
|
||||
def get_resolution_info(self):
|
||||
"""Return data for Supervisor resolution center.
|
||||
|
||||
This method return a coroutine.
|
||||
"""
|
||||
return self.send_command("/resolution/info", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(self, http_config, refresh_token):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
|
||||
185
homeassistant/components/hassio/repairs.py
Normal file
185
homeassistant/components/hassio/repairs.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Supervisor events monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_UNHEALTHY,
|
||||
ATTR_UNHEALTHY_REASONS,
|
||||
ATTR_UNSUPPORTED,
|
||||
ATTR_UNSUPPORTED_REASONS,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
DOMAIN,
|
||||
EVENT_HEALTH_CHANGED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .handler import HassIO
|
||||
|
||||
ISSUE_ID_UNHEALTHY = "unhealthy_system"
|
||||
ISSUE_ID_UNSUPPORTED = "unsupported_system"
|
||||
|
||||
INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy"
|
||||
INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"connectivity_check",
|
||||
"content_trust",
|
||||
"dbus",
|
||||
"dns_server",
|
||||
"docker_configuration",
|
||||
"docker_version",
|
||||
"cgroup_version",
|
||||
"job_conditions",
|
||||
"lxc",
|
||||
"network_manager",
|
||||
"os",
|
||||
"os_agent",
|
||||
"restart_policy",
|
||||
"software",
|
||||
"source_mods",
|
||||
"supervisor_version",
|
||||
"systemd",
|
||||
"systemd_journal",
|
||||
"systemd_resolved",
|
||||
}
|
||||
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
|
||||
# provides no additional information beyond the unhealthy one then skip that repair.
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"privileged",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
||||
class SupervisorRepairs:
|
||||
"""Create repairs from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
"""Initialize supervisor repairs."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
"""Get unhealthy reasons. Returns empty set if system is healthy."""
|
||||
return self._unhealthy_reasons
|
||||
|
||||
@unhealthy_reasons.setter
|
||||
def unhealthy_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unhealthy reasons. Create or delete repairs as necessary."""
|
||||
for unhealthy in reasons - self.unhealthy_reasons:
|
||||
if unhealthy in UNHEALTHY_REASONS:
|
||||
translation_key = f"unhealthy_{unhealthy}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unhealthy"
|
||||
translation_placeholders = {"reason": unhealthy}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNHEALTHY}_{unhealthy}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
for fixed in self.unhealthy_reasons - reasons:
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}")
|
||||
|
||||
self._unhealthy_reasons = reasons
|
||||
|
||||
@property
|
||||
def unsupported_reasons(self) -> set[str]:
|
||||
"""Get unsupported reasons. Returns empty set if system is supported."""
|
||||
return self._unsupported_reasons
|
||||
|
||||
@unsupported_reasons.setter
|
||||
def unsupported_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unsupported reasons. Create or delete repairs as necessary."""
|
||||
for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons:
|
||||
if unsupported in UNSUPPORTED_REASONS:
|
||||
translation_key = f"unsupported_{unsupported}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unsupported"
|
||||
translation_placeholders = {"reason": unsupported}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNSUPPORTED}_{unsupported}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR):
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}")
|
||||
|
||||
self._unsupported_reasons = reasons
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
await self.update()
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs
|
||||
)
|
||||
|
||||
async def update(self) -> None:
|
||||
"""Update repairs from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
@callback
|
||||
def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None:
|
||||
"""Create repairs from supervisor events."""
|
||||
if ATTR_WS_EVENT not in event:
|
||||
return
|
||||
|
||||
if (
|
||||
event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
|
||||
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
|
||||
):
|
||||
self._hass.async_create_task(self.update())
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED:
|
||||
self.unhealthy_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_HEALTHY]
|
||||
else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS])
|
||||
)
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED:
|
||||
self.unsupported_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_SUPPORTED]
|
||||
else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
|
||||
)
|
||||
@@ -15,5 +15,115 @@
|
||||
"update_channel": "Update Channel",
|
||||
"version_api": "Version API"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"title": "Unhealthy system - Docker misconfigured",
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"title": "Unhealthy system - Not privileged",
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"title": "Unhealthy system - Untrusted code",
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Unsupported system - {reason}",
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"title": "Unsupported system - AppArmor issues",
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"title": "Unsupported system - CGroup version",
|
||||
"description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this."
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"title": "Unsupported system - Connectivity check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"title": "Unsupported system - Content-trust check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"title": "Unsupported system - D-Bus issues",
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"title": "Unsupported system - DNS server issues",
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"title": "Unsupported system - Docker misconfigured",
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"title": "Unsupported system - Docker version",
|
||||
"description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this."
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"title": "Unsupported system - Protections disabled",
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"title": "Unsupported system - LXC detected",
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"title": "Unsupported system - Network Manager issues",
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_os": {
|
||||
"title": "Unsupported system - Operating System",
|
||||
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this."
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"title": "Unsupported system - OS-Agent issues",
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"title": "Unsupported system - Container restart policy",
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_software": {
|
||||
"title": "Unsupported system - Unsupported software",
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"title": "Unsupported system - Supervisor source modifications",
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"title": "Unsupported system - Supervisor version",
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"title": "Unsupported system - Systemd issues",
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"title": "Unsupported system - Systemd Journal issues",
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"title": "Unsupported system - Systemd-Resolved issues",
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.",
|
||||
"title": "Sistema no saludable - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.",
|
||||
"title": "Sistema no compatible - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Versi\u00f3 de l'agent",
|
||||
|
||||
@@ -1,4 +1,114 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - {reason}"
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Docker misconfigured"
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Not privileged"
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Setup failed"
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Supervisor update failed"
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Untrusted code"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - {reason}"
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - AppArmor issues"
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this.",
|
||||
"title": "Unsupported system - CGroup version"
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Connectivity check disabled"
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Content-trust check disabled"
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - D-Bus issues"
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - DNS server issues"
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Docker misconfigured"
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this.",
|
||||
"title": "Unsupported system - Docker version"
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Protections disabled"
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - LXC detected"
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Network Manager issues"
|
||||
},
|
||||
"unsupported_os": {
|
||||
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this.",
|
||||
"title": "Unsupported system - Operating System"
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - OS-Agent issues"
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Container restart policy"
|
||||
},
|
||||
"unsupported_software": {
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Unsupported software"
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Supervisor source modifications"
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Supervisor version"
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd issues"
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd Journal issues"
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd-Resolved issues"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Agent Version",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.",
|
||||
"title": "Sistema en mal estado: {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.",
|
||||
"title": "Sistema no compatible: {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Versi\u00f3n del agente",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.",
|
||||
"title": "Vigane s\u00fcsteem \u2013 {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.",
|
||||
"title": "Toetamata s\u00fcsteem \u2013 {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Agendi versioon",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.",
|
||||
"title": "Rendellenes \u00e1llapot \u2013 {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.",
|
||||
"title": "Nem t\u00e1mogatott rendszer \u2013 {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "\u00dcgyn\u00f6k verzi\u00f3",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.",
|
||||
"title": "Sistema insalubre - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.",
|
||||
"title": "Sistema n\u00e3o suportado - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Vers\u00e3o do Agent",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.",
|
||||
"title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.",
|
||||
"title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==2.2.9"],
|
||||
"requirements": ["aiohomekit==2.2.13"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -52,6 +52,7 @@ ABBREVIATIONS = {
|
||||
"e": "encoding",
|
||||
"en": "enabled_by_default",
|
||||
"ent_cat": "entity_category",
|
||||
"ent_pic": "entity_picture",
|
||||
"err_t": "error_topic",
|
||||
"err_tpl": "error_template",
|
||||
"fanspd_t": "fan_speed_topic",
|
||||
@@ -169,6 +170,8 @@ ABBREVIATIONS = {
|
||||
"pr_mode_val_tpl": "preset_mode_value_template",
|
||||
"pr_modes": "preset_modes",
|
||||
"r_tpl": "red_template",
|
||||
"rel_s": "release_summary",
|
||||
"rel_u": "release_url",
|
||||
"ret": "retain",
|
||||
"rgb_cmd_tpl": "rgb_command_template",
|
||||
"rgb_cmd_t": "rgb_command_topic",
|
||||
@@ -242,6 +245,7 @@ ABBREVIATIONS = {
|
||||
"tilt_opt": "tilt_optimistic",
|
||||
"tilt_status_t": "tilt_status_topic",
|
||||
"tilt_status_tpl": "tilt_status_template",
|
||||
"tit": "title",
|
||||
"t": "topic",
|
||||
"uniq_id": "unique_id",
|
||||
"unit_of_meas": "unit_of_measurement",
|
||||
|
||||
@@ -271,8 +271,8 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
)
|
||||
elif self.device_class == SensorDeviceClass.DATE:
|
||||
payload = payload.date()
|
||||
if payload != "":
|
||||
self._state = payload
|
||||
|
||||
self._state = payload
|
||||
|
||||
def _update_last_reset(msg):
|
||||
payload = self._last_reset_template(msg.payload)
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
@@ -30,6 +31,7 @@ from .const import (
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
)
|
||||
from .debug_info import log_messages
|
||||
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
|
||||
@@ -40,20 +42,28 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Update"
|
||||
|
||||
CONF_ENTITY_PICTURE = "entity_picture"
|
||||
CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
|
||||
CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
|
||||
CONF_PAYLOAD_INSTALL = "payload_install"
|
||||
CONF_RELEASE_SUMMARY = "release_summary"
|
||||
CONF_RELEASE_URL = "release_url"
|
||||
CONF_TITLE = "title"
|
||||
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
|
||||
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_INSTALL): cv.string,
|
||||
vol.Optional(CONF_RELEASE_SUMMARY): cv.string,
|
||||
vol.Optional(CONF_RELEASE_URL): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_TITLE): cv.string,
|
||||
},
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -99,10 +109,22 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
"""Initialize the MQTT update."""
|
||||
self._config = config
|
||||
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY)
|
||||
self._attr_release_url = self._config.get(CONF_RELEASE_URL)
|
||||
self._attr_title = self._config.get(CONF_TITLE)
|
||||
self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE)
|
||||
|
||||
UpdateEntity.__init__(self)
|
||||
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the entity picture to use in the frontend."""
|
||||
if self._entity_picture is not None:
|
||||
return self._entity_picture
|
||||
|
||||
return super().entity_picture
|
||||
|
||||
@staticmethod
|
||||
def config_schema() -> vol.Schema:
|
||||
"""Return the config schema."""
|
||||
@@ -138,15 +160,59 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_installed_version_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle receiving installed version via MQTT."""
|
||||
installed_version = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
|
||||
def handle_state_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle receiving state message via MQTT."""
|
||||
payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
|
||||
|
||||
if isinstance(installed_version, str) and installed_version != "":
|
||||
self._attr_installed_version = installed_version
|
||||
if not payload or payload == PAYLOAD_EMPTY_JSON:
|
||||
_LOGGER.debug(
|
||||
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||
payload,
|
||||
msg.topic,
|
||||
)
|
||||
return
|
||||
|
||||
json_payload = {}
|
||||
try:
|
||||
json_payload = json_loads(payload)
|
||||
_LOGGER.debug(
|
||||
"JSON payload detected after processing payload '%s' on topic %s",
|
||||
json_payload,
|
||||
msg.topic,
|
||||
)
|
||||
except JSON_DECODE_EXCEPTIONS:
|
||||
_LOGGER.debug(
|
||||
"No valid (JSON) payload detected after processing payload '%s' on topic %s",
|
||||
payload,
|
||||
msg.topic,
|
||||
)
|
||||
json_payload["installed_version"] = payload
|
||||
|
||||
if "installed_version" in json_payload:
|
||||
self._attr_installed_version = json_payload["installed_version"]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received)
|
||||
if "latest_version" in json_payload:
|
||||
self._attr_latest_version = json_payload["latest_version"]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_TITLE in json_payload and not self._attr_title:
|
||||
self._attr_title = json_payload[CONF_TITLE]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary:
|
||||
self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_RELEASE_URL in json_payload and not self._attr_release_url:
|
||||
self._attr_release_url = json_payload[CONF_RELEASE_URL]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture:
|
||||
self._entity_picture = json_payload[CONF_ENTITY_PICTURE]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received)
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"manufacturer_id": 220
|
||||
}
|
||||
],
|
||||
"requirements": ["oralb-ble==0.9.0"],
|
||||
"requirements": ["oralb-ble==0.10.0"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, device_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import (
|
||||
CONF_COAP_PORT,
|
||||
@@ -113,13 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Shelly block based device from a config entry."""
|
||||
temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F"
|
||||
|
||||
options = aioshelly.common.ConnectionOptions(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_USERNAME),
|
||||
entry.data.get(CONF_PASSWORD),
|
||||
temperature_unit,
|
||||
)
|
||||
|
||||
coap_context = await get_coap_context(hass)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==4.1.1"],
|
||||
"requirements": ["aioshelly==4.1.2"],
|
||||
"dependencies": ["http"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.components.http import current_request
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, Unauthorized
|
||||
|
||||
@@ -137,6 +138,13 @@ class ActiveConnection:
|
||||
err_message = "Unknown error"
|
||||
log_handler = self.logger.exception
|
||||
|
||||
log_handler("Error handling message: %s (%s)", err_message, code)
|
||||
|
||||
self.send_message(messages.error_message(msg["id"], code, err_message))
|
||||
|
||||
if code:
|
||||
err_message += f" ({code})"
|
||||
if request := current_request.get():
|
||||
err_message += f" from {request.remote}"
|
||||
if user_agent := request.headers.get("user-agent"):
|
||||
err_message += f" ({user_agent})"
|
||||
|
||||
log_handler("Error handling message: %s", err_message)
|
||||
|
||||
@@ -94,6 +94,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.title, push_lock
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_device_unavailable(
|
||||
_service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
) -> None:
|
||||
"""Handle device not longer being seen by the bluetooth stack."""
|
||||
push_lock.reset_advertisement_state()
|
||||
|
||||
entry.async_on_unload(
|
||||
bluetooth.async_track_unavailable(
|
||||
hass, _async_device_unavailable, push_lock.address
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
return True
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Yale Access Bluetooth",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"requirements": ["yalexs-ble==1.9.4"],
|
||||
"requirements": ["yalexs-ble==1.9.5"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
||||
@@ -39,13 +39,13 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
GasSensor(coordinator, device),
|
||||
PowerMeterSensor(
|
||||
EnergyMeterSensor(
|
||||
coordinator, device, "low", SensorStateClass.TOTAL_INCREASING
|
||||
),
|
||||
PowerMeterSensor(
|
||||
EnergyMeterSensor(
|
||||
coordinator, device, "high", SensorStateClass.TOTAL_INCREASING
|
||||
),
|
||||
PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL),
|
||||
EnergyMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL),
|
||||
CurrentPowerSensor(coordinator, device),
|
||||
DeliveryMeterSensor(coordinator, device, "low"),
|
||||
DeliveryMeterSensor(coordinator, device, "high"),
|
||||
@@ -68,10 +68,6 @@ class YoulessBaseSensor(CoordinatorEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Create the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._device_group = device_group
|
||||
self._sensor_id = sensor_id
|
||||
|
||||
self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{device}_{device_group}")},
|
||||
@@ -149,10 +145,10 @@ class DeliveryMeterSensor(YoulessBaseSensor):
|
||||
) -> None:
|
||||
"""Instantiate a delivery meter sensor."""
|
||||
super().__init__(
|
||||
coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}"
|
||||
coordinator, device, "delivery", "Energy delivery", f"delivery_{dev_type}"
|
||||
)
|
||||
self._type = dev_type
|
||||
self._attr_name = f"Power delivery {dev_type}"
|
||||
self._attr_name = f"Energy delivery {dev_type}"
|
||||
|
||||
@property
|
||||
def get_sensor(self) -> YoulessSensor | None:
|
||||
@@ -163,7 +159,7 @@ class DeliveryMeterSensor(YoulessBaseSensor):
|
||||
return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None)
|
||||
|
||||
|
||||
class PowerMeterSensor(YoulessBaseSensor):
|
||||
class EnergyMeterSensor(YoulessBaseSensor):
|
||||
"""The Youless low meter value sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||
@@ -177,13 +173,13 @@ class PowerMeterSensor(YoulessBaseSensor):
|
||||
dev_type: str,
|
||||
state_class: SensorStateClass,
|
||||
) -> None:
|
||||
"""Instantiate a power meter sensor."""
|
||||
"""Instantiate a energy meter sensor."""
|
||||
super().__init__(
|
||||
coordinator, device, "power", "Power usage", f"power_{dev_type}"
|
||||
coordinator, device, "power", "Energy usage", f"power_{dev_type}"
|
||||
)
|
||||
self._device = device
|
||||
self._type = dev_type
|
||||
self._attr_name = f"Power {dev_type}"
|
||||
self._attr_name = f"Energy {dev_type}"
|
||||
self._attr_state_class = state_class
|
||||
|
||||
@property
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.39.3"],
|
||||
"requirements": ["zeroconf==0.39.4"],
|
||||
"dependencies": ["network", "api"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
|
||||
@@ -156,7 +156,7 @@ class BasicChannel(ZigbeeChannel):
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize Basic channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
if is_hue_motion_sensor(self):
|
||||
if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2:
|
||||
self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name
|
||||
self.ZCL_INIT_ATTRS.copy()
|
||||
)
|
||||
|
||||
@@ -59,6 +59,41 @@ class PhillipsRemote(ZigbeeChannel):
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
class TuyaChannel(ZigbeeChannel):
|
||||
"""Channel for the Tuya manufacturer Zigbee cluster."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize TuyaChannel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
|
||||
if self.cluster.endpoint.manufacturer in (
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
):
|
||||
self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name
|
||||
"backlight_mode": True,
|
||||
"power_on_state": True,
|
||||
}
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0)
|
||||
class OppleRemote(ZigbeeChannel):
|
||||
|
||||
@@ -33,6 +33,7 @@ PHILLIPS_REMOTE_CLUSTER = 0xFC00
|
||||
SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02
|
||||
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000
|
||||
SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45
|
||||
TUYA_MANUFACTURER_CLUSTER = 0xEF00
|
||||
VOC_LEVEL_CLUSTER = 0x042E
|
||||
|
||||
REMOTE_DEVICE_TYPES = {
|
||||
|
||||
@@ -93,7 +93,7 @@ DEVICE_ACTION_SCHEMAS = {
|
||||
),
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"zigpy-deconz==0.19.0",
|
||||
"zigpy==0.51.5",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.1"
|
||||
],
|
||||
"usb": [
|
||||
|
||||
@@ -240,6 +240,27 @@ class TuyaPowerOnState(types.enum8):
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
|
||||
)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
},
|
||||
)
|
||||
class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"):
|
||||
"""Representation of a ZHA power on state select entity."""
|
||||
|
||||
@@ -248,6 +269,44 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat
|
||||
_attr_name = "Power on state"
|
||||
|
||||
|
||||
class MoesBacklightMode(types.enum8):
|
||||
"""MOES switch backlight mode enum."""
|
||||
|
||||
Off = 0x00
|
||||
LightWhenOn = 0x01
|
||||
LightWhenOff = 0x02
|
||||
Freeze = 0x03
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
},
|
||||
)
|
||||
class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"):
|
||||
"""Moes devices have a different backlight mode select options."""
|
||||
|
||||
_select_attr = "backlight_mode"
|
||||
_enum = MoesBacklightMode
|
||||
_attr_name = "Backlight mode"
|
||||
|
||||
|
||||
class AqaraMotionSensitivities(types.enum8):
|
||||
"""Aqara motion sensitivities."""
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "0b4"
|
||||
PATCH_VERSION: Final = "0b7"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
||||
@@ -10,18 +10,18 @@ atomicwrites-homeassistant==1.4.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.9.0
|
||||
bcrypt==3.1.7
|
||||
bleak-retry-connector==2.8.1
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak==0.19.1
|
||||
bluetooth-adapters==0.6.0
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
cryptography==38.0.1
|
||||
dbus-fast==1.60.0
|
||||
dbus-fast==1.61.1
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.56.0
|
||||
home-assistant-bluetooth==1.6.0
|
||||
home-assistant-frontend==20221027.0
|
||||
home-assistant-frontend==20221102.0
|
||||
httpx==0.23.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
@@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0
|
||||
voluptuous-serialize==2.5.0
|
||||
voluptuous==0.13.1
|
||||
yarl==1.8.1
|
||||
zeroconf==0.39.3
|
||||
zeroconf==0.39.4
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.11.0b4"
|
||||
version = "2022.11.0b7"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -153,7 +153,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.1
|
||||
aioesphomeapi==11.4.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -171,7 +171,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.9
|
||||
aiohomekit==2.2.13
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -255,7 +255,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==4.1.1
|
||||
aioshelly==4.1.2
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -413,7 +413,7 @@ bimmer_connected==0.10.4
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.1
|
||||
bleak-retry-connector==2.8.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
@@ -540,7 +540,7 @@ datadog==0.15.0
|
||||
datapoint==0.9.8
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==1.60.0
|
||||
dbus-fast==1.61.1
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.6.3
|
||||
@@ -725,7 +725,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.0
|
||||
gcal-sync==2.2.2
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -868,7 +868,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221027.0
|
||||
home-assistant-frontend==20221102.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.9.0
|
||||
oralb-ble==0.10.0
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@@ -2577,7 +2577,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.9.4
|
||||
yalexs-ble==1.9.5
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.6
|
||||
@@ -2604,7 +2604,7 @@ zamg==0.1.1
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.39.3
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
@@ -2622,7 +2622,7 @@ zigpy-deconz==0.19.0
|
||||
zigpy-xbee==0.16.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.10.2
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.1
|
||||
|
||||
@@ -140,7 +140,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.1
|
||||
aioesphomeapi==11.4.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -155,7 +155,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.9
|
||||
aiohomekit==2.2.13
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -230,7 +230,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==4.1.1
|
||||
aioshelly==4.1.2
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -337,7 +337,7 @@ bellows==0.34.2
|
||||
bimmer_connected==0.10.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.1
|
||||
bleak-retry-connector==2.8.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
@@ -420,7 +420,7 @@ datadog==0.15.0
|
||||
datapoint==0.9.8
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==1.60.0
|
||||
dbus-fast==1.61.1
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.6.3
|
||||
@@ -541,7 +541,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.0
|
||||
gcal-sync==2.2.2
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@@ -648,7 +648,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221027.0
|
||||
home-assistant-frontend==20221102.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -883,7 +883,7 @@ open-meteo==0.2.1
|
||||
openerz-api==0.1.0
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.9.0
|
||||
oralb-ble==0.10.0
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.2.0
|
||||
@@ -1787,7 +1787,7 @@ xmltodict==0.13.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.9.4
|
||||
yalexs-ble==1.9.5
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.6
|
||||
@@ -1805,7 +1805,7 @@ youless-api==0.16
|
||||
zamg==0.1.1
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.39.3
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
@@ -1817,7 +1817,7 @@ zigpy-deconz==0.19.0
|
||||
zigpy-xbee==0.16.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.10.2
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.1
|
||||
|
||||
@@ -5,11 +5,14 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from bleak.backends.scanner import BLEDevice
|
||||
from bluetooth_adapters import AdvertisementHistory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import models
|
||||
from homeassistant.components.bluetooth.manager import (
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
@@ -20,8 +23,28 @@ from . import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def register_hci0_scanner(hass: HomeAssistant) -> None:
|
||||
"""Register an hci0 scanner."""
|
||||
cancel = bluetooth.async_register_scanner(
|
||||
hass, models.BaseHaScanner(hass, "hci0"), True
|
||||
)
|
||||
yield
|
||||
cancel()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def register_hci1_scanner(hass: HomeAssistant) -> None:
|
||||
"""Register an hci1 scanner."""
|
||||
cancel = bluetooth.async_register_scanner(
|
||||
hass, models.BaseHaScanner(hass, "hci1"), True
|
||||
)
|
||||
yield
|
||||
cancel()
|
||||
|
||||
|
||||
async def test_advertisements_do_not_switch_adapters_for_no_reason(
|
||||
hass, enable_bluetooth
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test we only switch adapters when needed."""
|
||||
|
||||
@@ -68,7 +91,9 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason(
|
||||
)
|
||||
|
||||
|
||||
async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth):
|
||||
async def test_switching_adapters_based_on_rssi(
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test switching adapters based on rssi."""
|
||||
|
||||
address = "44:44:33:11:23:45"
|
||||
@@ -122,7 +147,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth):
|
||||
)
|
||||
|
||||
|
||||
async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth):
|
||||
async def test_switching_adapters_based_on_zero_rssi(
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test switching adapters based on zero rssi."""
|
||||
|
||||
address = "44:44:33:11:23:45"
|
||||
@@ -176,7 +203,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth):
|
||||
)
|
||||
|
||||
|
||||
async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
|
||||
async def test_switching_adapters_based_on_stale(
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test switching adapters based on the previous advertisement being stale."""
|
||||
|
||||
address = "44:44:33:11:23:41"
|
||||
@@ -256,7 +285,7 @@ async def test_restore_history_from_dbus(hass, one_adapter):
|
||||
|
||||
|
||||
async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
|
||||
hass, enable_bluetooth
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test switching adapters based on rssi from connectable to non connectable."""
|
||||
|
||||
@@ -339,7 +368,7 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
|
||||
|
||||
|
||||
async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable(
|
||||
hass, enable_bluetooth
|
||||
hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner
|
||||
):
|
||||
"""Test we can still get a connectable BLEDevice when the best path is non-connectable.
|
||||
|
||||
@@ -384,3 +413,54 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_
|
||||
bluetooth.async_ble_device_from_address(hass, address, True)
|
||||
is switchbot_device_poor_signal
|
||||
)
|
||||
|
||||
|
||||
async def test_switching_adapters_when_one_goes_away(
|
||||
hass, enable_bluetooth, register_hci0_scanner
|
||||
):
|
||||
"""Test switching adapters when one goes away."""
|
||||
cancel_hci2 = bluetooth.async_register_scanner(
|
||||
hass, models.BaseHaScanner(hass, "hci2"), True
|
||||
)
|
||||
|
||||
address = "44:44:33:11:23:45"
|
||||
|
||||
switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal")
|
||||
switchbot_adv_good_signal = generate_advertisement_data(
|
||||
local_name="wohand_good_signal", service_uuids=[], rssi=-60
|
||||
)
|
||||
inject_advertisement_with_source(
|
||||
hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci2"
|
||||
)
|
||||
|
||||
assert (
|
||||
bluetooth.async_ble_device_from_address(hass, address)
|
||||
is switchbot_device_good_signal
|
||||
)
|
||||
|
||||
switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal")
|
||||
switchbot_adv_poor_signal = generate_advertisement_data(
|
||||
local_name="wohand_poor_signal", service_uuids=[], rssi=-100
|
||||
)
|
||||
inject_advertisement_with_source(
|
||||
hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0"
|
||||
)
|
||||
|
||||
# We want to prefer the good signal when we have options
|
||||
assert (
|
||||
bluetooth.async_ble_device_from_address(hass, address)
|
||||
is switchbot_device_good_signal
|
||||
)
|
||||
|
||||
cancel_hci2()
|
||||
|
||||
inject_advertisement_with_source(
|
||||
hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0"
|
||||
)
|
||||
|
||||
# Now that hci2 is gone, we should prefer the poor signal
|
||||
# since no poor signal is better than no signal
|
||||
assert (
|
||||
bluetooth.async_ble_device_from_address(hass, address)
|
||||
is switchbot_device_poor_signal
|
||||
)
|
||||
|
||||
@@ -19,11 +19,13 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_GALLONS,
|
||||
UnitOfEnergy,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
@@ -832,7 +834,10 @@ async def test_cost_sensor_handle_price_units(
|
||||
assert state.state == "20.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
|
||||
@pytest.mark.parametrize(
|
||||
"unit",
|
||||
(VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
|
||||
)
|
||||
async def test_cost_sensor_handle_gas(
|
||||
setup_integration, hass, hass_storage, unit
|
||||
) -> None:
|
||||
@@ -933,13 +938,22 @@ async def test_cost_sensor_handle_gas_kwh(
|
||||
assert state.state == "50.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
|
||||
@pytest.mark.parametrize(
|
||||
"unit_system,usage_unit,growth",
|
||||
(
|
||||
# 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3:
|
||||
(US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974),
|
||||
(US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0),
|
||||
(METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0),
|
||||
),
|
||||
)
|
||||
async def test_cost_sensor_handle_water(
|
||||
setup_integration, hass, hass_storage, unit
|
||||
setup_integration, hass, hass_storage, unit_system, usage_unit, growth
|
||||
) -> None:
|
||||
"""Test water cost price from sensor entity."""
|
||||
hass.config.units = unit_system
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: unit,
|
||||
ATTR_UNIT_OF_MEASUREMENT: usage_unit,
|
||||
ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
@@ -981,7 +995,7 @@ async def test_cost_sensor_handle_water(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.water_consumption_cost")
|
||||
assert state.state == "50.0"
|
||||
assert float(state.state) == pytest.approx(growth)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state_class", [None])
|
||||
|
||||
@@ -133,6 +133,19 @@ def mock_all(aioclient_mock, request):
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request):
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
|
||||
@@ -183,6 +183,19 @@ def mock_all(aioclient_mock, request, os_info):
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_api_ping(hass, aioclient_mock):
|
||||
@@ -191,7 +204,7 @@ async def test_setup_api_ping(hass, aioclient_mock):
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
|
||||
assert hass.components.hassio.is_hassio()
|
||||
|
||||
@@ -230,7 +243,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock):
|
||||
)
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||
assert aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||
@@ -246,7 +259,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
|
||||
)
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||
@@ -258,7 +271,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag
|
||||
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
|
||||
@@ -325,7 +338,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage
|
||||
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
|
||||
@@ -339,7 +352,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock):
|
||||
result = await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
|
||||
|
||||
with patch("homeassistant.util.dt.set_default_time_zone"):
|
||||
@@ -356,7 +369,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock):
|
||||
result = await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
|
||||
|
||||
|
||||
@@ -426,14 +439,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 9
|
||||
assert aioclient_mock.call_count == 10
|
||||
assert aioclient_mock.mock_calls[-1][2] == "test"
|
||||
|
||||
await hass.services.async_call("hassio", "host_shutdown", {})
|
||||
await hass.services.async_call("hassio", "host_reboot", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 11
|
||||
assert aioclient_mock.call_count == 12
|
||||
|
||||
await hass.services.async_call("hassio", "backup_full", {})
|
||||
await hass.services.async_call(
|
||||
@@ -448,7 +461,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 13
|
||||
assert aioclient_mock.call_count == 14
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"homeassistant": True,
|
||||
"addons": ["test"],
|
||||
@@ -472,7 +485,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"addons": ["test"],
|
||||
"folders": ["ssl"],
|
||||
@@ -491,12 +504,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
||||
await hass.services.async_call("homeassistant", "stop")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 5
|
||||
assert aioclient_mock.call_count == 6
|
||||
|
||||
await hass.services.async_call("homeassistant", "check_config")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 5
|
||||
assert aioclient_mock.call_count == 6
|
||||
|
||||
with patch(
|
||||
"homeassistant.config.async_check_ha_config_file", return_value=None
|
||||
@@ -505,7 +518,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
||||
await hass.async_block_till_done()
|
||||
assert mock_check_config.called
|
||||
|
||||
assert aioclient_mock.call_count == 6
|
||||
assert aioclient_mock.call_count == 7
|
||||
|
||||
|
||||
async def test_entry_load_and_unload(hass):
|
||||
@@ -758,7 +771,7 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration):
|
||||
assert result
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
|
||||
464
tests/components/hassio/test_repairs.py
Normal file
464
tests/components/hassio/test_repairs.py
Normal file
@@ -0,0 +1,464 @@
|
||||
"""Test repairs from supervisor issues."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_init import MOCK_ENVIRON
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_repairs(hass):
|
||||
"""Set up the repairs integration."""
|
||||
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all(aioclient_mock: AiohttpClientMocker, request: pytest.FixtureRequest):
|
||||
"""Mock all setup requests."""
|
||||
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
|
||||
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
|
||||
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"supervisor": "222",
|
||||
"homeassistant": "0.110.0",
|
||||
"hassos": "1.2.3",
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/store",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {"addons": [], "repositories": []},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/host/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"chassis": "vm",
|
||||
"operating_system": "Debian GNU/Linux 10 (buster)",
|
||||
"kernel": "4.19.0-6-amd64",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/core/info",
|
||||
json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/os/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"version_latest": "1.0.0",
|
||||
"version": "1.0.0",
|
||||
"update_available": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/supervisor/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"result": "ok",
|
||||
"version": "1.0.0",
|
||||
"version_latest": "1.0.0",
|
||||
"auto_update": True,
|
||||
"addons": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def fixture_supervisor_environ():
|
||||
"""Mock os environ for supervisor."""
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
yield
|
||||
|
||||
|
||||
def mock_resolution_info(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
unsupported: list[str] | None = None,
|
||||
unhealthy: list[str] | None = None,
|
||||
):
|
||||
"""Mock resolution/info endpoint with unsupported/unhealthy reasons."""
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": unsupported or [],
|
||||
"unhealthy": unhealthy or [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [
|
||||
{"enabled": True, "slug": "supervisor_trust"},
|
||||
{"enabled": True, "slug": "free_space"},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str):
|
||||
"""Assert repair for unhealthy/unsupported in list."""
|
||||
repair_type = "unhealthy" if unhealthy else "unsupported"
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
"dismissed_version": None,
|
||||
"domain": "hassio",
|
||||
"ignored": False,
|
||||
"is_fixable": False,
|
||||
"issue_id": f"{repair_type}_system_{reason}",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}",
|
||||
"severity": "critical" if unhealthy else "warning",
|
||||
"translation_key": f"{repair_type}_{reason}",
|
||||
"translation_placeholders": None,
|
||||
} in issues
|
||||
|
||||
|
||||
async def test_unhealthy_repairs(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test repairs added for unhealthy systems."""
|
||||
mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
|
||||
|
||||
|
||||
async def test_unsupported_repairs(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test repairs added for unsupported systems."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(
|
||||
msg["result"]["issues"], unhealthy=False, reason="content_trust"
|
||||
)
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
|
||||
async def test_unhealthy_repairs_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test unhealthy repairs added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "health_changed",
|
||||
"data": {
|
||||
"healthy": False,
|
||||
"unhealthy_reasons": ["docker"],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "health_changed",
|
||||
"data": {"healthy": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 4, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_unsupported_repairs_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test unsupported repairs added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supported_changed",
|
||||
"data": {
|
||||
"supported": False,
|
||||
"unsupported_reasons": ["os"],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supported_changed",
|
||||
"data": {"supported": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 4, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_reset_repairs_supervisor_restart(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Unsupported/unhealthy repairs reset on supervisor restart."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
mock_resolution_info(aioclient_mock)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supervisor_update",
|
||||
"update_key": "supervisor",
|
||||
"data": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 3, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_reasons_added_and_removed(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
mock_resolution_info(
|
||||
aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"]
|
||||
)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supervisor_update",
|
||||
"update_key": "supervisor",
|
||||
"data": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 3, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
|
||||
assert_repair_in_list(
|
||||
msg["result"]["issues"], unhealthy=False, reason="content_trust"
|
||||
)
|
||||
|
||||
|
||||
async def test_ignored_unsupported_skipped(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Unsupported reasons which have an identical unhealthy reason are ignored."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock, unsupported=["privileged"], unhealthy=["privileged"]
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged")
|
||||
|
||||
|
||||
async def test_new_unsupported_unhealthy_reason(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""New unsupported/unhealthy reasons result in a generic repair until next core update."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock, unsupported=["fake_unsupported"], unhealthy=["fake_unhealthy"]
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
"dismissed_version": None,
|
||||
"domain": "hassio",
|
||||
"ignored": False,
|
||||
"is_fixable": False,
|
||||
"issue_id": "unhealthy_system_fake_unhealthy",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": "https://www.home-assistant.io/more-info/unhealthy/fake_unhealthy",
|
||||
"severity": "critical",
|
||||
"translation_key": "unhealthy",
|
||||
"translation_placeholders": {"reason": "fake_unhealthy"},
|
||||
} in msg["result"]["issues"]
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
"dismissed_version": None,
|
||||
"domain": "hassio",
|
||||
"ignored": False,
|
||||
"is_fixable": False,
|
||||
"issue_id": "unsupported_system_fake_unsupported",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": "https://www.home-assistant.io/more-info/unsupported/fake_unsupported",
|
||||
"severity": "warning",
|
||||
"translation_key": "unsupported",
|
||||
"translation_placeholders": {"reason": "fake_unsupported"},
|
||||
} in msg["result"]["issues"]
|
||||
@@ -126,6 +126,19 @@ def mock_all(aioclient_mock, request):
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request):
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -61,6 +61,19 @@ def mock_all(aioclient_mock):
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client):
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_config_entry_accessory(
|
||||
"iid": 7,
|
||||
"perms": ["pr"],
|
||||
"type": "52",
|
||||
"value": "2022.11.0",
|
||||
"value": ANY,
|
||||
},
|
||||
],
|
||||
"iid": 1,
|
||||
|
||||
@@ -198,7 +198,17 @@ async def test_access_from_supervisor_ip(
|
||||
|
||||
manager: IpBanManager = app[KEY_BAN_MANAGER]
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
with patch(
|
||||
"homeassistant.components.hassio.HassIO.get_resolution_info",
|
||||
return_value={
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
|
||||
m_open = mock_open()
|
||||
|
||||
|
||||
@@ -313,6 +313,12 @@ async def test_setting_sensor_value_via_mqtt_json_message(
|
||||
|
||||
assert state.state == "100"
|
||||
|
||||
# Make sure the state is written when a sensor value is reset to ''
|
||||
async_fire_mqtt_message(hass, "test-topic", '{ "val": "" }')
|
||||
state = hass.states.get("sensor.test")
|
||||
|
||||
assert state.state == ""
|
||||
|
||||
|
||||
async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state(
|
||||
hass, mqtt_mock_entry_with_yaml_config
|
||||
|
||||
@@ -6,7 +6,13 @@ import pytest
|
||||
|
||||
from homeassistant.components import mqtt, update
|
||||
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_common import (
|
||||
@@ -68,6 +74,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"state_topic": installed_version_topic,
|
||||
"latest_version_topic": latest_version_topic,
|
||||
"name": "Test Update",
|
||||
"release_summary": "Test release summary",
|
||||
"release_url": "https://example.com/release",
|
||||
"title": "Test Update Title",
|
||||
"entity_picture": "https://example.com/icon.png",
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -84,6 +94,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "1.9.0"
|
||||
assert state.attributes.get("release_summary") == "Test release summary"
|
||||
assert state.attributes.get("release_url") == "https://example.com/release"
|
||||
assert state.attributes.get("title") == "Test Update Title"
|
||||
assert state.attributes.get("entity_picture") == "https://example.com/icon.png"
|
||||
|
||||
async_fire_mqtt_message(hass, latest_version_topic, "2.0.0")
|
||||
|
||||
@@ -126,6 +140,10 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "1.9.0"
|
||||
assert (
|
||||
state.attributes.get("entity_picture")
|
||||
== "https://brands.home-assistant.io/_/mqtt/icon.png"
|
||||
)
|
||||
|
||||
async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0.0"}')
|
||||
|
||||
@@ -137,6 +155,120 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.attributes.get("latest_version") == "2.0.0"
|
||||
|
||||
|
||||
async def test_empty_json_state_message(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test an empty JSON payload."""
|
||||
state_topic = "test/state-topic"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
mqtt.DOMAIN,
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
update.DOMAIN: {
|
||||
"state_topic": state_topic,
|
||||
"name": "Test Update",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await mqtt_mock_entry_with_yaml_config()
|
||||
|
||||
async_fire_mqtt_message(hass, state_topic, "{}")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.test_update")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test whether it fetches data from a JSON payload."""
|
||||
state_topic = "test/state-topic"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
mqtt.DOMAIN,
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
update.DOMAIN: {
|
||||
"state_topic": state_topic,
|
||||
"name": "Test Update",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await mqtt_mock_entry_with_yaml_config()
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
state_topic,
|
||||
'{"installed_version":"1.9.0","latest_version":"1.9.0",'
|
||||
'"title":"Test Update Title","release_url":"https://example.com/release",'
|
||||
'"release_summary":"Test release summary"}',
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.test_update")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "1.9.0"
|
||||
assert state.attributes.get("release_summary") == "Test release summary"
|
||||
assert state.attributes.get("release_url") == "https://example.com/release"
|
||||
assert state.attributes.get("title") == "Test Update Title"
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
state_topic,
|
||||
'{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}',
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.test_update")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "2.0.0"
|
||||
|
||||
|
||||
async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test whether it fetches data from a JSON payload with template."""
|
||||
state_topic = "test/state-topic"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
mqtt.DOMAIN,
|
||||
{
|
||||
mqtt.DOMAIN: {
|
||||
update.DOMAIN: {
|
||||
"state_topic": state_topic,
|
||||
"value_template": '{{ {"installed_version": value_json.installed, "latest_version": value_json.latest} | to_json }}',
|
||||
"name": "Test Update",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await mqtt_mock_entry_with_yaml_config()
|
||||
|
||||
async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"1.9.0"}')
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.test_update")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "1.9.0"
|
||||
|
||||
async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"2.0.0"}')
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.test_update")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("installed_version") == "1.9.0"
|
||||
assert state.attributes.get("latest_version") == "2.0.0"
|
||||
|
||||
|
||||
async def test_run_install_service(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"""Test that install service works."""
|
||||
installed_version_topic = "test/installed-version"
|
||||
|
||||
@@ -57,6 +57,19 @@ async def mock_supervisor_fixture(hass, aioclient_mock):
|
||||
"""Mock supervisor."""
|
||||
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
|
||||
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch(
|
||||
"homeassistant.components.hassio.HassIO.is_connected",
|
||||
return_value=True,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Test WebSocket Connection class."""
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import Mock
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp.test_utils import make_mocked_request
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import exceptions
|
||||
@@ -11,37 +14,86 @@ from homeassistant.components import websocket_api
|
||||
from tests.common import MockUser
|
||||
|
||||
|
||||
async def test_exception_handling():
|
||||
"""Test handling of exceptions."""
|
||||
send_messages = []
|
||||
user = MockUser()
|
||||
refresh_token = Mock()
|
||||
conn = websocket_api.ActiveConnection(
|
||||
logging.getLogger(__name__), None, send_messages.append, user, refresh_token
|
||||
)
|
||||
|
||||
for (exc, code, err) in (
|
||||
(exceptions.Unauthorized(), websocket_api.ERR_UNAUTHORIZED, "Unauthorized"),
|
||||
@pytest.mark.parametrize(
|
||||
"exc,code,err,log",
|
||||
[
|
||||
(
|
||||
exceptions.Unauthorized(),
|
||||
websocket_api.ERR_UNAUTHORIZED,
|
||||
"Unauthorized",
|
||||
"Error handling message: Unauthorized (unauthorized) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
(
|
||||
vol.Invalid("Invalid something"),
|
||||
websocket_api.ERR_INVALID_FORMAT,
|
||||
"Invalid something. Got {'id': 5}",
|
||||
"Error handling message: Invalid something. Got {'id': 5} (invalid_format) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
(
|
||||
asyncio.TimeoutError(),
|
||||
websocket_api.ERR_TIMEOUT,
|
||||
"Timeout",
|
||||
"Error handling message: Timeout (timeout) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
(asyncio.TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout"),
|
||||
(
|
||||
exceptions.HomeAssistantError("Failed to do X"),
|
||||
websocket_api.ERR_UNKNOWN_ERROR,
|
||||
"Failed to do X",
|
||||
"Error handling message: Failed to do X (unknown_error) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
(ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"),
|
||||
(
|
||||
exceptions.HomeAssistantError(),
|
||||
ValueError("Really bad"),
|
||||
websocket_api.ERR_UNKNOWN_ERROR,
|
||||
"Unknown error",
|
||||
"Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
):
|
||||
send_messages.clear()
|
||||
(
|
||||
exceptions.HomeAssistantError,
|
||||
websocket_api.ERR_UNKNOWN_ERROR,
|
||||
"Unknown error",
|
||||
"Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_exception_handling(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
exc: Exception,
|
||||
code: str,
|
||||
err: str,
|
||||
log: str,
|
||||
):
|
||||
"""Test handling of exceptions."""
|
||||
send_messages = []
|
||||
user = MockUser()
|
||||
refresh_token = Mock()
|
||||
current_request = AsyncMock()
|
||||
|
||||
def get_extra_info(key: str) -> Any:
|
||||
if key == "sslcontext":
|
||||
return True
|
||||
|
||||
if key == "peername":
|
||||
return ("127.0.0.42", 8123)
|
||||
|
||||
mocked_transport = Mock()
|
||||
mocked_transport.get_extra_info = get_extra_info
|
||||
mocked_request = make_mocked_request(
|
||||
"GET",
|
||||
"/api/websocket",
|
||||
headers={"Host": "example.com", "User-Agent": "Browser"},
|
||||
transport=mocked_transport,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.websocket_api.connection.current_request",
|
||||
) as current_request:
|
||||
current_request.get.return_value = mocked_request
|
||||
conn = websocket_api.ActiveConnection(
|
||||
logging.getLogger(__name__), None, send_messages.append, user, refresh_token
|
||||
)
|
||||
|
||||
conn.async_handle_exception({"id": 5}, exc)
|
||||
assert len(send_messages) == 1
|
||||
assert send_messages[0]["error"]["code"] == code
|
||||
assert send_messages[0]["error"]["message"] == err
|
||||
assert len(send_messages) == 1
|
||||
assert send_messages[0]["error"]["code"] == code
|
||||
assert send_messages[0]["error"]["message"] == err
|
||||
assert log in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user