Compare commits

..

36 Commits

Author SHA1 Message Date
Franck Nijhof
3409dea28c Bumped version to 2022.11.0b7 2022-11-02 12:46:33 +01:00
Bram Kragten
06d22d8249 Update frontend to 20221102.0 (#81405) 2022-11-02 12:46:19 +01:00
J. Nick Koston
a6e745b687 Bump aiohomekit to 2.2.13 (#81398) 2022-11-02 12:46:16 +01:00
Mike Degatano
f6c094b017 Improve supervisor repairs (#81387)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-11-02 12:46:12 +01:00
J. Nick Koston
9f54e332ec Bump dbus-fast to 1.61.1 (#81386) 2022-11-02 12:46:08 +01:00
Paulus Schoutsen
3aca376374 Add unit conversion for energy costs (#81379)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2022-11-02 12:46:05 +01:00
J. Nick Koston
a5f209b219 Bump aiohomekit to 2.2.12 (#81372)
* Bump aiohomekit to 2.2.12

Fixes a missing lock which was noticable on the esp32s
since they disconnect right away when you ask for gatt
notify.

https://github.com/Jc2k/aiohomekit/compare/2.2.11...2.2.12

* empty
2022-11-02 12:46:02 +01:00
J. Nick Koston
0dbf0504ff Bump bleak-retry-connector to 2.8.2 (#81370)
* Bump bleak-retry-connector to 2.8.2

Tweaks for the esp32 proxies now that we have better error
reporting. This change improves the retry cases a bit with
the new https://github.com/esphome/esphome/pull/3971

* empty
2022-11-02 12:45:58 +01:00
puddly
95ce20638a Bump zigpy-zigate to 0.10.3 (#81363) 2022-11-02 12:45:55 +01:00
Franck Nijhof
b9132e78b4 Improve error logging of WebSocket API (#81360) 2022-11-02 12:45:50 +01:00
Paulus Schoutsen
e8f93d9c7f Bumped version to 2022.11.0b6 2022-11-01 13:09:48 -04:00
J. Nick Koston
1efec8323a Bump aiohomekit to 2.2.11 (#81358) 2022-11-01 13:09:42 -04:00
J. Nick Koston
8c63a9ce5e Immediately prefer advertisements from alternate sources when a scanner goes away (#81357) 2022-11-01 13:09:42 -04:00
J. Nick Koston
9dff7ab6b9 Adjust time to remove stale connectable devices from the esphome ble to closer match bluez (#81356) 2022-11-01 13:09:41 -04:00
David F. Mulcahey
d0ddbb5f58 Fix individual LED range for ZHA device action (#81351)
The inovelli individual LED effect device action can address 7 LEDs. I had set the range 1-7 but it should be 0-6.
2022-11-01 13:09:40 -04:00
Maciej Bieniek
f265c160d1 Lower log level for non-JSON payload in MQTT update (#81348)
Change log level
2022-11-01 13:09:39 -04:00
Jan Bouwhuis
a2d432dfd6 Revert "Do not write state if payload is ''" for MQTT sensor (#81347)
* Revert "Do not write state if payload is ''"

This reverts commit 869c11884e.

* Add test
2022-11-01 13:09:37 -04:00
Franck Nijhof
c4bb225060 Fix power/energy mixup in Youless (#81345) 2022-11-01 13:09:36 -04:00
Shay Levy
473490aee7 Bump aioshelly to 4.1.2 (#81342) 2022-11-01 13:09:35 -04:00
Allen Porter
f9493bc313 Bump gcal_sync to 2.2.2 and fix recurring event bug (#81339)
* Bump gcal_sync to 2.2.2 and fix recurring event bug

* Bump to 2.2.2
2022-11-01 13:09:34 -04:00
Ron Klinkien
1cc85f77e3 Add task id attribute to fireservicerota sensor (#81323) 2022-11-01 13:09:33 -04:00
javicalle
c2c57712d2 Tuya configuration for tuya_manufacturer cluster (#81311)
* Tuya configuration for tuya_manufacturer cluster

* fix codespell

* Add attributes initialization

* Fix pylint complaints
2022-11-01 13:09:32 -04:00
Maciej Bieniek
4684101a85 Improve MQTT update platform (#81131)
* Allow JSON as state_topic payload

* Add title

* Add release_url

* Add release_summary

* Add entity_picture

* Fix typo

* Add abbreviations
2022-11-01 13:09:31 -04:00
J. Nick Koston
9b87f7f6f9 Fix homekit diagnostics test when version changes (#81046) 2022-11-01 13:09:31 -04:00
Maciej Bieniek
8965a1322c Always use Celsius in Shelly integration (#80842) 2022-11-01 13:09:30 -04:00
Franck Nijhof
dfe399e370 Cherry-pick translation updates for Supervisor (#81341) 2022-11-01 13:08:26 -04:00
Paulus Schoutsen
0ac0e9c0d5 Bumped version to 2022.11.0b5 2022-10-31 21:23:21 -04:00
J. Nick Koston
941512641b Improve esphome bluetooth error reporting (#81326) 2022-10-31 21:23:14 -04:00
Bram Kragten
599c23c1d7 Update frontend to 20221031.0 (#81324) 2022-10-31 21:23:14 -04:00
J. Nick Koston
882ad31a99 Fix Yale Access Bluetooth not being available again after being unavailable (#81320) 2022-10-31 21:23:13 -04:00
Franck Nijhof
356953c8bc Update base image to 2022.10.0 (#81317) 2022-10-31 21:23:12 -04:00
J. Nick Koston
19a5c87da6 Bump oralb-ble to 0.10.0 (#81315) 2022-10-31 21:23:11 -04:00
J. Nick Koston
d7e76fdf3a Bump zeroconf to 0.39.4 (#81313) 2022-10-31 21:23:10 -04:00
J. Nick Koston
9b4f2df8f3 Bump aiohomekit to 2.2.10 (#81312) 2022-10-31 21:23:09 -04:00
TheJulianJES
7046f5f19e Only try initializing Hue motion LED on endpoint 2 with ZHA (#81205) 2022-10-31 21:23:08 -04:00
Mike Degatano
3ddcc637da Create repairs for unsupported and unhealthy (#80747) 2022-10-31 21:23:08 -04:00
60 changed files with 1738 additions and 176 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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}"

View File

@@ -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]

View File

@@ -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"],

View File

@@ -79,6 +79,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity):
"type",
"responder_mode",
"can_respond_until",
"task_ids",
):
if data.get(value):
attr[value] = data[value]

View File

@@ -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",

View File

@@ -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"]

View File

@@ -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

View File

@@ -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"

View File

@@ -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."""

View 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])
)

View File

@@ -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."
}
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"],

View File

@@ -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",

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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": [
{

View File

@@ -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)

View File

@@ -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

View File

@@ -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": [

View File

@@ -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

View File

@@ -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",

View File

@@ -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()
)

View File

@@ -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):

View File

@@ -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 = {

View File

@@ -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()
),

View File

@@ -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": [

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
)

View File

@@ -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])

View File

@@ -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(

View File

@@ -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(

View File

@@ -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

View 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"]

View File

@@ -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(

View File

@@ -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(

View File

@@ -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):

View File

@@ -190,7 +190,7 @@ async def test_config_entry_accessory(
"iid": 7,
"perms": ["pr"],
"type": "52",
"value": "2022.11.0",
"value": ANY,
},
],
"iid": 1,

View File

@@ -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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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