mirror of
https://github.com/home-assistant/core.git
synced 2026-01-09 00:58:32 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2d30c993 | ||
|
|
cb7f7dff72 | ||
|
|
c254598331 | ||
|
|
646c56e0e9 | ||
|
|
f3a3ff28f2 | ||
|
|
087f443368 | ||
|
|
fcee1ff865 | ||
|
|
6084b323df | ||
|
|
6857562e9e | ||
|
|
aef2588f9c | ||
|
|
27c5460feb | ||
|
|
60b4600019 | ||
|
|
27752f7ad3 | ||
|
|
669c99474b | ||
|
|
76872e3789 | ||
|
|
2594500452 | ||
|
|
65c8363323 | ||
|
|
dfcad3a13d | ||
|
|
92bc780dd7 | ||
|
|
eb781060e8 | ||
|
|
a2e7897b1e | ||
|
|
854308fec2 | ||
|
|
0199e8cc43 | ||
|
|
7cc9a4310d | ||
|
|
5976238126 | ||
|
|
caedef5f1a | ||
|
|
a96b91d120 | ||
|
|
2e6ee5165e | ||
|
|
7dd7c1dadd | ||
|
|
4c548af6ef | ||
|
|
200e07b8d6 | ||
|
|
ae5a885387 | ||
|
|
bebdaacf47 | ||
|
|
339fc0a2af | ||
|
|
f44ca5f9d5 | ||
|
|
a869c1bc88 | ||
|
|
d5443b8dee | ||
|
|
6ec09320dd | ||
|
|
550f80ddd2 | ||
|
|
23d2168952 | ||
|
|
c1cb0a0f8e | ||
|
|
e53227be79 | ||
|
|
c8c1543b26 | ||
|
|
715fe95abd | ||
|
|
02cb879717 | ||
|
|
9734216215 | ||
|
|
0f06ebde06 | ||
|
|
7195372616 | ||
|
|
ac63a7e01e | ||
|
|
f08ebf5b7e | ||
|
|
49d6048278 | ||
|
|
ceae63d457 | ||
|
|
f170aba0cc | ||
|
|
66e076b57f | ||
|
|
1338b347b5 | ||
|
|
9b471ab653 | ||
|
|
e90a6bbe1c | ||
|
|
aa9965675d | ||
|
|
ad3b2f02b4 | ||
|
|
0dbe9b7cf4 | ||
|
|
b9d346baed | ||
|
|
7791711603 | ||
|
|
fdfffcb73e | ||
|
|
8e6bd840a4 | ||
|
|
619a52a387 | ||
|
|
a4d59aa599 | ||
|
|
4ba494f5cd | ||
|
|
7a7f9deb89 | ||
|
|
5786f68bb7 | ||
|
|
bccfe6646e | ||
|
|
fc7ea6e1b3 | ||
|
|
058420bb2f | ||
|
|
9695235920 | ||
|
|
57526bd21f | ||
|
|
eff9690c8a | ||
|
|
d754ea1645 | ||
|
|
5f6214ede7 |
@@ -264,6 +264,7 @@ tests/components/enphase_envoy/* @gtdiehl
|
||||
homeassistant/components/entur_public_transport/* @hfurubotten
|
||||
homeassistant/components/environment_canada/* @gwww @michaeldavie
|
||||
tests/components/environment_canada/* @gwww @michaeldavie
|
||||
homeassistant/components/envisalink/* @ufodone
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epson/* @pszafer
|
||||
tests/components/epson/* @pszafer
|
||||
|
||||
@@ -17,7 +17,12 @@ from homeassistant.components.weather import (
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
|
||||
self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][
|
||||
self._unit_system
|
||||
]["Unit"]
|
||||
wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][
|
||||
"Unit"
|
||||
]
|
||||
if wind_speed_unit == "mi/h":
|
||||
self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR
|
||||
else:
|
||||
self._attr_wind_speed_unit = wind_speed_unit
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = coordinator.location_key
|
||||
self._attr_temperature_unit = (
|
||||
|
||||
@@ -515,8 +515,8 @@ class AmcrestCam(Camera):
|
||||
max_tries = 3
|
||||
for tries in range(max_tries, 0, -1):
|
||||
try:
|
||||
await getattr(self, f"_set_{func}")(value)
|
||||
new_value = await getattr(self, f"_get_{func}")()
|
||||
await getattr(self, f"_async_set_{func}")(value)
|
||||
new_value = await getattr(self, f"_async_get_{func}")()
|
||||
if new_value != value:
|
||||
raise AmcrestCommandFailed
|
||||
except (AmcrestError, AmcrestCommandFailed) as error:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "amcrest",
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/integrations/amcrest",
|
||||
"requirements": ["amcrest==1.9.3"],
|
||||
"requirements": ["amcrest==1.9.4"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": ["@flacjacket"],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -33,16 +34,30 @@ from .const import (
|
||||
DEVICE_ANDROIDTV,
|
||||
DEVICE_FIRETV,
|
||||
DOMAIN,
|
||||
PROP_ETHMAC,
|
||||
PROP_SERIALNO,
|
||||
PROP_WIFIMAC,
|
||||
SIGNAL_CONFIG_ENTITY,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
||||
|
||||
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_androidtv_mac(dev_props):
|
||||
"""Return formatted mac from device properties."""
|
||||
for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC):
|
||||
if if_mac := dev_props.get(prop_mac):
|
||||
mac = format_mac(if_mac)
|
||||
if mac not in _INVALID_MACS:
|
||||
return mac
|
||||
return None
|
||||
|
||||
|
||||
def _setup_androidtv(hass, config):
|
||||
"""Generate an ADB key (if needed) and load it."""
|
||||
adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey"))
|
||||
|
||||
@@ -11,9 +11,8 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from . import async_connect_androidtv
|
||||
from . import async_connect_androidtv, get_androidtv_mac
|
||||
from .const import (
|
||||
CONF_ADB_SERVER_IP,
|
||||
CONF_ADB_SERVER_PORT,
|
||||
@@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
PROP_WIFIMAC,
|
||||
dev_prop.get(PROP_WIFIMAC),
|
||||
)
|
||||
unique_id = format_mac(
|
||||
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
|
||||
)
|
||||
unique_id = get_androidtv_mac(dev_prop)
|
||||
await aftv.adb_close()
|
||||
return None, unique_id
|
||||
|
||||
|
||||
@@ -51,12 +51,13 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import get_androidtv_mac
|
||||
from .const import (
|
||||
ANDROID_DEV,
|
||||
ANDROID_DEV_OPT,
|
||||
@@ -80,8 +81,6 @@ from .const import (
|
||||
DEVICE_ANDROIDTV,
|
||||
DEVICE_CLASSES,
|
||||
DOMAIN,
|
||||
PROP_ETHMAC,
|
||||
PROP_WIFIMAC,
|
||||
SIGNAL_CONFIG_ENTITY,
|
||||
)
|
||||
|
||||
@@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity):
|
||||
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
|
||||
if sw_version := info.get(ATTR_SW_VERSION):
|
||||
self._attr_device_info[ATTR_SW_VERSION] = sw_version
|
||||
if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")):
|
||||
if mac := get_androidtv_mac(info):
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
|
||||
|
||||
self._app_id_to_name = {}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Aseko Pool Live",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"requirements": ["aioaseko==0.0.1"],
|
||||
"requirements": ["aioaseko==0.0.2"],
|
||||
"codeowners": [
|
||||
"@milanmeu"
|
||||
],
|
||||
|
||||
@@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except asyncio.TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||
except (ClientResponseError, CannotConnect) as err:
|
||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ async def async_setup_august(
|
||||
hass.config_entries.async_update_entry(config_entry, data=config_data)
|
||||
|
||||
await august_gateway.async_authenticate()
|
||||
await august_gateway.async_refresh_access_token_if_needed()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
data = hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
@@ -106,11 +107,10 @@ class AugustData(AugustSubscriberMixin):
|
||||
async def async_setup(self):
|
||||
"""Async setup of august device data and activities."""
|
||||
token = self._august_gateway.access_token
|
||||
user_data, locks, doorbells = await asyncio.gather(
|
||||
self._api.async_get_user(token),
|
||||
self._api.async_get_operable_locks(token),
|
||||
self._api.async_get_doorbells(token),
|
||||
)
|
||||
# This used to be a gather but it was less reliable with august's recent api changes.
|
||||
user_data = await self._api.async_get_user(token)
|
||||
locks = await self._api.async_get_operable_locks(token)
|
||||
doorbells = await self._api.async_get_doorbells(token)
|
||||
if not doorbells:
|
||||
doorbells = []
|
||||
if not locks:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "august",
|
||||
"name": "August",
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"requirements": ["yalexs==1.1.20"],
|
||||
"requirements": ["yalexs==1.1.22"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
||||
@@ -222,7 +222,12 @@ async def async_get_mjpeg_stream(
|
||||
"""Fetch an mjpeg stream from a camera entity."""
|
||||
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||
|
||||
return await camera.handle_async_mjpeg_stream(request)
|
||||
try:
|
||||
stream = await camera.handle_async_mjpeg_stream(request)
|
||||
except ConnectionResetError:
|
||||
stream = None
|
||||
_LOGGER.debug("Error while writing MJPEG stream to transport")
|
||||
return stream
|
||||
|
||||
|
||||
async def async_get_still_stream(
|
||||
@@ -784,7 +789,11 @@ class CameraMjpegStream(CameraView):
|
||||
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
|
||||
"""Serve camera stream, possibly with interval."""
|
||||
if (interval_str := request.query.get("interval")) is None:
|
||||
stream = await camera.handle_async_mjpeg_stream(request)
|
||||
try:
|
||||
stream = await camera.handle_async_mjpeg_stream(request)
|
||||
except ConnectionResetError:
|
||||
stream = None
|
||||
_LOGGER.debug("Error while writing MJPEG stream to transport")
|
||||
if stream is None:
|
||||
raise web.HTTPBadGateway()
|
||||
return stream
|
||||
|
||||
@@ -50,6 +50,7 @@ from homeassistant.const import (
|
||||
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
@@ -636,7 +637,7 @@ class CastDevice(MediaPlayerEntity):
|
||||
return STATE_PLAYING
|
||||
return STATE_IDLE
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
return STATE_IDLE
|
||||
return STATE_OFF
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg):
|
||||
msg.pop("type")
|
||||
msg_id = msg.pop("id")
|
||||
|
||||
if "disabled_by" in msg:
|
||||
if msg.get("disabled_by") is not None:
|
||||
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
|
||||
|
||||
entry = registry.async_update_device(**msg)
|
||||
|
||||
@@ -81,8 +81,8 @@ class CPUSpeedSensor(SensorEntity):
|
||||
|
||||
if info:
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ARCH: info["arch_string_raw"],
|
||||
ATTR_BRAND: info["brand_raw"],
|
||||
ATTR_ARCH: info.get("arch_string_raw"),
|
||||
ATTR_BRAND: info.get("brand_raw"),
|
||||
}
|
||||
if HZ_ADVERTISED in info:
|
||||
self._attr_extra_state_attributes[ATTR_HZ] = round(
|
||||
|
||||
@@ -179,6 +179,7 @@ class WatcherBase:
|
||||
lowercase_hostname,
|
||||
)
|
||||
|
||||
matched_domains = set()
|
||||
for entry in self._integration_matchers:
|
||||
if MAC_ADDRESS in entry and not fnmatch.fnmatch(
|
||||
uppercase_mac, entry[MAC_ADDRESS]
|
||||
@@ -191,6 +192,11 @@ class WatcherBase:
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Matched %s against %s", data, entry)
|
||||
if entry["domain"] in matched_domains:
|
||||
# Only match once per domain
|
||||
continue
|
||||
|
||||
matched_domains.add(entry["domain"])
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
entry["domain"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "doods",
|
||||
"name": "DOODS - Dedicated Open Object Detection Service",
|
||||
"documentation": "https://www.home-assistant.io/integrations/doods",
|
||||
"requirements": ["pydoods==1.0.2", "pillow==9.0.0"],
|
||||
"requirements": ["pydoods==1.0.2", "pillow==9.0.1"],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
keep_alive,
|
||||
hass.loop,
|
||||
connection_timeout,
|
||||
False,
|
||||
)
|
||||
hass.data[DATA_EVL] = controller
|
||||
|
||||
@@ -181,12 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
_LOGGER.debug("The envisalink sent a partition update event")
|
||||
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def async_zone_bypass_update(data):
|
||||
"""Handle zone bypass status updates."""
|
||||
_LOGGER.debug("Envisalink sent a zone bypass update event. Updating zones")
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_BYPASS_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
@@ -206,7 +201,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
controller.callback_login_failure = async_login_fail_callback
|
||||
controller.callback_login_timeout = async_connection_fail_callback
|
||||
controller.callback_login_success = async_connection_success_callback
|
||||
controller.callback_zone_bypass_update = async_zone_bypass_update
|
||||
|
||||
_LOGGER.info("Start envisalink")
|
||||
controller.start()
|
||||
@@ -240,13 +234,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass, Platform.BINARY_SENSOR, "envisalink", {CONF_ZONES: zones}, config
|
||||
)
|
||||
)
|
||||
# Only DSC panels support getting zone bypass status
|
||||
if panel_type == PANEL_TYPE_DSC:
|
||||
hass.async_create_task(
|
||||
async_load_platform(
|
||||
hass, "switch", "envisalink", {CONF_ZONES: zones}, config
|
||||
)
|
||||
)
|
||||
|
||||
# Zone bypass switches are not currently created due to an issue with some panels.
|
||||
# These switches will be re-added in the future after some further refactoring of the integration.
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CUSTOM_FUNCTION, handle_custom_function, schema=SERVICE_SCHEMA
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "envisalink",
|
||||
"name": "Envisalink",
|
||||
"documentation": "https://www.home-assistant.io/integrations/envisalink",
|
||||
"requirements": ["pyenvisalink==4.3"],
|
||||
"codeowners": [],
|
||||
"requirements": ["pyenvisalink==4.4"],
|
||||
"codeowners": ["@ufodone"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==10.8.1"],
|
||||
"requirements": ["aioesphomeapi==10.8.2"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
"after_dependencies": ["zeroconf", "tag"],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.28.20"],
|
||||
"requirements": ["flux_led==0.28.22"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch", "@bdraco"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -155,7 +155,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.mesh_role = MeshRoles.NONE
|
||||
self.device_is_router: bool = True
|
||||
self.device_conn_type: str | None = None
|
||||
self.device_is_router: bool = False
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.username = username
|
||||
@@ -213,7 +214,15 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
self._current_firmware = info.get("NewSoftwareVersion")
|
||||
|
||||
self._update_available, self._latest_firmware = self._update_device_info()
|
||||
self.device_is_router = "WANIPConn1" in self.connection.services
|
||||
if "Layer3Forwarding1" in self.connection.services:
|
||||
if connection_type := self.connection.call_action(
|
||||
"Layer3Forwarding1", "GetDefaultConnectionService"
|
||||
).get("NewDefaultConnectionService"):
|
||||
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
|
||||
self.device_conn_type = connection_type[2:][:-2]
|
||||
self.device_is_router = self.connection.call_action(
|
||||
self.device_conn_type, "GetInfo"
|
||||
).get("NewEnable")
|
||||
|
||||
@callback
|
||||
async def _async_update_data(self) -> None:
|
||||
@@ -567,13 +576,6 @@ class AvmWrapper(FritzBoxTools):
|
||||
)
|
||||
return {}
|
||||
|
||||
async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]:
|
||||
"""Call WANDSLInterfaceConfig service."""
|
||||
|
||||
return await self.hass.async_add_executor_job(
|
||||
partial(self.get_wan_dsl_interface_config)
|
||||
)
|
||||
|
||||
async def async_get_wan_link_properties(self) -> dict[str, Any]:
|
||||
"""Call WANCommonInterfaceConfig service."""
|
||||
|
||||
@@ -678,11 +680,6 @@ class AvmWrapper(FritzBoxTools):
|
||||
|
||||
return self._service_call_action("WLANConfiguration", str(index), "GetInfo")
|
||||
|
||||
def get_wan_dsl_interface_config(self) -> dict[str, Any]:
|
||||
"""Call WANDSLInterfaceConfig service."""
|
||||
|
||||
return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo")
|
||||
|
||||
def get_wan_link_properties(self) -> dict[str, Any]:
|
||||
"""Call WANCommonInterfaceConfig service."""
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"current_firmware": avm_wrapper.current_firmware,
|
||||
"latest_firmware": avm_wrapper.latest_firmware,
|
||||
"update_available": avm_wrapper.update_available,
|
||||
"connection_type": avm_wrapper.device_conn_type,
|
||||
"is_router": avm_wrapper.device_is_router,
|
||||
"mesh_role": avm_wrapper.mesh_role,
|
||||
"last_update success": avm_wrapper.last_update_success,
|
||||
|
||||
@@ -277,10 +277,14 @@ async def async_setup_entry(
|
||||
_LOGGER.debug("Setting up FRITZ!Box sensors")
|
||||
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
dsl: bool = False
|
||||
dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config()
|
||||
if dslinterface:
|
||||
dsl = dslinterface["NewEnable"]
|
||||
link_properties = await avm_wrapper.async_get_wan_link_properties()
|
||||
dsl: bool = link_properties.get("NewWANAccessType") == "DSL"
|
||||
|
||||
_LOGGER.debug(
|
||||
"WANAccessType of FritzBox %s is '%s'",
|
||||
avm_wrapper.host,
|
||||
link_properties.get("NewWANAccessType"),
|
||||
)
|
||||
|
||||
entities = [
|
||||
FritzBoxSensor(avm_wrapper, entry.title, description)
|
||||
|
||||
@@ -81,16 +81,12 @@ def port_entities_list(
|
||||
|
||||
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PORTFORWARD)
|
||||
entities_list: list[FritzBoxPortSwitch] = []
|
||||
connection_type = avm_wrapper.get_default_connection()
|
||||
if not connection_type:
|
||||
if not avm_wrapper.device_conn_type:
|
||||
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_PORTFORWARD)
|
||||
return []
|
||||
|
||||
# Return NewDefaultConnectionService sample: "1.WANPPPConnection.1"
|
||||
con_type: str = connection_type["NewDefaultConnectionService"][2:][:-2]
|
||||
|
||||
# Query port forwardings and setup a switch for each forward for the current device
|
||||
resp = avm_wrapper.get_num_port_mapping(con_type)
|
||||
resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type)
|
||||
if not resp:
|
||||
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
|
||||
return []
|
||||
@@ -107,7 +103,7 @@ def port_entities_list(
|
||||
|
||||
for i in range(port_forwards_count):
|
||||
|
||||
portmap = avm_wrapper.get_port_mapping(con_type, i)
|
||||
portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i)
|
||||
if not portmap:
|
||||
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
|
||||
continue
|
||||
@@ -133,7 +129,7 @@ def port_entities_list(
|
||||
portmap,
|
||||
port_name,
|
||||
i,
|
||||
con_type,
|
||||
avm_wrapper.device_conn_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class GenericCamera(Camera):
|
||||
if self._stream_source is not None:
|
||||
self._stream_source.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
self._attr_frames_interval = 1 / device_info[CONF_FRAMERATE]
|
||||
self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
|
||||
self._supported_features = SUPPORT_STREAM if self._stream_source else 0
|
||||
self.content_type = device_info[CONF_CONTENT_TYPE]
|
||||
self.verify_ssl = device_info[CONF_VERIFY_SSL]
|
||||
|
||||
@@ -57,6 +57,8 @@ KEY_POSITION = "position"
|
||||
|
||||
DEFAULT_NAME = "Cover Group"
|
||||
|
||||
# No limit on parallel updates to enable a group calling another group
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -52,6 +52,8 @@ SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE}
|
||||
|
||||
DEFAULT_NAME = "Fan Group"
|
||||
|
||||
# No limit on parallel updates to enable a group calling another group
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -58,6 +58,9 @@ from .util import find_state_attributes, mean_tuple, reduce_attribute
|
||||
|
||||
DEFAULT_NAME = "Light Group"
|
||||
|
||||
# No limit on parallel updates to enable a group calling another group
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
||||
@@ -191,6 +191,8 @@ def parse_mapping(mapping, parents=None):
|
||||
def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up the CEC capability."""
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
# Parse configuration into a dict of device name to physical address
|
||||
# represented as a list of four elements.
|
||||
device_aliases = {}
|
||||
|
||||
44
homeassistant/components/homekit/diagnostics.py
Normal file
44
homeassistant/components/homekit/diagnostics.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Diagnostics support for HomeKit."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
from pyhap.state import State
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import HomeKit
|
||||
from .const import DOMAIN, HOMEKIT
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
|
||||
driver: AccessoryDriver = homekit.driver
|
||||
data: dict[str, Any] = {
|
||||
"status": homekit.status,
|
||||
"config-entry": {
|
||||
"title": entry.title,
|
||||
"version": entry.version,
|
||||
"data": dict(entry.data),
|
||||
"options": dict(entry.options),
|
||||
},
|
||||
}
|
||||
if not driver:
|
||||
return data
|
||||
data.update(driver.get_accessories())
|
||||
state: State = driver.state
|
||||
data.update(
|
||||
{
|
||||
"client_properties": {
|
||||
str(client): props for client, props in state.client_properties.items()
|
||||
},
|
||||
"config_version": state.config_version,
|
||||
"pairing_id": state.mac,
|
||||
}
|
||||
)
|
||||
return data
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Class to hold all light accessories."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
@@ -12,12 +14,13 @@ from homeassistant.components.light import (
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_MAX_MIREDS,
|
||||
ATTR_MIN_MIREDS,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_RGBWW_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_WHITE,
|
||||
COLOR_MODE_RGBW,
|
||||
COLOR_MODE_RGBWW,
|
||||
COLOR_MODE_WHITE,
|
||||
DOMAIN,
|
||||
brightness_supported,
|
||||
color_supported,
|
||||
@@ -32,9 +35,9 @@ from homeassistant.const import (
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.color import (
|
||||
color_hsv_to_RGB,
|
||||
color_temperature_mired_to_kelvin,
|
||||
color_temperature_to_hs,
|
||||
color_temperature_to_rgbww,
|
||||
)
|
||||
|
||||
from .accessories import TYPES, HomeAccessory
|
||||
@@ -51,12 +54,13 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RGB_COLOR = "rgb_color"
|
||||
|
||||
CHANGE_COALESCE_TIME_WINDOW = 0.01
|
||||
|
||||
DEFAULT_MIN_MIREDS = 153
|
||||
DEFAULT_MAX_MIREDS = 500
|
||||
|
||||
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW}
|
||||
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE}
|
||||
|
||||
|
||||
@TYPES.register("Light")
|
||||
@@ -79,8 +83,12 @@ class Light(HomeAccessory):
|
||||
self.color_modes = color_modes = (
|
||||
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
|
||||
)
|
||||
self._previous_color_mode = attributes.get(ATTR_COLOR_MODE)
|
||||
self.color_supported = color_supported(color_modes)
|
||||
self.color_temp_supported = color_temp_supported(color_modes)
|
||||
self.rgbw_supported = COLOR_MODE_RGBW in color_modes
|
||||
self.rgbww_supported = COLOR_MODE_RGBWW in color_modes
|
||||
self.white_supported = COLOR_MODE_WHITE in color_modes
|
||||
self.brightness_supported = brightness_supported(color_modes)
|
||||
|
||||
if self.brightness_supported:
|
||||
@@ -89,7 +97,9 @@ class Light(HomeAccessory):
|
||||
if self.color_supported:
|
||||
self.chars.extend([CHAR_HUE, CHAR_SATURATION])
|
||||
|
||||
if self.color_temp_supported:
|
||||
if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection(
|
||||
self.color_modes
|
||||
):
|
||||
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||
|
||||
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
|
||||
@@ -101,13 +111,22 @@ class Light(HomeAccessory):
|
||||
# to set to the correct initial value.
|
||||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||
|
||||
if self.color_temp_supported:
|
||||
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153))
|
||||
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500))
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
self.min_mireds = math.floor(
|
||||
attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS)
|
||||
)
|
||||
self.max_mireds = math.ceil(
|
||||
attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS)
|
||||
)
|
||||
if not self.color_temp_supported and not self.rgbww_supported:
|
||||
self.max_mireds = self.min_mireds
|
||||
self.char_color_temp = serv_light.configure_char(
|
||||
CHAR_COLOR_TEMPERATURE,
|
||||
value=min_mireds,
|
||||
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
|
||||
value=self.min_mireds,
|
||||
properties={
|
||||
PROP_MIN_VALUE: self.min_mireds,
|
||||
PROP_MAX_VALUE: self.max_mireds,
|
||||
},
|
||||
)
|
||||
|
||||
if self.color_supported:
|
||||
@@ -165,33 +184,32 @@ class Light(HomeAccessory):
|
||||
)
|
||||
return
|
||||
|
||||
# Handle white channels
|
||||
if CHAR_COLOR_TEMPERATURE in char_values:
|
||||
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
|
||||
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
|
||||
temp = char_values[CHAR_COLOR_TEMPERATURE]
|
||||
events.append(f"color temperature at {temp}")
|
||||
bright_val = round(
|
||||
((brightness_pct or self.char_brightness.value) * 255) / 100
|
||||
)
|
||||
if self.color_temp_supported:
|
||||
params[ATTR_COLOR_TEMP] = temp
|
||||
elif self.rgbww_supported:
|
||||
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
|
||||
temp, bright_val, self.min_mireds, self.max_mireds
|
||||
)
|
||||
elif self.rgbw_supported:
|
||||
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
|
||||
elif self.white_supported:
|
||||
params[ATTR_WHITE] = bright_val
|
||||
|
||||
elif (
|
||||
CHAR_HUE in char_values
|
||||
or CHAR_SATURATION in char_values
|
||||
# If we are adjusting brightness we need to send the full RGBW/RGBWW values
|
||||
# since HomeKit does not support RGBW/RGBWW
|
||||
or brightness_pct
|
||||
and COLOR_MODES_WITH_WHITES.intersection(self.color_modes)
|
||||
):
|
||||
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
|
||||
hue_sat = (
|
||||
char_values.get(CHAR_HUE, self.char_hue.value),
|
||||
char_values.get(CHAR_SATURATION, self.char_saturation.value),
|
||||
)
|
||||
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
|
||||
events.append(f"set color at {hue_sat}")
|
||||
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values
|
||||
if COLOR_MODE_RGBWW in self.color_modes:
|
||||
val = brightness_pct or self.char_brightness.value
|
||||
params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0)
|
||||
elif COLOR_MODE_RGBW in self.color_modes:
|
||||
val = brightness_pct or self.char_brightness.value
|
||||
params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0)
|
||||
else:
|
||||
params[ATTR_HS_COLOR] = hue_sat
|
||||
params[ATTR_HS_COLOR] = hue_sat
|
||||
|
||||
if (
|
||||
brightness_pct
|
||||
@@ -200,6 +218,9 @@ class Light(HomeAccessory):
|
||||
):
|
||||
params[ATTR_BRIGHTNESS_PCT] = brightness_pct
|
||||
|
||||
_LOGGER.debug(
|
||||
"Calling light service with params: %s -> %s", char_values, params
|
||||
)
|
||||
self.async_call_service(DOMAIN, service, params, ", ".join(events))
|
||||
|
||||
@callback
|
||||
@@ -210,52 +231,59 @@ class Light(HomeAccessory):
|
||||
attributes = new_state.attributes
|
||||
color_mode = attributes.get(ATTR_COLOR_MODE)
|
||||
self.char_on.set_value(int(state == STATE_ON))
|
||||
color_mode_changed = self._previous_color_mode != color_mode
|
||||
self._previous_color_mode = color_mode
|
||||
|
||||
# Handle Brightness
|
||||
if self.brightness_supported:
|
||||
if (
|
||||
color_mode
|
||||
and COLOR_MODES_WITH_WHITES.intersection({color_mode})
|
||||
and (rgb_color := attributes.get(ATTR_RGB_COLOR))
|
||||
):
|
||||
# HomeKit doesn't support RGBW/RGBWW so we need to
|
||||
# give it the color brightness only
|
||||
brightness = max(rgb_color)
|
||||
else:
|
||||
brightness = attributes.get(ATTR_BRIGHTNESS)
|
||||
if isinstance(brightness, (int, float)):
|
||||
brightness = round(brightness / 255 * 100, 0)
|
||||
# The homeassistant component might report its brightness as 0 but is
|
||||
# not off. But 0 is a special value in homekit. When you turn on a
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0 and state == STATE_ON:
|
||||
brightness = 1
|
||||
self.char_brightness.set_value(brightness)
|
||||
if (
|
||||
self.brightness_supported
|
||||
and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
|
||||
and isinstance(brightness, (int, float))
|
||||
):
|
||||
brightness = round(brightness / 255 * 100, 0)
|
||||
# The homeassistant component might report its brightness as 0 but is
|
||||
# not off. But 0 is a special value in homekit. When you turn on a
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0 and state == STATE_ON:
|
||||
brightness = 1
|
||||
self.char_brightness.set_value(brightness)
|
||||
if color_mode_changed:
|
||||
self.char_brightness.notify()
|
||||
|
||||
# Handle Color - color must always be set before color temperature
|
||||
# or the iOS UI will not display it correctly.
|
||||
if self.color_supported:
|
||||
if ATTR_COLOR_TEMP in attributes:
|
||||
if color_temp := attributes.get(ATTR_COLOR_TEMP):
|
||||
hue, saturation = color_temperature_to_hs(
|
||||
color_temperature_mired_to_kelvin(
|
||||
new_state.attributes[ATTR_COLOR_TEMP]
|
||||
)
|
||||
color_temperature_mired_to_kelvin(color_temp)
|
||||
)
|
||||
elif color_mode == COLOR_MODE_WHITE:
|
||||
hue, saturation = 0, 0
|
||||
else:
|
||||
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
|
||||
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
|
||||
self.char_hue.set_value(round(hue, 0))
|
||||
self.char_saturation.set_value(round(saturation, 0))
|
||||
if color_mode_changed:
|
||||
# If the color temp changed, be sure to force the color to update
|
||||
self.char_hue.notify()
|
||||
self.char_saturation.notify()
|
||||
|
||||
# Handle color temperature
|
||||
if self.color_temp_supported:
|
||||
color_temp = attributes.get(ATTR_COLOR_TEMP)
|
||||
# Handle white channels
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
color_temp = None
|
||||
if self.color_temp_supported:
|
||||
color_temp = attributes.get(ATTR_COLOR_TEMP)
|
||||
elif color_mode == COLOR_MODE_WHITE:
|
||||
color_temp = self.min_mireds
|
||||
if isinstance(color_temp, (int, float)):
|
||||
self.char_color_temp.set_value(round(color_temp, 0))
|
||||
if color_mode_changed:
|
||||
self.char_color_temp.notify()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Image",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/image",
|
||||
"requirements": ["pillow==9.0.0"],
|
||||
"requirements": ["pillow==9.0.1"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "IntelliFire",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/intellifire",
|
||||
"requirements": ["intellifire4py==0.5"],
|
||||
"requirements": ["intellifire4py==0.6"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@jeeftor"],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||
"requirements": [
|
||||
"xknx==0.19.1"
|
||||
"xknx==0.19.2"
|
||||
],
|
||||
"codeowners": [
|
||||
"@Julius2342",
|
||||
|
||||
@@ -35,7 +35,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
|
||||
from homeassistant.helpers.entity import validate_entity_category
|
||||
|
||||
from .const import (
|
||||
CONF_INVERT,
|
||||
@@ -320,7 +320,7 @@ class BinarySensorSchema(KNXPlatformSchema):
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -356,7 +356,7 @@ class ButtonSchema(KNXPlatformSchema):
|
||||
vol.Exclusive(
|
||||
CONF_TYPE, "length_or_type", msg=length_or_type_msg
|
||||
): object,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
@@ -500,7 +500,7 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
): vol.In(HVAC_MODES),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -555,7 +555,7 @@ class CoverSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -618,7 +618,7 @@ class FanSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_MAX_STEP): cv.byte,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -722,7 +722,7 @@ class LightSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
@@ -802,7 +802,7 @@ class NumberSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_MAX): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP): cv.positive_float,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
number_limit_sub_validator,
|
||||
@@ -824,7 +824,7 @@ class SceneSchema(KNXPlatformSchema):
|
||||
vol.Required(CONF_SCENE_NUMBER): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=64)
|
||||
),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -855,7 +855,7 @@ class SelectSchema(KNXPlatformSchema):
|
||||
],
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
select_options_sub_validator,
|
||||
@@ -880,7 +880,7 @@ class SensorSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
vol.Required(CONF_TYPE): sensor_type_validator,
|
||||
vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -901,7 +901,7 @@ class SwitchSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -948,7 +948,7 @@ class WeatherSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -138,6 +138,11 @@ async def async_setup_entry(
|
||||
|
||||
devices = bridge.get_devices()
|
||||
bridge_device = devices[BRIDGE_DEVICE_ID]
|
||||
if not config_entry.unique_id:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=hex(bridge_device["serial"])[2:].zfill(8)
|
||||
)
|
||||
|
||||
buttons = bridge.buttons
|
||||
_async_register_bridge_device(hass, entry_id, bridge_device)
|
||||
button_devices = _async_register_button_devices(
|
||||
|
||||
@@ -44,7 +44,7 @@ from homeassistant.helpers import (
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
|
||||
from homeassistant.helpers.entity import validate_entity_category
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import (
|
||||
@@ -423,7 +423,7 @@ def _validate_state_class_sensor(value: dict):
|
||||
vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any(
|
||||
None, bool, str, int, float
|
||||
),
|
||||
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): validate_entity_category,
|
||||
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
|
||||
vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Motion Blinds",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"requirements": ["motionblinds==0.5.10"],
|
||||
"requirements": ["motionblinds==0.5.12"],
|
||||
"dependencies": ["network"],
|
||||
"codeowners": ["@starkillerOG"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -577,6 +577,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_subscribe)
|
||||
websocket_api.async_register_command(hass, websocket_remove_device)
|
||||
websocket_api.async_register_command(hass, websocket_mqtt_info)
|
||||
debug_info.initialize(hass)
|
||||
|
||||
if conf is None:
|
||||
# If we have a config entry, setup is done by that config entry.
|
||||
|
||||
@@ -133,6 +133,10 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
||||
self._expired = False
|
||||
self._state = last_state.state
|
||||
|
||||
if self._expiration_trigger:
|
||||
# We might have set up a trigger already after subscribing from
|
||||
# super().async_added_to_hass()
|
||||
self._expiration_trigger()
|
||||
self._expiration_trigger = async_track_point_in_utc_time(
|
||||
self.hass, self._value_is_expired, expiration_at
|
||||
)
|
||||
@@ -189,7 +193,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
||||
# Reset old trigger
|
||||
if self._expiration_trigger:
|
||||
self._expiration_trigger()
|
||||
self._expiration_trigger = None
|
||||
|
||||
# Set new trigger
|
||||
expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)
|
||||
|
||||
@@ -15,6 +15,11 @@ DATA_MQTT_DEBUG_INFO = "mqtt_debug_info"
|
||||
STORED_MESSAGES = 10
|
||||
|
||||
|
||||
def initialize(hass: HomeAssistant):
|
||||
"""Initialize MQTT debug info."""
|
||||
hass.data[DATA_MQTT_DEBUG_INFO] = {"entities": {}, "triggers": {}}
|
||||
|
||||
|
||||
def log_messages(
|
||||
hass: HomeAssistant, entity_id: str
|
||||
) -> Callable[[MessageCallbackType], MessageCallbackType]:
|
||||
@@ -45,9 +50,7 @@ def log_messages(
|
||||
def add_subscription(hass, message_callback, subscription):
|
||||
"""Prepare debug data for subscription."""
|
||||
if entity_id := getattr(message_callback, "__entity_id", None):
|
||||
debug_info = hass.data.setdefault(
|
||||
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
|
||||
)
|
||||
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
|
||||
entity_info = debug_info["entities"].setdefault(
|
||||
entity_id, {"subscriptions": {}, "discovery_data": {}}
|
||||
)
|
||||
@@ -76,9 +79,7 @@ def remove_subscription(hass, message_callback, subscription):
|
||||
|
||||
def add_entity_discovery_data(hass, discovery_data, entity_id):
|
||||
"""Add discovery data."""
|
||||
debug_info = hass.data.setdefault(
|
||||
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
|
||||
)
|
||||
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
|
||||
entity_info = debug_info["entities"].setdefault(
|
||||
entity_id, {"subscriptions": {}, "discovery_data": {}}
|
||||
)
|
||||
@@ -93,14 +94,13 @@ def update_entity_discovery_data(hass, discovery_payload, entity_id):
|
||||
|
||||
def remove_entity_data(hass, entity_id):
|
||||
"""Remove discovery data."""
|
||||
hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id)
|
||||
if entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]:
|
||||
hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id)
|
||||
|
||||
|
||||
def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id):
|
||||
"""Add discovery data."""
|
||||
debug_info = hass.data.setdefault(
|
||||
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
|
||||
)
|
||||
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
|
||||
debug_info["triggers"][discovery_hash] = {
|
||||
"device_id": device_id,
|
||||
"discovery_data": discovery_data,
|
||||
@@ -126,9 +126,7 @@ async def info_for_device(hass, device_id):
|
||||
entries = hass.helpers.entity_registry.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=True
|
||||
)
|
||||
mqtt_debug_info = hass.data.setdefault(
|
||||
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
|
||||
)
|
||||
mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
|
||||
for entry in entries:
|
||||
if entry.entity_id not in mqtt_debug_info["entities"]:
|
||||
continue
|
||||
|
||||
@@ -30,11 +30,11 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import (
|
||||
ENTITY_CATEGORIES_SCHEMA,
|
||||
DeviceInfo,
|
||||
Entity,
|
||||
EntityCategory,
|
||||
async_generate_entity_id,
|
||||
validate_entity_category,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -191,7 +191,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): validate_entity_category,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
|
||||
@@ -549,7 +549,6 @@ class MqttDiscoveryUpdate(Entity):
|
||||
def _cleanup_discovery_on_remove(self) -> None:
|
||||
"""Stop listening to signal and cleanup discovery data."""
|
||||
if self._discovery_data and not self._removed_from_hass:
|
||||
debug_info.remove_entity_data(self.hass, self.entity_id)
|
||||
clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH])
|
||||
self._removed_from_hass = True
|
||||
|
||||
@@ -677,6 +676,7 @@ class MqttEntity(
|
||||
await MqttAttributes.async_will_remove_from_hass(self)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
await MqttDiscoveryUpdate.async_will_remove_from_hass(self)
|
||||
debug_info.remove_entity_data(self.hass, self.entity_id)
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
|
||||
@@ -180,6 +180,10 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity):
|
||||
self._expired = False
|
||||
self._state = last_state.state
|
||||
|
||||
if self._expiration_trigger:
|
||||
# We might have set up a trigger already after subscribing from
|
||||
# super().async_added_to_hass()
|
||||
self._expiration_trigger()
|
||||
self._expiration_trigger = async_track_point_in_utc_time(
|
||||
self.hass, self._value_is_expired, expiration_at
|
||||
)
|
||||
@@ -227,7 +231,6 @@ class MqttSensor(MqttEntity, SensorEntity, RestoreEntity):
|
||||
# Reset old trigger
|
||||
if self._expiration_trigger:
|
||||
self._expiration_trigger()
|
||||
self._expiration_trigger = None
|
||||
|
||||
# Set new trigger
|
||||
expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)
|
||||
|
||||
@@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DATA_SUBSCRIBER, DOMAIN
|
||||
from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN
|
||||
|
||||
REDACT_DEVICE_TRAITS = {InfoTrait.NAME}
|
||||
|
||||
@@ -21,6 +21,9 @@ async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict:
|
||||
"""Return diagnostics for a config entry."""
|
||||
if DATA_SDM not in config_entry.data:
|
||||
return {}
|
||||
|
||||
if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
|
||||
return {"error": "No subscriber configured"}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg", "http", "media_source"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"requirements": ["python-nest==4.1.0", "google-nest-sdm==1.6.0"],
|
||||
"requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"quality_scale": "platinum",
|
||||
"dhcp": [
|
||||
|
||||
@@ -26,7 +26,7 @@ CONST_LIST_E_TO_H: list[str] = ["E", "F", "G", "H"]
|
||||
CONST_LIST_I_TO_L: list[str] = ["I", "J", "K", "L"]
|
||||
CONST_LIST_M_TO_Q: list[str] = ["M", "N", "O", "Ö", "P", "Q"]
|
||||
CONST_LIST_R_TO_U: list[str] = ["R", "S", "T", "U", "Ü"]
|
||||
CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y"]
|
||||
CONST_LIST_V_TO_Z: list[str] = ["V", "W", "X", "Y", "Z"]
|
||||
|
||||
CONST_REGION_A_TO_D: Final = "_a_to_d"
|
||||
CONST_REGION_E_TO_H: Final = "_e_to_h"
|
||||
|
||||
@@ -121,14 +121,22 @@ async def async_setup_entry(
|
||||
if coordinator.data:
|
||||
if coordinator.data.electricity:
|
||||
for description in SENSOR_TYPES_ELECTRICITY:
|
||||
if description.key == KEY_LAST_ELECTRICITY_COST:
|
||||
if (
|
||||
description.key == KEY_LAST_ELECTRICITY_COST
|
||||
and coordinator.data.electricity[-1] is not None
|
||||
and coordinator.data.electricity[-1].cost is not None
|
||||
):
|
||||
description.native_unit_of_measurement = (
|
||||
coordinator.data.electricity[-1].cost.currency_unit
|
||||
)
|
||||
entities.append(OVOEnergySensor(coordinator, description, client))
|
||||
if coordinator.data.gas:
|
||||
for description in SENSOR_TYPES_GAS:
|
||||
if description.key == KEY_LAST_GAS_COST:
|
||||
if (
|
||||
description.key == KEY_LAST_GAS_COST
|
||||
and coordinator.data.gas[-1] is not None
|
||||
and coordinator.data.gas[-1].cost is not None
|
||||
):
|
||||
description.native_unit_of_measurement = coordinator.data.gas[
|
||||
-1
|
||||
].cost.currency_unit
|
||||
|
||||
@@ -82,7 +82,7 @@ async def async_setup_entry(
|
||||
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
||||
"""Representation of a Philips TV exposing the JointSpace API."""
|
||||
|
||||
_coordinator: PhilipsTVDataUpdateCoordinator
|
||||
coordinator: PhilipsTVDataUpdateCoordinator
|
||||
_attr_device_class = MediaPlayerDeviceClass.TV
|
||||
|
||||
def __init__(
|
||||
@@ -91,7 +91,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
||||
) -> None:
|
||||
"""Initialize the Philips TV."""
|
||||
self._tv = coordinator.api
|
||||
self._coordinator = coordinator
|
||||
self._sources = {}
|
||||
self._channels = {}
|
||||
self._supports = SUPPORT_PHILIPS_JS
|
||||
@@ -125,7 +124,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
supports = self._supports
|
||||
if self._coordinator.turn_on or (
|
||||
if self.coordinator.turn_on or (
|
||||
self._tv.on and self._tv.powerstate is not None
|
||||
):
|
||||
supports |= SUPPORT_TURN_ON
|
||||
@@ -170,7 +169,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
||||
await self._tv.setPowerState("On")
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
await self._coordinator.turn_on.async_run(self.hass, self._context)
|
||||
await self.coordinator.turn_on.async_run(self.hass, self._context)
|
||||
await self._async_update_soon()
|
||||
|
||||
async def async_turn_off(self):
|
||||
|
||||
@@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
|
||||
"""Device that sends commands."""
|
||||
|
||||
_coordinator: PhilipsTVDataUpdateCoordinator
|
||||
coordinator: PhilipsTVDataUpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -63,7 +63,7 @@ class PhilipsTVRemote(CoordinatorEntity, RemoteEntity):
|
||||
if self._tv.on and self._tv.powerstate:
|
||||
await self._tv.setPowerState("On")
|
||||
else:
|
||||
await self._coordinator.turn_on.async_run(self.hass, self._context)
|
||||
await self.coordinator.turn_on.async_run(self.hass, self._context)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/plex",
|
||||
"requirements": [
|
||||
"plexapi==4.9.1",
|
||||
"plexapi==4.9.2",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.13"
|
||||
],
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"domain": "proxy",
|
||||
"name": "Camera Proxy",
|
||||
"documentation": "https://www.home-assistant.io/integrations/proxy",
|
||||
"requirements": ["pillow==9.0.0"],
|
||||
"requirements": ["pillow==9.0.1"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -
|
||||
api_key=api_key,
|
||||
system_id=system_id,
|
||||
)
|
||||
await pvoutput.status()
|
||||
await pvoutput.system()
|
||||
|
||||
|
||||
class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""DataUpdateCoordinator for the PVOutput integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pvo import PVOutput, PVOutputAuthenticationError, Status
|
||||
from pvo import PVOutput, PVOutputAuthenticationError, PVOutputNoDataError, Status
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
@@ -33,5 +33,7 @@ class PVOutputDataUpdateCoordinator(DataUpdateCoordinator[Status]):
|
||||
"""Fetch system status from PVOutput."""
|
||||
try:
|
||||
return await self.pvoutput.status()
|
||||
except PVOutputNoDataError as err:
|
||||
raise UpdateFailed("PVOutput has no data available") from err
|
||||
except PVOutputAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pvoutput",
|
||||
"config_flow": true,
|
||||
"codeowners": ["@fabaff", "@frenck"],
|
||||
"requirements": ["pvo==0.2.1"],
|
||||
"requirements": ["pvo==0.2.2"],
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "qrcode",
|
||||
"name": "QR Code",
|
||||
"documentation": "https://www.home-assistant.io/integrations/qrcode",
|
||||
"requirements": ["pillow==9.0.0", "pyzbar==0.1.7"],
|
||||
"requirements": ["pillow==9.0.1", "pyzbar==0.1.7"],
|
||||
"codeowners": [],
|
||||
"iot_class": "calculated"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md"
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][I2C_HATS_MANAGER] = I2CHatsManager()
|
||||
hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()}
|
||||
|
||||
def start_i2c_hats_keep_alive(event):
|
||||
"""Start I2C-HATs keep alive."""
|
||||
|
||||
@@ -101,15 +101,15 @@ def _create_index(instance, table_name, index_name):
|
||||
"be patient!",
|
||||
index_name,
|
||||
)
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
index.create(connection)
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
)
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
)
|
||||
|
||||
_LOGGER.debug("Finished creating %s", index_name)
|
||||
|
||||
@@ -129,19 +129,19 @@ def _drop_index(instance, table_name, index_name):
|
||||
success = False
|
||||
|
||||
# Engines like DB2/Oracle
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(text(f"DROP INDEX {index_name}"))
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
|
||||
# Engines like SQLite, SQL Server
|
||||
if not success:
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -150,15 +150,15 @@ def _drop_index(instance, table_name, index_name):
|
||||
)
|
||||
)
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
|
||||
if not success:
|
||||
# Engines like MySQL, MS Access
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -167,10 +167,10 @@ def _drop_index(instance, table_name, index_name):
|
||||
)
|
||||
)
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
|
||||
if success:
|
||||
_LOGGER.debug(
|
||||
@@ -203,8 +203,8 @@ def _add_columns(instance, table_name, columns_def):
|
||||
|
||||
columns_def = [f"ADD {col_def}" for col_def in columns_def]
|
||||
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -214,14 +214,14 @@ def _add_columns(instance, table_name, columns_def):
|
||||
)
|
||||
)
|
||||
return
|
||||
except (InternalError, OperationalError, ProgrammingError):
|
||||
# Some engines support adding all columns at once,
|
||||
# this error is when they don't
|
||||
_LOGGER.info("Unable to use quick column add. Adding 1 by 1")
|
||||
except (InternalError, OperationalError, ProgrammingError):
|
||||
# Some engines support adding all columns at once,
|
||||
# this error is when they don't
|
||||
_LOGGER.info("Unable to use quick column add. Adding 1 by 1")
|
||||
|
||||
for column_def in columns_def:
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -230,13 +230,13 @@ def _add_columns(instance, table_name, columns_def):
|
||||
)
|
||||
)
|
||||
)
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Column %s already exists on %s, continuing",
|
||||
column_def.split(" ")[1],
|
||||
table_name,
|
||||
)
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Column %s already exists on %s, continuing",
|
||||
column_def.split(" ")[1],
|
||||
table_name,
|
||||
)
|
||||
|
||||
|
||||
def _modify_columns(instance, engine, table_name, columns_def):
|
||||
@@ -271,8 +271,8 @@ def _modify_columns(instance, engine, table_name, columns_def):
|
||||
else:
|
||||
columns_def = [f"MODIFY {col_def}" for col_def in columns_def]
|
||||
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -282,12 +282,12 @@ def _modify_columns(instance, engine, table_name, columns_def):
|
||||
)
|
||||
)
|
||||
return
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
|
||||
|
||||
for column_def in columns_def:
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
@@ -296,10 +296,10 @@ def _modify_columns(instance, engine, table_name, columns_def):
|
||||
)
|
||||
)
|
||||
)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not modify column %s in table %s", column_def, table_name
|
||||
)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not modify column %s in table %s", column_def, table_name
|
||||
)
|
||||
|
||||
|
||||
def _update_states_table_with_foreign_key_options(instance, engine):
|
||||
@@ -330,17 +330,17 @@ def _update_states_table_with_foreign_key_options(instance, engine):
|
||||
)
|
||||
|
||||
for alter in alters:
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(DropConstraint(alter["old_fk"]))
|
||||
for fkc in states_key_constraints:
|
||||
if fkc.column_keys == alter["columns"]:
|
||||
connection.execute(AddConstraint(fkc))
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not update foreign options in %s table", TABLE_STATES
|
||||
)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not update foreign options in %s table", TABLE_STATES
|
||||
)
|
||||
|
||||
|
||||
def _drop_foreign_key_constraints(instance, engine, table, columns):
|
||||
@@ -361,16 +361,16 @@ def _drop_foreign_key_constraints(instance, engine, table, columns):
|
||||
)
|
||||
|
||||
for drop in drops:
|
||||
try:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(DropConstraint(drop))
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not drop foreign constraints in %s table on %s",
|
||||
TABLE_STATES,
|
||||
columns,
|
||||
)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not drop foreign constraints in %s table on %s",
|
||||
TABLE_STATES,
|
||||
columns,
|
||||
)
|
||||
|
||||
|
||||
def _apply_update(instance, new_version, old_version): # noqa: C901
|
||||
|
||||
@@ -290,7 +290,7 @@ def _find_duplicates(
|
||||
)
|
||||
.filter(subquery.c.is_duplicate == 1)
|
||||
.order_by(table.metadata_id, table.start, table.id.desc())
|
||||
.limit(MAX_ROWS_TO_PURGE)
|
||||
.limit(1000 * MAX_ROWS_TO_PURGE)
|
||||
)
|
||||
duplicates = execute(query)
|
||||
original_as_dict = {}
|
||||
@@ -343,12 +343,13 @@ def _delete_duplicates_from_table(
|
||||
if not duplicate_ids:
|
||||
break
|
||||
all_non_identical_duplicates.extend(non_identical_duplicates)
|
||||
deleted_rows = (
|
||||
session.query(table)
|
||||
.filter(table.id.in_(duplicate_ids))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
total_deleted_rows += deleted_rows
|
||||
for i in range(0, len(duplicate_ids), MAX_ROWS_TO_PURGE):
|
||||
deleted_rows = (
|
||||
session.query(table)
|
||||
.filter(table.id.in_(duplicate_ids[i : i + MAX_ROWS_TO_PURGE]))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
total_deleted_rows += deleted_rows
|
||||
return (total_deleted_rows, all_non_identical_duplicates)
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.const import (
|
||||
CONF_TYPE,
|
||||
CONF_USERNAME,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
MASS_KILOGRAMS,
|
||||
POWER_WATT,
|
||||
@@ -33,6 +32,7 @@ from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -131,17 +131,19 @@ async def async_setup_platform(
|
||||
|
||||
return values
|
||||
|
||||
@callback
|
||||
def start_update_interval(event):
|
||||
"""Start the update interval scheduling."""
|
||||
nonlocal remove_interval_update
|
||||
remove_interval_update = async_track_time_interval_backoff(hass, async_saj)
|
||||
|
||||
@callback
|
||||
def stop_update_interval(event):
|
||||
"""Properly cancel the scheduled update."""
|
||||
remove_interval_update() # pylint: disable=not-callable
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval)
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval)
|
||||
async_at_start(hass, start_update_interval)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "seven_segments",
|
||||
"name": "Seven Segments OCR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
|
||||
"requirements": ["pillow==9.0.0"],
|
||||
"requirements": ["pillow==9.0.1"],
|
||||
"codeowners": ["@fabaff"],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==1.0.8"],
|
||||
"requirements": ["aioshelly==1.0.9"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sighthound",
|
||||
"name": "Sighthound",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sighthound",
|
||||
"requirements": ["pillow==9.0.0", "simplehound==0.3"],
|
||||
"requirements": ["pillow==9.0.1", "simplehound==0.3"],
|
||||
"codeowners": ["@robmarkcole"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -482,6 +482,7 @@ class SimpliSafe:
|
||||
self._websocket_reconnect_task: asyncio.Task | None = None
|
||||
self.entry = entry
|
||||
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
||||
self.subscription_data: dict[int, Any] = api.subscription_data
|
||||
self.systems: dict[int, SystemType] = {}
|
||||
|
||||
# This will get filled in by async_init:
|
||||
|
||||
@@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant
|
||||
from . import SimpliSafe
|
||||
from .const import DOMAIN
|
||||
|
||||
CONF_CREDIT_CARD = "creditCard"
|
||||
CONF_EXPIRES = "expires"
|
||||
CONF_LOCATION = "location"
|
||||
CONF_LOCATION_NAME = "locationName"
|
||||
CONF_PAYMENT_PROFILE_ID = "paymentProfileId"
|
||||
CONF_SERIAL = "serial"
|
||||
CONF_SID = "sid"
|
||||
CONF_SYSTEM_ID = "system_id"
|
||||
CONF_UID = "uid"
|
||||
CONF_WIFI_SSID = "wifi_ssid"
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_ADDRESS,
|
||||
CONF_CREDIT_CARD,
|
||||
CONF_EXPIRES,
|
||||
CONF_LOCATION,
|
||||
CONF_LOCATION_NAME,
|
||||
CONF_PAYMENT_PROFILE_ID,
|
||||
CONF_SERIAL,
|
||||
CONF_SID,
|
||||
CONF_SYSTEM_ID,
|
||||
CONF_UID,
|
||||
CONF_WIFI_SSID,
|
||||
}
|
||||
|
||||
@@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"entry": {
|
||||
"options": dict(entry.options),
|
||||
},
|
||||
"subscription_data": simplisafe.subscription_data,
|
||||
"systems": [system.as_dict() for system in simplisafe.systems.values()],
|
||||
},
|
||||
TO_REDACT,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==2022.01.0"],
|
||||
"requirements": ["simplisafe-python==2022.02.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"dhcp": [
|
||||
|
||||
@@ -44,6 +44,7 @@ SONOS_ALBUM_ARTIST = "album_artists"
|
||||
SONOS_TRACKS = "tracks"
|
||||
SONOS_COMPOSER = "composers"
|
||||
SONOS_RADIO = "radio"
|
||||
SONOS_OTHER_ITEM = "other items"
|
||||
|
||||
SONOS_STATE_PLAYING = "PLAYING"
|
||||
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
|
||||
@@ -76,6 +77,7 @@ SONOS_TO_MEDIA_CLASSES = {
|
||||
"object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
|
||||
"object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
|
||||
"object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
|
||||
"object.item": MEDIA_CLASS_TRACK,
|
||||
"object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
|
||||
"object.item.audioItem.audioBroadcast": MEDIA_CLASS_GENRE,
|
||||
}
|
||||
@@ -121,6 +123,7 @@ SONOS_TYPES_MAPPING = {
|
||||
"object.container.person.musicArtist": SONOS_ALBUM_ARTIST,
|
||||
"object.container.playlistContainer.sameArtist": SONOS_ARTIST,
|
||||
"object.container.playlistContainer": SONOS_PLAYLISTS,
|
||||
"object.item": SONOS_OTHER_ITEM,
|
||||
"object.item.audioItem.musicTrack": SONOS_TRACKS,
|
||||
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Sonos",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"requirements": ["soco==0.26.0"],
|
||||
"requirements": ["soco==0.26.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
|
||||
"zeroconf": ["_sonos._tcp.local."],
|
||||
|
||||
@@ -162,8 +162,17 @@ def build_item_response(media_library, payload, get_thumbnail_url=None):
|
||||
payload["idstring"].split("/")[2:]
|
||||
)
|
||||
|
||||
try:
|
||||
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
|
||||
except KeyError:
|
||||
_LOGGER.debug(
|
||||
"Unknown media type received when building item response: %s",
|
||||
payload["search_type"],
|
||||
)
|
||||
return
|
||||
|
||||
media = media_library.browse_by_idstring(
|
||||
MEDIA_TYPES_TO_SONOS[payload["search_type"]],
|
||||
search_type,
|
||||
payload["idstring"],
|
||||
full_album_art_uri=True,
|
||||
max_items=0,
|
||||
@@ -371,11 +380,16 @@ def favorites_payload(favorites):
|
||||
|
||||
group_types = {fav.reference.item_class for fav in favorites}
|
||||
for group_type in sorted(group_types):
|
||||
media_content_type = SONOS_TYPES_MAPPING[group_type]
|
||||
try:
|
||||
media_content_type = SONOS_TYPES_MAPPING[group_type]
|
||||
media_class = SONOS_TO_MEDIA_CLASSES[group_type]
|
||||
except KeyError:
|
||||
_LOGGER.debug("Unknown media type or class received %s", group_type)
|
||||
continue
|
||||
children.append(
|
||||
BrowseMedia(
|
||||
title=media_content_type.title(),
|
||||
media_class=SONOS_TO_MEDIA_CLASSES[group_type],
|
||||
media_class=media_class,
|
||||
media_content_id=group_type,
|
||||
media_content_type="favorites_folder",
|
||||
can_play=False,
|
||||
|
||||
@@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
plex_plugin.play_now(media)
|
||||
return
|
||||
|
||||
share_link = self.speaker.share_link
|
||||
share_link = self.coordinator.share_link
|
||||
if share_link.is_share_link(media_id):
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
share_link.add_share_link_to_queue(media_id)
|
||||
|
||||
@@ -399,13 +399,20 @@ class SonosSpeaker:
|
||||
return_exceptions=True,
|
||||
)
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
_LOGGER.debug(
|
||||
"Unsubscribe failed for %s: %s",
|
||||
self.zone_name,
|
||||
result,
|
||||
exc_info=result,
|
||||
)
|
||||
if isinstance(result, asyncio.exceptions.TimeoutError):
|
||||
message = "Request timed out"
|
||||
exc_info = None
|
||||
elif isinstance(result, Exception):
|
||||
message = result
|
||||
exc_info = result if not str(result) else None
|
||||
else:
|
||||
continue
|
||||
_LOGGER.debug(
|
||||
"Unsubscribe failed for %s: %s",
|
||||
self.zone_name,
|
||||
message,
|
||||
exc_info=exc_info,
|
||||
)
|
||||
self._subscriptions = []
|
||||
|
||||
@callback
|
||||
@@ -422,19 +429,18 @@ class SonosSpeaker:
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
if getattr(exception, "status", None) == 412:
|
||||
_LOGGER.warning(
|
||||
"Subscriptions for %s failed, speaker may have lost power",
|
||||
self.zone_name,
|
||||
)
|
||||
if isinstance(exception, asyncio.exceptions.TimeoutError):
|
||||
message = "Request timed out"
|
||||
exc_info = None
|
||||
else:
|
||||
exc_info = exception if _LOGGER.isEnabledFor(logging.DEBUG) else None
|
||||
_LOGGER.error(
|
||||
"Subscription renewals for %s failed: %s",
|
||||
self.zone_name,
|
||||
exception,
|
||||
exc_info=exc_info,
|
||||
)
|
||||
message = exception
|
||||
exc_info = exception if not str(exception) else None
|
||||
_LOGGER.warning(
|
||||
"Subscription renewals for %s failed, marking unavailable: %s",
|
||||
self.zone_name,
|
||||
message,
|
||||
exc_info=exc_info,
|
||||
)
|
||||
await self.async_offline()
|
||||
|
||||
@callback
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""The spotify integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
from spotipy import Spotify, SpotifyException
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -20,13 +25,16 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from . import config_flow
|
||||
from .const import (
|
||||
DATA_SPOTIFY_CLIENT,
|
||||
DATA_SPOTIFY_DEVICES,
|
||||
DATA_SPOTIFY_ME,
|
||||
DATA_SPOTIFY_SESSION,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MEDIA_PLAYER_PREFIX,
|
||||
SPOTIFY_SCOPES,
|
||||
)
|
||||
@@ -112,9 +120,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except SpotifyException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def _update_devices() -> list[dict[str, Any]]:
|
||||
try:
|
||||
devices: dict[str, Any] | None = await hass.async_add_executor_job(
|
||||
spotify.devices
|
||||
)
|
||||
except (requests.RequestException, SpotifyException) as err:
|
||||
raise UpdateFailed from err
|
||||
|
||||
if devices is None:
|
||||
return []
|
||||
|
||||
return devices.get("devices", [])
|
||||
|
||||
device_coordinator: DataUpdateCoordinator[
|
||||
list[dict[str, Any]]
|
||||
] = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=f"{entry.title} Devices",
|
||||
update_interval=timedelta(minutes=5),
|
||||
update_method=_update_devices,
|
||||
)
|
||||
await device_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_SPOTIFY_CLIENT: spotify,
|
||||
DATA_SPOTIFY_DEVICES: device_coordinator,
|
||||
DATA_SPOTIFY_ME: current_user,
|
||||
DATA_SPOTIFY_SESSION: session,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"""Define constants for the Spotify integration."""
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "spotify"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DATA_SPOTIFY_CLIENT = "spotify_client"
|
||||
DATA_SPOTIFY_DEVICES = "spotify_devices"
|
||||
DATA_SPOTIFY_ME = "spotify_me"
|
||||
DATA_SPOTIFY_SESSION = "spotify_session"
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ from homeassistant.const import (
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
@@ -62,6 +62,7 @@ from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from .const import (
|
||||
DATA_SPOTIFY_CLIENT,
|
||||
DATA_SPOTIFY_DEVICES,
|
||||
DATA_SPOTIFY_ME,
|
||||
DATA_SPOTIFY_SESSION,
|
||||
DOMAIN,
|
||||
@@ -269,7 +270,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
)
|
||||
|
||||
self._currently_playing: dict | None = {}
|
||||
self._devices: list[dict] | None = []
|
||||
self._playlist: dict | None = None
|
||||
|
||||
self._attr_name = self._name
|
||||
@@ -290,6 +290,11 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
"""Return spotify API."""
|
||||
return self._spotify_data[DATA_SPOTIFY_CLIENT]
|
||||
|
||||
@property
|
||||
def _devices(self) -> list:
|
||||
"""Return spotify devices."""
|
||||
return self._spotify_data[DATA_SPOTIFY_DEVICES].data
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this entity."""
|
||||
@@ -517,13 +522,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
current = self._spotify.current_playback()
|
||||
self._currently_playing = current or {}
|
||||
|
||||
self._playlist = None
|
||||
context = self._currently_playing.get("context")
|
||||
if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST:
|
||||
self._playlist = self._spotify.playlist(current["context"]["uri"])
|
||||
|
||||
devices = self._spotify.devices() or {}
|
||||
self._devices = devices.get("devices", [])
|
||||
if context is not None and (
|
||||
self._playlist is None or self._playlist["uri"] != context["uri"]
|
||||
):
|
||||
self._playlist = None
|
||||
if context["type"] == MEDIA_TYPE_PLAYLIST:
|
||||
self._playlist = self._spotify.playlist(current["context"]["uri"])
|
||||
|
||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||
"""Implement the websocket media browsing helper."""
|
||||
@@ -543,6 +548,22 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
media_content_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_devices_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if not self.enabled:
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._spotify_data[DATA_SPOTIFY_DEVICES].async_add_listener(
|
||||
self._handle_devices_update
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_browse_media_internal(
|
||||
hass,
|
||||
|
||||
@@ -338,7 +338,6 @@ class Stream:
|
||||
)
|
||||
except StreamWorkerError as err:
|
||||
self._logger.error("Error from stream worker: %s", str(err))
|
||||
self._available = False
|
||||
|
||||
stream_state.discontinuity()
|
||||
if not self.keepalive or self._thread_quit.is_set():
|
||||
|
||||
@@ -16,6 +16,7 @@ from synology_dsm.api.storage.storage import SynoStorage
|
||||
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
|
||||
from synology_dsm.exceptions import (
|
||||
SynologyDSMAPIErrorException,
|
||||
SynologyDSMException,
|
||||
SynologyDSMLoginFailedException,
|
||||
SynologyDSMRequestException,
|
||||
)
|
||||
@@ -32,7 +33,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import CONF_DEVICE_TOKEN
|
||||
from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -217,6 +218,11 @@ class SynoApi:
|
||||
)
|
||||
self.surveillance_station = self.dsm.surveillance_station
|
||||
|
||||
def _set_system_loaded(self, state: bool = False) -> None:
|
||||
"""Set system loaded flag."""
|
||||
dsm_device = self._hass.data[DOMAIN].get(self.information.serial)
|
||||
dsm_device[SYSTEM_LOADED] = state
|
||||
|
||||
async def _syno_api_executer(self, api_call: Callable) -> None:
|
||||
"""Synology api call wrapper."""
|
||||
try:
|
||||
@@ -230,14 +236,20 @@ class SynoApi:
|
||||
async def async_reboot(self) -> None:
|
||||
"""Reboot NAS."""
|
||||
await self._syno_api_executer(self.system.reboot)
|
||||
self._set_system_loaded()
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Shutdown NAS."""
|
||||
await self._syno_api_executer(self.system.shutdown)
|
||||
self._set_system_loaded()
|
||||
|
||||
async def async_unload(self) -> None:
|
||||
"""Stop interacting with the NAS and prepare for removal from hass."""
|
||||
await self._syno_api_executer(self.dsm.logout)
|
||||
try:
|
||||
await self._syno_api_executer(self.dsm.logout)
|
||||
except SynologyDSMException:
|
||||
# ignore API errors during logout
|
||||
pass
|
||||
|
||||
async def async_update(self, now: timedelta | None = None) -> None:
|
||||
"""Update function for updating API information."""
|
||||
|
||||
@@ -267,7 +267,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
and existing_entry.data[CONF_HOST] != parsed_url.hostname
|
||||
and not fqdn_with_ssl_verification
|
||||
):
|
||||
_LOGGER.debug(
|
||||
_LOGGER.info(
|
||||
"Update host from '%s' to '%s' for NAS '%s' via SSDP discovery",
|
||||
existing_entry.data[CONF_HOST],
|
||||
parsed_url.hostname,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "synology_dsm",
|
||||
"name": "Synology DSM",
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"requirements": ["py-synologydsm-api==1.0.5"],
|
||||
"requirements": ["py-synologydsm-api==1.0.6"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
|
||||
"config_flow": true,
|
||||
"ssdp": [
|
||||
|
||||
@@ -15,7 +15,6 @@ from .const import (
|
||||
SERVICE_SHUTDOWN,
|
||||
SERVICES,
|
||||
SYNO_API,
|
||||
SYSTEM_LOADED,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@@ -57,7 +56,6 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
)
|
||||
dsm_api: SynoApi = dsm_device[SYNO_API]
|
||||
try:
|
||||
dsm_device[SYSTEM_LOADED] = False
|
||||
await getattr(dsm_api, f"async_{call.service}")()
|
||||
except SynologyDSMException as ex:
|
||||
LOGGER.error(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"tf-models-official==2.5.0",
|
||||
"pycocotools==2.0.1",
|
||||
"numpy==1.21.4",
|
||||
"pillow==9.0.0"
|
||||
"pillow==9.0.1"
|
||||
],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -161,6 +161,21 @@ class TodSensor(BinarySensorEntity):
|
||||
|
||||
self._time_before = before_event_date
|
||||
|
||||
# We are calculating the _time_after value assuming that it will happen today
|
||||
# But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00
|
||||
# If _time_before and _time_after are ahead of nowutc:
|
||||
# _time_before is set to 12:00 next day
|
||||
# _time_after is set to 23:00 today
|
||||
# nowutc is set to 10:00 today
|
||||
if (
|
||||
not is_sun_event(self._after)
|
||||
and self._time_after > nowutc
|
||||
and self._time_before > nowutc + timedelta(days=1)
|
||||
):
|
||||
# remove one day from _time_before and _time_after
|
||||
self._time_after -= timedelta(days=1)
|
||||
self._time_before -= timedelta(days=1)
|
||||
|
||||
# Add offset to utc boundaries according to the configuration
|
||||
self._time_after += self._after_offset
|
||||
self._time_before += self._before_offset
|
||||
|
||||
@@ -12,7 +12,7 @@ from async_upnp_client.exceptions import UpnpError
|
||||
from async_upnp_client.profiles.igd import IgdDevice
|
||||
|
||||
from homeassistant.components import ssdp
|
||||
from homeassistant.components.ssdp import SsdpChange
|
||||
from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -71,19 +71,22 @@ class Device:
|
||||
return device
|
||||
|
||||
async def async_ssdp_callback(
|
||||
self, headers: Mapping[str, Any], change: SsdpChange
|
||||
self, service_info: SsdpServiceInfo, change: SsdpChange
|
||||
) -> None:
|
||||
"""SSDP callback, update if needed."""
|
||||
_LOGGER.debug("SSDP Callback, change: %s, headers: %s", change, headers)
|
||||
if ssdp.ATTR_SSDP_LOCATION not in headers:
|
||||
_LOGGER.debug(
|
||||
"SSDP Callback, change: %s, headers: %s", change, service_info.ssdp_headers
|
||||
)
|
||||
if service_info.ssdp_location is None:
|
||||
return
|
||||
|
||||
location = headers[ssdp.ATTR_SSDP_LOCATION]
|
||||
device = self._igd_device.device
|
||||
if location == device.device_url:
|
||||
if service_info.ssdp_location == device.device_url:
|
||||
return
|
||||
|
||||
new_upnp_device = await async_create_upnp_device(self.hass, location)
|
||||
new_upnp_device = await async_create_upnp_device(
|
||||
self.hass, service_info.ssdp_location
|
||||
)
|
||||
device.reinit(new_upnp_device)
|
||||
|
||||
@property
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyhaversion.consts import HaVersionChannel, HaVersionSource
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -75,8 +74,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._entry_data.update(user_input)
|
||||
|
||||
if not self.show_advanced_options or user_input[CONF_SOURCE] in (
|
||||
HaVersionSource.LOCAL,
|
||||
HaVersionSource.HAIO,
|
||||
"local",
|
||||
"haio",
|
||||
):
|
||||
return self.async_create_entry(
|
||||
title=self._config_entry_name,
|
||||
@@ -92,8 +91,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the version_source step."""
|
||||
if user_input is None:
|
||||
if self._entry_data[CONF_SOURCE] in (
|
||||
HaVersionSource.SUPERVISOR,
|
||||
HaVersionSource.CONTAINER,
|
||||
"supervisor",
|
||||
"container",
|
||||
):
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
@@ -102,7 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
): vol.In(VALID_CHANNELS),
|
||||
}
|
||||
)
|
||||
if self._entry_data[CONF_SOURCE] == HaVersionSource.SUPERVISOR:
|
||||
if self._entry_data[CONF_SOURCE] == "supervisor":
|
||||
data_schema = data_schema.extend(
|
||||
{
|
||||
vol.Required(CONF_IMAGE, default=DEFAULT_IMAGE): vol.In(
|
||||
@@ -151,7 +150,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@property
|
||||
def _config_entry_name(self) -> str:
|
||||
"""Return the name of the config entry."""
|
||||
if self._entry_data[CONF_SOURCE] == HaVersionSource.LOCAL:
|
||||
if self._entry_data[CONF_SOURCE] == "local":
|
||||
return DEFAULT_NAME_CURRENT
|
||||
|
||||
name = self._entry_data[CONF_VERSION_SOURCE]
|
||||
@@ -166,21 +165,21 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any:
|
||||
"""Convert a key from the imported configuration."""
|
||||
data = DEFAULT_CONFIGURATION.copy()
|
||||
if config.get(CONF_BETA):
|
||||
data[CONF_CHANNEL] = HaVersionChannel.BETA
|
||||
data[CONF_CHANNEL] = "beta"
|
||||
|
||||
if (source := config.get(CONF_SOURCE)) and source != DEFAULT_SOURCE:
|
||||
if source == SOURCE_HASSIO:
|
||||
data[CONF_SOURCE] = HaVersionSource.SUPERVISOR
|
||||
data[CONF_SOURCE] = "supervisor"
|
||||
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_VERSIONS
|
||||
elif source == SOURCE_DOKCER:
|
||||
data[CONF_SOURCE] = HaVersionSource.CONTAINER
|
||||
data[CONF_SOURCE] = "container"
|
||||
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_DOCKER_HUB
|
||||
else:
|
||||
data[CONF_SOURCE] = source
|
||||
data[CONF_VERSION_SOURCE] = VERSION_SOURCE_MAP_INVERTED[source]
|
||||
|
||||
if (image := config.get(CONF_IMAGE)) and image != DEFAULT_IMAGE:
|
||||
if data[CONF_SOURCE] == HaVersionSource.CONTAINER:
|
||||
if data[CONF_SOURCE] == "container":
|
||||
data[CONF_IMAGE] = f"{config[CONF_IMAGE]}{POSTFIX_CONTAINER_NAME}"
|
||||
else:
|
||||
data[CONF_IMAGE] = config[CONF_IMAGE]
|
||||
@@ -188,7 +187,7 @@ def _convert_imported_configuration(config: dict[str, Any]) -> Any:
|
||||
if (name := config.get(CONF_NAME)) and name != DEFAULT_NAME:
|
||||
data[CONF_NAME] = config[CONF_NAME]
|
||||
else:
|
||||
if data[CONF_SOURCE] == HaVersionSource.LOCAL:
|
||||
if data[CONF_SOURCE] == "local":
|
||||
data[CONF_NAME] = DEFAULT_NAME_CURRENT
|
||||
else:
|
||||
data[CONF_NAME] = DEFAULT_NAME_LATEST
|
||||
|
||||
@@ -41,12 +41,12 @@ VERSION_SOURCE_VERSIONS: Final = "Home Assistant Versions"
|
||||
|
||||
DEFAULT_BETA: Final = False
|
||||
DEFAULT_BOARD: Final = "OVA"
|
||||
DEFAULT_CHANNEL: Final[HaVersionChannel] = HaVersionChannel.STABLE
|
||||
DEFAULT_CHANNEL: Final = "stable"
|
||||
DEFAULT_IMAGE: Final = "default"
|
||||
DEFAULT_NAME_CURRENT: Final = "Current Version"
|
||||
DEFAULT_NAME_LATEST: Final = "Latest Version"
|
||||
DEFAULT_NAME: Final = ""
|
||||
DEFAULT_SOURCE: Final[HaVersionSource] = HaVersionSource.LOCAL
|
||||
DEFAULT_SOURCE: Final = "local"
|
||||
DEFAULT_CONFIGURATION: Final[dict[str, Any]] = {
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_CHANNEL: DEFAULT_CHANNEL,
|
||||
@@ -81,22 +81,22 @@ BOARD_MAP: Final[dict[str, str]] = {
|
||||
|
||||
VALID_BOARDS: Final[list[str]] = list(BOARD_MAP)
|
||||
|
||||
VERSION_SOURCE_MAP: Final[dict[str, HaVersionSource]] = {
|
||||
VERSION_SOURCE_LOCAL: HaVersionSource.LOCAL,
|
||||
VERSION_SOURCE_VERSIONS: HaVersionSource.SUPERVISOR,
|
||||
VERSION_SOURCE_HAIO: HaVersionSource.HAIO,
|
||||
VERSION_SOURCE_DOCKER_HUB: HaVersionSource.CONTAINER,
|
||||
VERSION_SOURCE_PYPI: HaVersionSource.PYPI,
|
||||
VERSION_SOURCE_MAP: Final[dict[str, str]] = {
|
||||
VERSION_SOURCE_LOCAL: "local",
|
||||
VERSION_SOURCE_VERSIONS: "supervisor",
|
||||
VERSION_SOURCE_HAIO: "haio",
|
||||
VERSION_SOURCE_DOCKER_HUB: "container",
|
||||
VERSION_SOURCE_PYPI: "pypi",
|
||||
}
|
||||
|
||||
VERSION_SOURCE_MAP_INVERTED: Final[dict[HaVersionSource, str]] = {
|
||||
VERSION_SOURCE_MAP_INVERTED: Final[dict[str, str]] = {
|
||||
value: key for key, value in VERSION_SOURCE_MAP.items()
|
||||
}
|
||||
|
||||
|
||||
VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [
|
||||
SOURCE_HASSIO, # Kept to not break existing configurations
|
||||
SOURCE_DOKCER, # Kept to not break existing configurations
|
||||
"hassio", # Kept to not break existing configurations
|
||||
"docker", # Kept to not break existing configurations
|
||||
]
|
||||
|
||||
VALID_IMAGES: Final = [
|
||||
|
||||
56
homeassistant/components/version/diagnostics.py
Normal file
56
homeassistant/components/version/diagnostics.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Provides diagnostics for Version."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from attr import asdict
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
devices = []
|
||||
|
||||
registry_devices = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
|
||||
for device in registry_devices:
|
||||
entities = []
|
||||
|
||||
registry_entities = er.async_entries_for_device(
|
||||
entity_registry,
|
||||
device_id=device.id,
|
||||
include_disabled_entities=True,
|
||||
)
|
||||
|
||||
for entity in registry_entities:
|
||||
state_dict = None
|
||||
if state := hass.states.get(entity.entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
state_dict.pop("context", None)
|
||||
|
||||
entities.append({"entry": asdict(entity), "state": state_dict})
|
||||
|
||||
devices.append({"device": asdict(device), "entities": entities})
|
||||
|
||||
return {
|
||||
"entry": config_entry.as_dict(),
|
||||
"coordinator_data": {
|
||||
"version": coordinator.version,
|
||||
"version_data": coordinator.version_data,
|
||||
},
|
||||
"devices": devices,
|
||||
}
|
||||
@@ -133,6 +133,7 @@ APPLICATION_VERSION = "application_version"
|
||||
MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval"
|
||||
UUID = "uuid"
|
||||
SUPPORTED_PROTOCOLS = "supported_protocols"
|
||||
ADDITIONAL_PROPERTIES = "additional_properties"
|
||||
|
||||
FEATURE = "feature"
|
||||
UNPROVISION = "unprovision"
|
||||
@@ -170,6 +171,7 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation
|
||||
max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL),
|
||||
uuid=info.get(UUID),
|
||||
supported_protocols=protocols if protocols else None,
|
||||
additional_properties=info.get(ADDITIONAL_PROPERTIES, {}),
|
||||
)
|
||||
return info
|
||||
|
||||
@@ -212,6 +214,7 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(Protocols)],
|
||||
),
|
||||
vol.Optional(ADDITIONAL_PROPERTIES): dict,
|
||||
}
|
||||
),
|
||||
convert_qr_provisioning_information,
|
||||
|
||||
@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 2
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "6"
|
||||
__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)
|
||||
|
||||
@@ -11,7 +11,7 @@ import logging
|
||||
import math
|
||||
import sys
|
||||
from timeit import default_timer as timer
|
||||
from typing import Any, Final, Literal, TypedDict, final
|
||||
from typing import Any, Literal, TypedDict, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -58,7 +58,13 @@ SOURCE_PLATFORM_CONFIG = "platform_config"
|
||||
FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1
|
||||
|
||||
|
||||
ENTITY_CATEGORIES_SCHEMA: Final = vol.In(ENTITY_CATEGORIES)
|
||||
def validate_entity_category(value: Any | None) -> EntityCategory:
|
||||
"""Validate entity category configuration."""
|
||||
value = vol.In(ENTITY_CATEGORIES)(value)
|
||||
return EntityCategory(value)
|
||||
|
||||
|
||||
ENTITY_CATEGORIES_SCHEMA = validate_entity_category
|
||||
|
||||
|
||||
@callback
|
||||
@@ -217,7 +223,10 @@ def convert_to_entity_category(
|
||||
"EntityCategory instead" % (type(value).__name__, value),
|
||||
error_if_core=False,
|
||||
)
|
||||
return EntityCategory(value)
|
||||
try:
|
||||
return EntityCategory(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ httpx==0.21.3
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.3
|
||||
paho-mqtt==1.6.1
|
||||
pillow==9.0.0
|
||||
pillow==9.0.1
|
||||
pip>=8.0.3,<20.3
|
||||
pyserial==3.5
|
||||
python-slugify==4.0.1
|
||||
|
||||
@@ -472,7 +472,10 @@ def color_rgbww_to_rgb(
|
||||
except ZeroDivisionError:
|
||||
ct_ratio = 0.5
|
||||
color_temp_mired = min_mireds + ct_ratio * mired_range
|
||||
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
|
||||
if color_temp_mired:
|
||||
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
|
||||
else:
|
||||
color_temp_kelvin = 0
|
||||
w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
|
||||
white_level = max(cw, ww) / 255
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ aio_georss_gdacs==0.5
|
||||
aioambient==2021.11.0
|
||||
|
||||
# homeassistant.components.aseko_pool_live
|
||||
aioaseko==0.0.1
|
||||
aioaseko==0.0.2
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.4.0
|
||||
@@ -166,7 +166,7 @@ aioeagle==1.1.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==10.8.1
|
||||
aioesphomeapi==10.8.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -254,7 +254,7 @@ aioridwell==2021.12.2
|
||||
aiosenseme==0.6.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==1.0.8
|
||||
aioshelly==1.0.9
|
||||
|
||||
# homeassistant.components.steamist
|
||||
aiosteamist==0.3.1
|
||||
@@ -308,7 +308,7 @@ amberelectric==1.0.3
|
||||
ambiclimate==0.2.1
|
||||
|
||||
# homeassistant.components.amcrest
|
||||
amcrest==1.9.3
|
||||
amcrest==1.9.4
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.63
|
||||
@@ -681,7 +681,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.20
|
||||
flux_led==0.28.22
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -764,7 +764,7 @@ google-cloud-pubsub==2.9.0
|
||||
google-cloud-texttospeech==0.4.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==1.6.0
|
||||
google-nest-sdm==1.7.1
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -914,7 +914,7 @@ influxdb-client==1.24.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==0.5
|
||||
intellifire4py==0.6
|
||||
|
||||
# homeassistant.components.iotawatt
|
||||
iotawattpy==0.1.0
|
||||
@@ -1049,7 +1049,7 @@ minio==5.0.10
|
||||
mitemp_bt==0.0.5
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.5.10
|
||||
motionblinds==0.5.12
|
||||
|
||||
# homeassistant.components.motioneye
|
||||
motioneye-client==0.3.12
|
||||
@@ -1249,13 +1249,13 @@ pilight==0.1.1
|
||||
# homeassistant.components.seven_segments
|
||||
# homeassistant.components.sighthound
|
||||
# homeassistant.components.tensorflow
|
||||
pillow==9.0.0
|
||||
pillow==9.0.1
|
||||
|
||||
# homeassistant.components.dominos
|
||||
pizzapi==0.0.3
|
||||
|
||||
# homeassistant.components.plex
|
||||
plexapi==4.9.1
|
||||
plexapi==4.9.2
|
||||
|
||||
# homeassistant.components.plex
|
||||
plexauth==0.0.6
|
||||
@@ -1310,7 +1310,7 @@ pushbullet.py==0.11.0
|
||||
pushover_complete==1.1.1
|
||||
|
||||
# homeassistant.components.pvoutput
|
||||
pvo==0.2.1
|
||||
pvo==0.2.2
|
||||
|
||||
# homeassistant.components.rpi_gpio_pwm
|
||||
pwmled==1.6.7
|
||||
@@ -1331,7 +1331,7 @@ py-nightscout==1.2.2
|
||||
py-schluter==0.1.7
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==1.0.5
|
||||
py-synologydsm-api==1.0.6
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
py-zabbix==1.1.7
|
||||
@@ -1500,7 +1500,7 @@ pyeight==0.2.0
|
||||
pyemby==1.8
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.3
|
||||
pyenvisalink==4.4
|
||||
|
||||
# homeassistant.components.ephember
|
||||
pyephember==0.3.1
|
||||
@@ -1954,7 +1954,7 @@ python-mpd2==3.0.4
|
||||
python-mystrom==1.1.2
|
||||
|
||||
# homeassistant.components.nest
|
||||
python-nest==4.1.0
|
||||
python-nest==4.2.0
|
||||
|
||||
# homeassistant.components.ozw
|
||||
python-openzwave-mqtt[mqtt-client]==1.4.0
|
||||
@@ -2190,7 +2190,7 @@ simplehound==0.3
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.01.0
|
||||
simplisafe-python==2022.02.1
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==3.1.2
|
||||
@@ -2228,7 +2228,7 @@ smhi-pkg==1.0.15
|
||||
snapcast==2.1.3
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.26.0
|
||||
soco==0.26.2
|
||||
|
||||
# homeassistant.components.solaredge_local
|
||||
solaredge-local==0.2.0
|
||||
@@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11
|
||||
xboxapi==2.0.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==0.19.1
|
||||
xknx==0.19.2
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@@ -2513,7 +2513,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.7
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.1.20
|
||||
yalexs==1.1.22
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.8
|
||||
|
||||
@@ -91,7 +91,7 @@ aio_georss_gdacs==0.5
|
||||
aioambient==2021.11.0
|
||||
|
||||
# homeassistant.components.aseko_pool_live
|
||||
aioaseko==0.0.1
|
||||
aioaseko==0.0.2
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.4.0
|
||||
@@ -119,7 +119,7 @@ aioeagle==1.1.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==10.8.1
|
||||
aioesphomeapi==10.8.2
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -189,7 +189,7 @@ aioridwell==2021.12.2
|
||||
aiosenseme==0.6.1
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==1.0.8
|
||||
aioshelly==1.0.9
|
||||
|
||||
# homeassistant.components.steamist
|
||||
aiosteamist==0.3.1
|
||||
@@ -427,7 +427,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.20
|
||||
flux_led==0.28.22
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@@ -492,7 +492,7 @@ google-api-python-client==1.6.4
|
||||
google-cloud-pubsub==2.9.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==1.6.0
|
||||
google-nest-sdm==1.7.1
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -586,7 +586,7 @@ influxdb-client==1.24.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.intellifire
|
||||
intellifire4py==0.5
|
||||
intellifire4py==0.6
|
||||
|
||||
# homeassistant.components.iotawatt
|
||||
iotawattpy==0.1.0
|
||||
@@ -655,7 +655,7 @@ millheater==0.9.0
|
||||
minio==5.0.10
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.5.10
|
||||
motionblinds==0.5.12
|
||||
|
||||
# homeassistant.components.motioneye
|
||||
motioneye-client==0.3.12
|
||||
@@ -771,10 +771,10 @@ pilight==0.1.1
|
||||
# homeassistant.components.seven_segments
|
||||
# homeassistant.components.sighthound
|
||||
# homeassistant.components.tensorflow
|
||||
pillow==9.0.0
|
||||
pillow==9.0.1
|
||||
|
||||
# homeassistant.components.plex
|
||||
plexapi==4.9.1
|
||||
plexapi==4.9.2
|
||||
|
||||
# homeassistant.components.plex
|
||||
plexauth==0.0.6
|
||||
@@ -814,7 +814,7 @@ pure-python-adb[async]==0.3.0.dev0
|
||||
pushbullet.py==0.11.0
|
||||
|
||||
# homeassistant.components.pvoutput
|
||||
pvo==0.2.1
|
||||
pvo==0.2.2
|
||||
|
||||
# homeassistant.components.canary
|
||||
py-canary==0.5.1
|
||||
@@ -829,7 +829,7 @@ py-melissa-climate==2.1.4
|
||||
py-nightscout==1.2.2
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==1.0.5
|
||||
py-synologydsm-api==1.0.6
|
||||
|
||||
# homeassistant.components.seventeentrack
|
||||
py17track==2021.12.2
|
||||
@@ -1206,7 +1206,7 @@ python-kasa==0.4.1
|
||||
python-miio==0.5.9.2
|
||||
|
||||
# homeassistant.components.nest
|
||||
python-nest==4.1.0
|
||||
python-nest==4.2.0
|
||||
|
||||
# homeassistant.components.ozw
|
||||
python-openzwave-mqtt[mqtt-client]==1.4.0
|
||||
@@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8
|
||||
simplehound==0.3
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.01.0
|
||||
simplisafe-python==2022.02.1
|
||||
|
||||
# homeassistant.components.slack
|
||||
slackclient==2.5.0
|
||||
@@ -1355,7 +1355,7 @@ smarthab==0.21
|
||||
smhi-pkg==1.0.15
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.26.0
|
||||
soco==0.26.2
|
||||
|
||||
# homeassistant.components.solaredge
|
||||
solaredge==0.0.2
|
||||
@@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==0.19.1
|
||||
xknx==0.19.2
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
@@ -1541,7 +1541,7 @@ xmltodict==0.12.0
|
||||
yalesmartalarmclient==0.3.7
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.1.20
|
||||
yalexs==1.1.22
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.8
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.2.2
|
||||
version = 2022.2.6
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"""Define patches used for androidtv tests."""
|
||||
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0
|
||||
|
||||
KEY_PYTHON = "python"
|
||||
KEY_SERVER = "server"
|
||||
|
||||
ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake"
|
||||
DEVICE_ASYNC_FAKE = "DeviceAsyncFake"
|
||||
|
||||
PROPS_DEV_INFO = "fake\nfake\n0123456\nfake"
|
||||
PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd"
|
||||
|
||||
|
||||
class AdbDeviceTcpAsyncFake:
|
||||
"""A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class."""
|
||||
@@ -100,12 +104,18 @@ def patch_connect(success):
|
||||
}
|
||||
|
||||
|
||||
def patch_shell(response=None, error=False):
|
||||
def patch_shell(response=None, error=False, mac_eth=False):
|
||||
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods."""
|
||||
|
||||
async def shell_success(self, cmd, *args, **kwargs):
|
||||
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful."""
|
||||
self.shell_cmd = cmd
|
||||
if cmd == CMD_DEVICE_PROPERTIES:
|
||||
return PROPS_DEV_INFO
|
||||
if cmd == CMD_MAC_WLAN0:
|
||||
return PROPS_DEV_MAC
|
||||
if cmd == CMD_MAC_ETH0:
|
||||
return PROPS_DEV_MAC if mac_eth else None
|
||||
return response
|
||||
|
||||
async def shell_fail_python(self, cmd, *args, **kwargs):
|
||||
@@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
|
||||
side_effect=ZeroDivisionError,
|
||||
)
|
||||
|
||||
PATCH_DEVICE_PROPERTIES = patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties",
|
||||
return_value={
|
||||
"manufacturer": "a",
|
||||
"model": "b",
|
||||
"serialno": "c",
|
||||
"sw_version": "d",
|
||||
"wifimac": "ab:cd:ef:gh:ij:kl",
|
||||
"ethmac": None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.components.androidtv.const import (
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
PROP_ETHMAC,
|
||||
PROP_WIFIMAC,
|
||||
)
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
@@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile
|
||||
|
||||
ADBKEY = "adbkey"
|
||||
ETH_MAC = "a1:b1:c1:d1:e1:f1"
|
||||
WIFI_MAC = "a2:b2:c2:d2:e2:f2"
|
||||
HOST = "127.0.0.1"
|
||||
VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}]
|
||||
|
||||
@@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch(
|
||||
class MockConfigDevice:
|
||||
"""Mock class to emulate Android TV device."""
|
||||
|
||||
def __init__(self, eth_mac=ETH_MAC):
|
||||
def __init__(self, eth_mac=ETH_MAC, wifi_mac=None):
|
||||
"""Initialize a fake device to test config flow."""
|
||||
self.available = True
|
||||
self.device_properties = {PROP_ETHMAC: eth_mac}
|
||||
self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac}
|
||||
|
||||
async def adb_close(self):
|
||||
"""Fake method to close connection."""
|
||||
self.available = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER])
|
||||
async def test_user(hass, config):
|
||||
@pytest.mark.parametrize(
|
||||
["config", "eth_mac", "wifi_mac"],
|
||||
[
|
||||
(CONFIG_PYTHON_ADB, ETH_MAC, None),
|
||||
(CONFIG_ADB_SERVER, ETH_MAC, None),
|
||||
(CONFIG_PYTHON_ADB, None, WIFI_MAC),
|
||||
(CONFIG_ADB_SERVER, None, WIFI_MAC),
|
||||
(CONFIG_PYTHON_ADB, ETH_MAC, WIFI_MAC),
|
||||
(CONFIG_ADB_SERVER, ETH_MAC, WIFI_MAC),
|
||||
],
|
||||
)
|
||||
async def test_user(hass, config, eth_mac, wifi_mac):
|
||||
"""Test user config."""
|
||||
flow_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}
|
||||
@@ -106,7 +118,7 @@ async def test_user(hass, config):
|
||||
# test with all provided
|
||||
with patch(
|
||||
CONNECT_METHOD,
|
||||
return_value=(MockConfigDevice(), None),
|
||||
return_value=(MockConfigDevice(eth_mac, wifi_mac), None),
|
||||
), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_result["flow_id"], user_input=config
|
||||
@@ -273,7 +285,7 @@ async def test_invalid_serial(hass):
|
||||
"""Test for invalid serialno."""
|
||||
with patch(
|
||||
CONNECT_METHOD,
|
||||
return_value=(MockConfigDevice(eth_mac=""), None),
|
||||
return_value=(MockConfigDevice(eth_mac=None), None),
|
||||
), PATCH_GET_HOST_IP:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
|
||||
@@ -142,29 +142,6 @@ def _setup(config):
|
||||
return patch_key, entity_id, config_entry
|
||||
|
||||
|
||||
async def test_setup_with_properties(hass):
|
||||
"""Test that setup succeeds with device properties.
|
||||
|
||||
the response must be a string with the following info separated with line break:
|
||||
"manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output"
|
||||
|
||||
"""
|
||||
|
||||
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
|
||||
config_entry.add_to_hass(hass)
|
||||
response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone"
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(response)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
@@ -190,9 +167,8 @@ async def test_reconnect(hass, caplog, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -259,9 +235,8 @@ async def test_adb_shell_returns_none(hass, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -289,9 +264,8 @@ async def test_setup_with_adbkey(hass):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -324,9 +298,8 @@ async def test_sources(hass, config0):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -404,9 +377,8 @@ async def _test_exclude_sources(hass, config0, expected_sources):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -486,9 +458,8 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch)
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -714,9 +685,8 @@ async def test_setup_fail(hass, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@@ -733,9 +703,8 @@ async def test_adb_command(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@@ -763,9 +732,8 @@ async def test_adb_command_unicode_decode_error(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell",
|
||||
@@ -793,9 +761,8 @@ async def test_adb_command_key(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@@ -823,9 +790,8 @@ async def test_adb_command_get_properties(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict",
|
||||
@@ -853,9 +819,8 @@ async def test_learn_sendevent(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent",
|
||||
@@ -882,9 +847,8 @@ async def test_update_lock_not_acquired(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@@ -918,9 +882,8 @@ async def test_download(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed download because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull:
|
||||
@@ -965,9 +928,8 @@ async def test_upload(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed upload because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push:
|
||||
@@ -1010,9 +972,8 @@ async def test_androidtv_volume_set(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5
|
||||
@@ -1038,9 +999,8 @@ async def test_get_image(hass, hass_ws_client):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell("11")[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@@ -1115,9 +1075,8 @@ async def test_services_androidtv(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(
|
||||
@@ -1162,9 +1121,8 @@ async def test_services_firetv(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back")
|
||||
@@ -1179,9 +1137,8 @@ async def test_volume_mute(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
@@ -1224,9 +1181,8 @@ async def test_connection_closed_on_ha_stop(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
|
||||
@@ -1249,9 +1205,8 @@ async def test_exception(hass):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
@@ -595,7 +595,7 @@ async def test_entity_availability(hass: HomeAssistant):
|
||||
conn_status_cb(connection_status)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
|
||||
connection_status = MagicMock()
|
||||
connection_status.status = "DISCONNECTED"
|
||||
@@ -624,7 +624,7 @@ async def test_entity_cast_status(hass: HomeAssistant):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# No media status, pause, play, stop not supported
|
||||
@@ -642,8 +642,8 @@ async def test_entity_cast_status(hass: HomeAssistant):
|
||||
cast_status_cb(cast_status)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
# Volume not hidden even if no app is active
|
||||
assert state.attributes.get("volume_level") == 0.5
|
||||
# Volume hidden if no app is active
|
||||
assert state.attributes.get("volume_level") is None
|
||||
assert not state.attributes.get("is_volume_muted")
|
||||
|
||||
chromecast.app_id = "1234"
|
||||
@@ -747,7 +747,7 @@ async def test_supported_features(
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert state.attributes.get("supported_features") == supported_features_no_media
|
||||
|
||||
media_status = MagicMock(images=None)
|
||||
@@ -882,7 +882,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# Play_media
|
||||
@@ -928,7 +928,7 @@ async def test_entity_play_media_cast(hass: HomeAssistant, quick_play_mock):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# Play_media - cast with app ID
|
||||
@@ -970,7 +970,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# play_media - media_type cast with invalid JSON
|
||||
@@ -1042,7 +1042,7 @@ async def test_entity_media_content_type(hass: HomeAssistant):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
media_status = MagicMock(images=None)
|
||||
@@ -1213,7 +1213,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
# App id updated, but no media status
|
||||
@@ -1258,7 +1258,7 @@ async def test_entity_media_states(hass: HomeAssistant, app_id, state_no_media):
|
||||
cast_status_cb(cast_status)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
|
||||
# No cast status
|
||||
chromecast.is_idle = False
|
||||
@@ -1286,7 +1286,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
chromecast.app_id = CAST_APP_ID_HOMEASSISTANT_LOVELACE
|
||||
@@ -1326,7 +1326,7 @@ async def test_entity_media_states_lovelace_app(hass: HomeAssistant):
|
||||
media_status_cb(media_status)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
|
||||
chromecast.is_idle = False
|
||||
media_status_cb(media_status)
|
||||
@@ -1355,7 +1355,7 @@ async def test_group_media_states(hass, mz_mock):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
group_media_status = MagicMock(images=None)
|
||||
@@ -1406,7 +1406,7 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock):
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.name == "Speaker"
|
||||
assert state.state == "idle"
|
||||
assert state.state == "off"
|
||||
assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid))
|
||||
|
||||
group_media_status = MagicMock(images=None)
|
||||
|
||||
@@ -61,3 +61,25 @@ async def test_sensor(
|
||||
assert state.attributes.get(ATTR_ARCH) == "aargh"
|
||||
assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7"
|
||||
assert state.attributes.get(ATTR_HZ) == 3.6
|
||||
|
||||
|
||||
async def test_sensor_partial_info(
|
||||
hass: HomeAssistant,
|
||||
mock_cpuinfo: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the CPU Speed sensor missing info."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Pop some info from the mocked CPUSpeed
|
||||
mock_cpuinfo.return_value.pop("brand_raw")
|
||||
mock_cpuinfo.return_value.pop("arch_string_raw")
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.cpu_speed")
|
||||
assert state
|
||||
assert state.state == "3.2"
|
||||
assert state.attributes.get(ATTR_ARCH) is None
|
||||
assert state.attributes.get(ATTR_BRAND) is None
|
||||
|
||||
@@ -255,6 +255,33 @@ async def test_dhcp_match_macaddress(hass):
|
||||
)
|
||||
|
||||
|
||||
async def test_dhcp_multiple_match_only_one_flow(hass):
|
||||
"""Test matching the domain multiple times only generates one flow."""
|
||||
integration_matchers = [
|
||||
{"domain": "mock-domain", "macaddress": "B8B7F1*"},
|
||||
{"domain": "mock-domain", "hostname": "connect"},
|
||||
]
|
||||
|
||||
packet = Ether(RAW_DHCP_REQUEST)
|
||||
|
||||
async_handle_dhcp_packet = await _async_get_handle_dhcp_packet(
|
||||
hass, integration_matchers
|
||||
)
|
||||
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
|
||||
await async_handle_dhcp_packet(packet)
|
||||
|
||||
assert len(mock_init.mock_calls) == 1
|
||||
assert mock_init.mock_calls[0][1][0] == "mock-domain"
|
||||
assert mock_init.mock_calls[0][2]["context"] == {
|
||||
"source": config_entries.SOURCE_DHCP
|
||||
}
|
||||
assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo(
|
||||
ip="192.168.210.56",
|
||||
hostname="connect",
|
||||
macaddress="b8b7f16db533",
|
||||
)
|
||||
|
||||
|
||||
async def test_dhcp_match_macaddress_without_hostname(hass):
|
||||
"""Test matching based on macaddress only."""
|
||||
integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}]
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
import respx
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
from homeassistant.components.camera import async_get_mjpeg_stream
|
||||
from homeassistant.components.generic import DOMAIN
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
@@ -515,3 +516,29 @@ async def test_no_still_image_url(hass, hass_client):
|
||||
mock_stream.async_get_image.assert_called_once()
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert await resp.read() == b"stream_keyframe_image"
|
||||
|
||||
|
||||
async def test_frame_interval_property(hass):
|
||||
"""Test that the frame interval is calculated and returned correctly."""
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"camera",
|
||||
{
|
||||
"camera": {
|
||||
"name": "config_test",
|
||||
"platform": "generic",
|
||||
"stream_source": "rtsp://example.com:554/rtsp/",
|
||||
"framerate": 5,
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
request = Mock()
|
||||
with patch(
|
||||
"homeassistant.components.camera.async_get_still_stream"
|
||||
) as mock_get_stream:
|
||||
await async_get_mjpeg_stream(hass, request, "camera.config_test")
|
||||
|
||||
assert mock_get_stream.call_args_list[0][0][3] == pytest.approx(0.2)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for the group cover platform."""
|
||||
from datetime import timedelta
|
||||
|
||||
import async_timeout
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
@@ -735,3 +736,52 @@ async def test_is_opening_closing(hass, setup_comp):
|
||||
assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING
|
||||
assert hass.states.get(DEMO_COVER_POS).state == STATE_OPEN
|
||||
assert hass.states.get(COVER_GROUP).state == STATE_OPENING
|
||||
|
||||
|
||||
async def test_nested_group(hass):
|
||||
"""Test nested cover group."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{
|
||||
DOMAIN: [
|
||||
{"platform": "demo"},
|
||||
{
|
||||
"platform": "group",
|
||||
"entities": ["cover.bedroom_group"],
|
||||
"name": "Nested Group",
|
||||
},
|
||||
{
|
||||
"platform": "group",
|
||||
CONF_ENTITIES: [DEMO_COVER_POS, DEMO_COVER_TILT],
|
||||
"name": "Bedroom Group",
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("cover.bedroom_group")
|
||||
assert state is not None
|
||||
assert state.state == STATE_OPEN
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == [DEMO_COVER_POS, DEMO_COVER_TILT]
|
||||
|
||||
state = hass.states.get("cover.nested_group")
|
||||
assert state is not None
|
||||
assert state.state == STATE_OPEN
|
||||
assert state.attributes.get(ATTR_ENTITY_ID) == ["cover.bedroom_group"]
|
||||
|
||||
# Test controlling the nested group
|
||||
async with async_timeout.timeout(0.5):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: "cover.nested_group"},
|
||||
blocking=True,
|
||||
)
|
||||
assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING
|
||||
assert hass.states.get(DEMO_COVER_TILT).state == STATE_CLOSING
|
||||
assert hass.states.get("cover.bedroom_group").state == STATE_CLOSING
|
||||
assert hass.states.get("cover.nested_group").state == STATE_CLOSING
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user