mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 16:57:19 +00:00
Merge pull request #76245 from home-assistant/rc
This commit is contained in:
commit
1dd701a89a
@ -388,6 +388,7 @@ omit =
|
||||
homeassistant/components/flume/__init__.py
|
||||
homeassistant/components/flume/sensor.py
|
||||
homeassistant/components/flunearyou/__init__.py
|
||||
homeassistant/components/flunearyou/repairs.py
|
||||
homeassistant/components/flunearyou/sensor.py
|
||||
homeassistant/components/folder/sensor.py
|
||||
homeassistant/components/folder_watcher/*
|
||||
|
@ -128,6 +128,7 @@ homeassistant.components.homekit.util
|
||||
homeassistant.components.homekit_controller
|
||||
homeassistant.components.homekit_controller.alarm_control_panel
|
||||
homeassistant.components.homekit_controller.button
|
||||
homeassistant.components.homekit_controller.config_flow
|
||||
homeassistant.components.homekit_controller.const
|
||||
homeassistant.components.homekit_controller.lock
|
||||
homeassistant.components.homekit_controller.select
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.39"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.41"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
@ -68,16 +68,19 @@ API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"}
|
||||
# back to HA state.
|
||||
API_THERMOSTAT_MODES = OrderedDict(
|
||||
[
|
||||
(climate.HVAC_MODE_HEAT, "HEAT"),
|
||||
(climate.HVAC_MODE_COOL, "COOL"),
|
||||
(climate.HVAC_MODE_HEAT_COOL, "AUTO"),
|
||||
(climate.HVAC_MODE_AUTO, "AUTO"),
|
||||
(climate.HVAC_MODE_OFF, "OFF"),
|
||||
(climate.HVAC_MODE_FAN_ONLY, "OFF"),
|
||||
(climate.HVAC_MODE_DRY, "CUSTOM"),
|
||||
(climate.HVACMode.HEAT, "HEAT"),
|
||||
(climate.HVACMode.COOL, "COOL"),
|
||||
(climate.HVACMode.HEAT_COOL, "AUTO"),
|
||||
(climate.HVACMode.AUTO, "AUTO"),
|
||||
(climate.HVACMode.OFF, "OFF"),
|
||||
(climate.HVACMode.FAN_ONLY, "CUSTOM"),
|
||||
(climate.HVACMode.DRY, "CUSTOM"),
|
||||
]
|
||||
)
|
||||
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
|
||||
API_THERMOSTAT_MODES_CUSTOM = {
|
||||
climate.HVACMode.DRY: "DEHUMIDIFY",
|
||||
climate.HVACMode.FAN_ONLY: "FAN",
|
||||
}
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
# AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode
|
||||
|
@ -188,7 +188,7 @@ async def async_process_advertisements(
|
||||
def _async_discovered_device(
|
||||
service_info: BluetoothServiceInfoBleak, change: BluetoothChange
|
||||
) -> None:
|
||||
if callback(service_info):
|
||||
if not done.done() and callback(service_info):
|
||||
done.set_result(service_info)
|
||||
|
||||
unload = async_register_callback(hass, _async_discovered_device, match_dict, mode)
|
||||
|
@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluetooth",
|
||||
"dependencies": ["websocket_api"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"],
|
||||
"requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
|
@ -9,6 +9,7 @@ from typing import Any
|
||||
from pyflunearyou import Client
|
||||
from pyflunearyou.errors import FluNearYouError
|
||||
|
||||
from homeassistant.components.repairs import IssueSeverity, async_create_issue
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -26,6 +27,15 @@ PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Flu Near You as config entry."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"integration_removal",
|
||||
is_fixable=True,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="integration_removal",
|
||||
)
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
client = Client(session=websession)
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
"name": "Flu Near You",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flunearyou",
|
||||
"dependencies": ["repairs"],
|
||||
"requirements": ["pyflunearyou==2.0.2"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
42
homeassistant/components/flunearyou/repairs.py
Normal file
42
homeassistant/components/flunearyou/repairs.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""Repairs platform for the Flu Near You integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class FluNearYouFixFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None:
|
||||
removal_tasks = [
|
||||
self.hass.config_entries.async_remove(entry.entry_id)
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||
]
|
||||
await asyncio.gather(*removal_tasks)
|
||||
return self.async_create_entry(title="Fixed issue", data={})
|
||||
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant, issue_id: str
|
||||
) -> FluNearYouFixFlow:
|
||||
"""Create flow."""
|
||||
return FluNearYouFixFlow()
|
@ -16,5 +16,18 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"integration_removal": {
|
||||
"title": "Flu Near You is no longer available",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Remove Flu Near You",
|
||||
"description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,5 +16,18 @@
|
||||
"title": "Configure Flu Near You"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"integration_removal": {
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.",
|
||||
"title": "Remove Flu Near You"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Flu Near You is no longer available"
|
||||
}
|
||||
}
|
||||
}
|
@ -105,27 +105,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert mac_address is not None
|
||||
mac = dr.format_mac(mac_address)
|
||||
await self.async_set_unique_id(mac)
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data[CONF_HOST] == device[ATTR_IPADDR] or (
|
||||
entry.unique_id
|
||||
and ":" in entry.unique_id
|
||||
and mac_matches_by_one(entry.unique_id, mac)
|
||||
for entry in self._async_current_entries(include_ignore=True):
|
||||
if not (
|
||||
entry.data.get(CONF_HOST) == device[ATTR_IPADDR]
|
||||
or (
|
||||
entry.unique_id
|
||||
and ":" in entry.unique_id
|
||||
and mac_matches_by_one(entry.unique_id, mac)
|
||||
)
|
||||
):
|
||||
if (
|
||||
async_update_entry_from_discovery(
|
||||
self.hass, entry, device, None, allow_update_mac
|
||||
)
|
||||
or entry.state == config_entries.ConfigEntryState.SETUP_RETRY
|
||||
):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(entry.entry_id)
|
||||
)
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
|
||||
)
|
||||
continue
|
||||
if entry.source == config_entries.SOURCE_IGNORE:
|
||||
raise AbortFlow("already_configured")
|
||||
if (
|
||||
async_update_entry_from_discovery(
|
||||
self.hass, entry, device, None, allow_update_mac
|
||||
)
|
||||
or entry.state == config_entries.ConfigEntryState.SETUP_RETRY
|
||||
):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(entry.entry_id)
|
||||
)
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
|
||||
)
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
async def _async_handle_discovery(self) -> FlowResult:
|
||||
"""Handle any discovery."""
|
||||
|
@ -223,12 +223,24 @@ HARDWARE_INTEGRATIONS = {
|
||||
async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict:
|
||||
"""Return add-on info.
|
||||
|
||||
The add-on must be installed.
|
||||
The caller of the function should handle HassioAPIError.
|
||||
"""
|
||||
hassio = hass.data[DOMAIN]
|
||||
return await hassio.get_addon_info(slug)
|
||||
|
||||
|
||||
@api_data
|
||||
async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict:
|
||||
"""Return add-on store info.
|
||||
|
||||
The caller of the function should handle HassioAPIError.
|
||||
"""
|
||||
hassio: HassIO = hass.data[DOMAIN]
|
||||
command = f"/store/addons/{slug}"
|
||||
return await hassio.send_command(command, method="get")
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict:
|
||||
"""Update Supervisor diagnostics toggle.
|
||||
|
@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from .config_flow import normalize_hkid
|
||||
from .connection import HKDevice, valid_serial_number
|
||||
from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS
|
||||
from .storage import async_get_entity_storage
|
||||
from .storage import EntityMapStorage, async_get_entity_storage
|
||||
from .utils import async_get_controller, folded_name
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -269,7 +269,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hkid = entry.data["AccessoryPairingID"]
|
||||
|
||||
if hkid in hass.data[KNOWN_DEVICES]:
|
||||
connection = hass.data[KNOWN_DEVICES][hkid]
|
||||
connection: HKDevice = hass.data[KNOWN_DEVICES][hkid]
|
||||
await connection.async_unload()
|
||||
|
||||
return True
|
||||
@ -280,7 +280,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
hkid = entry.data["AccessoryPairingID"]
|
||||
|
||||
# Remove cached type data from .storage/homekit_controller-entity-map
|
||||
hass.data[ENTITY_MAP].async_delete_map(hkid)
|
||||
entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP]
|
||||
entity_map_storage.async_delete_map(hkid)
|
||||
|
||||
controller = await async_get_controller(hass)
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
"""Config flow to configure homekit_controller."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import aiohomekit
|
||||
from aiohomekit import Controller, const as aiohomekit_const
|
||||
from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing
|
||||
from aiohomekit.controller.abstract import (
|
||||
AbstractDiscovery,
|
||||
AbstractPairing,
|
||||
FinishPairing,
|
||||
)
|
||||
from aiohomekit.exceptions import AuthenticationError
|
||||
from aiohomekit.model.categories import Categories
|
||||
from aiohomekit.model.status_flags import StatusFlags
|
||||
@ -17,7 +20,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
@ -78,7 +81,9 @@ def formatted_category(category: Categories) -> str:
|
||||
|
||||
|
||||
@callback
|
||||
def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None:
|
||||
def find_existing_host(
|
||||
hass: HomeAssistant, serial: str
|
||||
) -> config_entries.ConfigEntry | None:
|
||||
"""Return a set of the configured hosts."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.data.get("AccessoryPairingID") == serial:
|
||||
@ -115,15 +120,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.category: Categories | None = None
|
||||
self.devices: dict[str, AbstractDiscovery] = {}
|
||||
self.controller: Controller | None = None
|
||||
self.finish_pairing: Awaitable[AbstractPairing] | None = None
|
||||
self.finish_pairing: FinishPairing | None = None
|
||||
|
||||
async def _async_setup_controller(self):
|
||||
async def _async_setup_controller(self) -> None:
|
||||
"""Create the controller."""
|
||||
self.controller = await async_get_controller(self.hass)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow start."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
key = user_input["device"]
|
||||
@ -142,6 +149,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self.controller is None:
|
||||
await self._async_setup_controller()
|
||||
|
||||
assert self.controller
|
||||
|
||||
self.devices = {}
|
||||
|
||||
async for discovery in self.controller.async_discover():
|
||||
@ -167,7 +176,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_unignore(self, user_input):
|
||||
async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Rediscover a previously ignored discover."""
|
||||
unique_id = user_input["unique_id"]
|
||||
await self.async_set_unique_id(unique_id)
|
||||
@ -175,19 +184,21 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self.controller is None:
|
||||
await self._async_setup_controller()
|
||||
|
||||
assert self.controller
|
||||
|
||||
try:
|
||||
discovery = await self.controller.async_find(unique_id)
|
||||
except aiohomekit.AccessoryNotFoundError:
|
||||
return self.async_abort(reason="accessory_not_found_error")
|
||||
|
||||
self.name = discovery.description.name
|
||||
self.model = discovery.description.model
|
||||
self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME)
|
||||
self.category = discovery.description.category
|
||||
self.hkid = discovery.description.id
|
||||
|
||||
return self._async_step_pair_show_form()
|
||||
|
||||
async def _hkid_is_homekit(self, hkid):
|
||||
async def _hkid_is_homekit(self, hkid: str) -> bool:
|
||||
"""Determine if the device is a homekit bridge or accessory."""
|
||||
dev_reg = dr.async_get(self.hass)
|
||||
device = dev_reg.async_get_device(
|
||||
@ -410,7 +421,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self._async_step_pair_show_form()
|
||||
|
||||
async def async_step_pair(self, pair_info=None):
|
||||
async def async_step_pair(
|
||||
self, pair_info: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Pair with a new HomeKit accessory."""
|
||||
# If async_step_pair is called with no pairing code then we do the M1
|
||||
# phase of pairing. If this is successful the device enters pairing
|
||||
@ -428,11 +441,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# callable. We call the callable with the pin that the user has typed
|
||||
# in.
|
||||
|
||||
# Should never call this step without setting self.hkid
|
||||
assert self.hkid
|
||||
|
||||
errors = {}
|
||||
|
||||
if self.controller is None:
|
||||
await self._async_setup_controller()
|
||||
|
||||
assert self.controller
|
||||
|
||||
if pair_info and self.finish_pairing:
|
||||
self.context["pairing"] = True
|
||||
code = pair_info["pairing_code"]
|
||||
@ -507,21 +525,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self._async_step_pair_show_form(errors)
|
||||
|
||||
async def async_step_busy_error(self, user_input=None):
|
||||
async def async_step_busy_error(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Retry pairing after the accessory is busy."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_pair()
|
||||
|
||||
return self.async_show_form(step_id="busy_error")
|
||||
|
||||
async def async_step_max_tries_error(self, user_input=None):
|
||||
async def async_step_max_tries_error(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Retry pairing after the accessory has reached max tries."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_pair()
|
||||
|
||||
return self.async_show_form(step_id="max_tries_error")
|
||||
|
||||
async def async_step_protocol_error(self, user_input=None):
|
||||
async def async_step_protocol_error(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Retry pairing after the accessory has a protocol error."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_pair()
|
||||
@ -529,7 +553,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(step_id="protocol_error")
|
||||
|
||||
@callback
|
||||
def _async_step_pair_show_form(self, errors=None):
|
||||
def _async_step_pair_show_form(
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
assert self.category
|
||||
|
||||
placeholders = self.context["title_placeholders"] = {
|
||||
"name": self.name,
|
||||
"category": formatted_category(self.category),
|
||||
@ -569,7 +597,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
entity_storage = await async_get_entity_storage(self.hass)
|
||||
assert self.unique_id is not None
|
||||
entity_storage.async_create_or_update_map(
|
||||
self.unique_id,
|
||||
pairing.id,
|
||||
accessories_state.config_num,
|
||||
accessories_state.accessories.serialize(),
|
||||
)
|
||||
|
@ -107,9 +107,9 @@ class HomeKitLight(HomeKitEntity, LightEntity):
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode | str] | None:
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Flag supported color modes."""
|
||||
color_modes: set[ColorMode | str] = set()
|
||||
color_modes: set[ColorMode] = set()
|
||||
|
||||
if self.service.has(CharacteristicsTypes.HUE) or self.service.has(
|
||||
CharacteristicsTypes.SATURATION
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==1.2.3"],
|
||||
"requirements": ["aiohomekit==1.2.5"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -12,6 +13,7 @@ from .const import DOMAIN, ENTITY_MAP
|
||||
ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map"
|
||||
ENTITY_MAP_STORAGE_VERSION = 1
|
||||
ENTITY_MAP_SAVE_DELAY = 10
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pairing(TypedDict):
|
||||
@ -68,6 +70,7 @@ class EntityMapStorage:
|
||||
self, homekit_id: str, config_num: int, accessories: list[Any]
|
||||
) -> Pairing:
|
||||
"""Create a new pairing cache."""
|
||||
_LOGGER.debug("Creating or updating entity map for %s", homekit_id)
|
||||
data = Pairing(config_num=config_num, accessories=accessories)
|
||||
self.storage_data[homekit_id] = data
|
||||
self._async_schedule_save()
|
||||
@ -76,11 +79,17 @@ class EntityMapStorage:
|
||||
@callback
|
||||
def async_delete_map(self, homekit_id: str) -> None:
|
||||
"""Delete pairing cache."""
|
||||
if homekit_id not in self.storage_data:
|
||||
return
|
||||
|
||||
self.storage_data.pop(homekit_id)
|
||||
self._async_schedule_save()
|
||||
removed_one = False
|
||||
# Previously there was a bug where a lowercase homekit_id was stored
|
||||
# in the storage. We need to account for that.
|
||||
for hkid in (homekit_id, homekit_id.lower()):
|
||||
if hkid not in self.storage_data:
|
||||
continue
|
||||
_LOGGER.debug("Deleting entity map for %s", hkid)
|
||||
self.storage_data.pop(hkid)
|
||||
removed_one = True
|
||||
if removed_one:
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
|
@ -223,6 +223,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
== SensorDeviceClass.POWER
|
||||
):
|
||||
self._attr_device_class = SensorDeviceClass.ENERGY
|
||||
self._attr_icon = None
|
||||
update_state = True
|
||||
|
||||
if update_state:
|
||||
|
@ -91,9 +91,11 @@ class Life360Data:
|
||||
members: dict[str, Life360Member] = field(init=False, default_factory=dict)
|
||||
|
||||
|
||||
class Life360DataUpdateCoordinator(DataUpdateCoordinator):
|
||||
class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
|
||||
"""Life360 data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize data update coordinator."""
|
||||
super().__init__(
|
||||
|
@ -11,10 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_BATTERY_CHARGING
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
@ -31,6 +28,7 @@ from .const import (
|
||||
LOGGER,
|
||||
SHOW_DRIVING,
|
||||
)
|
||||
from .coordinator import Life360DataUpdateCoordinator, Life360Member
|
||||
|
||||
_LOC_ATTRS = (
|
||||
"address",
|
||||
@ -95,23 +93,27 @@ async def async_setup_entry(
|
||||
entry.async_on_unload(coordinator.async_add_listener(process_data))
|
||||
|
||||
|
||||
class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
|
||||
class Life360DeviceTracker(
|
||||
CoordinatorEntity[Life360DataUpdateCoordinator], TrackerEntity
|
||||
):
|
||||
"""Life360 Device Tracker."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_unique_id: str
|
||||
|
||||
def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None:
|
||||
def __init__(
|
||||
self, coordinator: Life360DataUpdateCoordinator, member_id: str
|
||||
) -> None:
|
||||
"""Initialize Life360 Entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = member_id
|
||||
|
||||
self._data = coordinator.data.members[self.unique_id]
|
||||
self._data: Life360Member | None = coordinator.data.members[member_id]
|
||||
self._prev_data = self._data
|
||||
|
||||
self._attr_name = self._data.name
|
||||
self._attr_entity_picture = self._data.entity_picture
|
||||
|
||||
self._prev_data = self._data
|
||||
|
||||
@property
|
||||
def _options(self) -> Mapping[str, Any]:
|
||||
"""Shortcut to config entry options."""
|
||||
@ -120,16 +122,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
# Get a shortcut to this member's data. Can't guarantee it's the same dict every
|
||||
# update, or that there is even data for this member every update, so need to
|
||||
# update shortcut each time.
|
||||
self._data = self.coordinator.data.members.get(self.unique_id)
|
||||
|
||||
# Get a shortcut to this Member's data. This needs to be updated each time since
|
||||
# coordinator provides a new Life360Member object each time, and it's possible
|
||||
# that there is no data for this Member on some updates.
|
||||
if self.available:
|
||||
# If nothing important has changed, then skip the update altogether.
|
||||
if self._data == self._prev_data:
|
||||
return
|
||||
self._data = self.coordinator.data.members.get(self._attr_unique_id)
|
||||
else:
|
||||
self._data = None
|
||||
|
||||
if self._data:
|
||||
# Check if we should effectively throw out new location data.
|
||||
last_seen = self._data.last_seen
|
||||
prev_seen = self._prev_data.last_seen
|
||||
@ -168,27 +169,21 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
|
||||
"""Return True if state updates should be forced."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
# Guard against member not being in last update for some reason.
|
||||
return super().available and self._data is not None
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the entity picture to use in the frontend, if any."""
|
||||
if self.available:
|
||||
if self._data:
|
||||
self._attr_entity_picture = self._data.entity_picture
|
||||
return super().entity_picture
|
||||
|
||||
# All of the following will only be called if self.available is True.
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level of the device.
|
||||
|
||||
Percentage from 0-100.
|
||||
"""
|
||||
if not self._data:
|
||||
return None
|
||||
return self._data.battery_level
|
||||
|
||||
@property
|
||||
@ -202,11 +197,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
|
||||
|
||||
Value in meters.
|
||||
"""
|
||||
if not self._data:
|
||||
return 0
|
||||
return self._data.gps_accuracy
|
||||
|
||||
@property
|
||||
def driving(self) -> bool:
|
||||
"""Return if driving."""
|
||||
if not self._data:
|
||||
return False
|
||||
if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None:
|
||||
if self._data.speed >= driving_speed:
|
||||
return True
|
||||
@ -222,23 +221,38 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity):
|
||||
@property
|
||||
def latitude(self) -> float | None:
|
||||
"""Return latitude value of the device."""
|
||||
if not self._data:
|
||||
return None
|
||||
return self._data.latitude
|
||||
|
||||
@property
|
||||
def longitude(self) -> float | None:
|
||||
"""Return longitude value of the device."""
|
||||
if not self._data:
|
||||
return None
|
||||
return self._data.longitude
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return entity specific state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ADDRESS] = self._data.address
|
||||
attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since
|
||||
attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging
|
||||
attrs[ATTR_DRIVING] = self.driving
|
||||
attrs[ATTR_LAST_SEEN] = self._data.last_seen
|
||||
attrs[ATTR_PLACE] = self._data.place
|
||||
attrs[ATTR_SPEED] = self._data.speed
|
||||
attrs[ATTR_WIFI_ON] = self._data.wifi_on
|
||||
return attrs
|
||||
if not self._data:
|
||||
return {
|
||||
ATTR_ADDRESS: None,
|
||||
ATTR_AT_LOC_SINCE: None,
|
||||
ATTR_BATTERY_CHARGING: None,
|
||||
ATTR_DRIVING: None,
|
||||
ATTR_LAST_SEEN: None,
|
||||
ATTR_PLACE: None,
|
||||
ATTR_SPEED: None,
|
||||
ATTR_WIFI_ON: None,
|
||||
}
|
||||
return {
|
||||
ATTR_ADDRESS: self._data.address,
|
||||
ATTR_AT_LOC_SINCE: self._data.at_loc_since,
|
||||
ATTR_BATTERY_CHARGING: self._data.battery_charging,
|
||||
ATTR_DRIVING: self.driving,
|
||||
ATTR_LAST_SEEN: self._data.last_seen,
|
||||
ATTR_PLACE: self._data.place,
|
||||
ATTR_SPEED: self._data.speed,
|
||||
ATTR_WIFI_ON: self._data.wifi_on,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "luci",
|
||||
"name": "OpenWRT (luci)",
|
||||
"name": "OpenWrt (luci)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/luci",
|
||||
"requirements": ["openwrt-luci-rpc==1.1.11"],
|
||||
"codeowners": ["@mzdrale"],
|
||||
|
@ -116,7 +116,7 @@ class MikrotikDataUpdateCoordinatorTracker(
|
||||
return self.device.mac
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str:
|
||||
def ip_address(self) -> str | None:
|
||||
"""Return the mac address of the client."""
|
||||
return self.device.ip_address
|
||||
|
||||
|
@ -60,9 +60,9 @@ class Device:
|
||||
return self._params.get("host-name", self.mac)
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str:
|
||||
def ip_address(self) -> str | None:
|
||||
"""Return device primary ip address."""
|
||||
return self._params["address"]
|
||||
return self._params.get("address")
|
||||
|
||||
@property
|
||||
def mac(self) -> str:
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "NextDNS",
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextdns",
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["nextdns==1.0.1"],
|
||||
"requirements": ["nextdns==1.0.2"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"]
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Risco",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"requirements": ["pyrisco==0.5.0"],
|
||||
"requirements": ["pyrisco==0.5.2"],
|
||||
"codeowners": ["@OnFreund"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling",
|
||||
|
@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -35,6 +36,7 @@ class RaspberryChargerBinarySensor(BinarySensorEntity):
|
||||
"""Binary sensor representing the rpi power status."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_icon = "mdi:raspberry-pi"
|
||||
_attr_name = "RPi Power status"
|
||||
_attr_unique_id = "rpi_power" # only one sensor possible
|
||||
|
@ -81,17 +81,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders={CONF_URL: self._oauth_values.auth_url},
|
||||
)
|
||||
|
||||
auth_code = user_input[CONF_AUTH_CODE]
|
||||
|
||||
if auth_code.startswith("="):
|
||||
# Sometimes, users may include the "=" from the URL query param; in that
|
||||
# case, strip it off and proceed:
|
||||
LOGGER.debug('Stripping "=" from the start of the authorization code')
|
||||
auth_code = auth_code[1:]
|
||||
|
||||
if len(auth_code) != 45:
|
||||
# SimpliSafe authorization codes are 45 characters in length; if the user
|
||||
# provides something different, stop them here:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_SCHEMA,
|
||||
errors={CONF_AUTH_CODE: "invalid_auth_code_length"},
|
||||
description_placeholders={CONF_URL: self._oauth_values.auth_url},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
simplisafe = await API.async_from_auth(
|
||||
user_input[CONF_AUTH_CODE],
|
||||
auth_code,
|
||||
self._oauth_values.code_verifier,
|
||||
session=session,
|
||||
)
|
||||
except InvalidCredentialsError:
|
||||
errors = {"base": "invalid_auth"}
|
||||
errors = {CONF_AUTH_CODE: "invalid_auth"}
|
||||
except SimplipyError as err:
|
||||
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
|
||||
errors = {"base": "unknown"}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.",
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.",
|
||||
"data": {
|
||||
"auth_code": "Authorization Code"
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
"error": {
|
||||
"identifier_exists": "Account already registered",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
|
@ -2,39 +2,21 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This SimpliSafe account is already in use.",
|
||||
"email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.",
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"wrong_account": "The user credentials provided do not match this SimpliSafe account."
|
||||
},
|
||||
"error": {
|
||||
"identifier_exists": "Account already registered",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"progress": {
|
||||
"email_2fa": "Check your email for a verification link from Simplisafe."
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Password"
|
||||
},
|
||||
"description": "Please re-enter the password for {username}.",
|
||||
"title": "Reauthenticate Integration"
|
||||
},
|
||||
"sms_2fa": {
|
||||
"data": {
|
||||
"code": "Code"
|
||||
},
|
||||
"description": "Input the two-factor authentication code sent to you via SMS."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"auth_code": "Authorization Code",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
"auth_code": "Authorization Code"
|
||||
},
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL."
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.components.hassio import (
|
||||
async_create_backup,
|
||||
async_get_addon_discovery_info,
|
||||
async_get_addon_info,
|
||||
async_get_addon_store_info,
|
||||
async_install_addon,
|
||||
async_restart_addon,
|
||||
async_set_addon_options,
|
||||
@ -136,7 +137,17 @@ class AddonManager:
|
||||
@api_error("Failed to get the Z-Wave JS add-on info")
|
||||
async def async_get_addon_info(self) -> AddonInfo:
|
||||
"""Return and cache Z-Wave JS add-on info."""
|
||||
addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG)
|
||||
addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG)
|
||||
LOGGER.debug("Add-on store info: %s", addon_store_info)
|
||||
if not addon_store_info["installed"]:
|
||||
return AddonInfo(
|
||||
options={},
|
||||
state=AddonState.NOT_INSTALLED,
|
||||
update_available=False,
|
||||
version=None,
|
||||
)
|
||||
|
||||
addon_info = await async_get_addon_info(self._hass, ADDON_SLUG)
|
||||
addon_state = self.async_get_addon_state(addon_info)
|
||||
return AddonInfo(
|
||||
options=addon_info["options"],
|
||||
@ -148,10 +159,8 @@ class AddonManager:
|
||||
@callback
|
||||
def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState:
|
||||
"""Return the current state of the Z-Wave JS add-on."""
|
||||
addon_state = AddonState.NOT_INSTALLED
|
||||
addon_state = AddonState.NOT_RUNNING
|
||||
|
||||
if addon_info["version"] is not None:
|
||||
addon_state = AddonState.NOT_RUNNING
|
||||
if addon_info["state"] == "started":
|
||||
addon_state = AddonState.RUNNING
|
||||
if self._install_task and not self._install_task.done():
|
||||
@ -226,7 +235,7 @@ class AddonManager:
|
||||
"""Update the Z-Wave JS add-on if needed."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if addon_info.version is None:
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
raise AddonError("Z-Wave JS add-on is not installed")
|
||||
|
||||
if not addon_info.update_available:
|
||||
@ -301,6 +310,9 @@ class AddonManager:
|
||||
"""Configure and start Z-Wave JS add-on."""
|
||||
addon_info = await self.async_get_addon_info()
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
raise AddonError("Z-Wave JS add-on is not installed")
|
||||
|
||||
new_addon_options = {
|
||||
CONF_ADDON_DEVICE: usb_path,
|
||||
CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
|
||||
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.6.0
|
||||
bcrypt==3.1.7
|
||||
bleak==0.15.0
|
||||
bleak==0.15.1
|
||||
bluetooth-adapters==0.1.3
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
|
11
mypy.ini
11
mypy.ini
@ -1131,6 +1131,17 @@ no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.homekit_controller.config_flow]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.homekit_controller.const]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.8.0"
|
||||
version = "2022.8.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -5,7 +5,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.aladdin_connect
|
||||
AIOAladdinConnect==0.1.39
|
||||
AIOAladdinConnect==0.1.41
|
||||
|
||||
# homeassistant.components.adax
|
||||
Adax-local==0.1.4
|
||||
@ -168,7 +168,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.2.3
|
||||
aiohomekit==1.2.5
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -405,7 +405,7 @@ bimmer_connected==0.10.1
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.15.0
|
||||
bleak==0.15.1
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.0.2
|
||||
@ -1103,7 +1103,7 @@ nextcloudmonitor==1.1.0
|
||||
nextcord==2.0.0a8
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==1.0.1
|
||||
nextdns==1.0.2
|
||||
|
||||
# homeassistant.components.niko_home_control
|
||||
niko-home-control==0.2.1
|
||||
@ -1784,7 +1784,7 @@ pyrecswitch==1.0.2
|
||||
pyrepetierng==0.1.0
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.5.0
|
||||
pyrisco==0.5.2
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
|
@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.aladdin_connect
|
||||
AIOAladdinConnect==0.1.39
|
||||
AIOAladdinConnect==0.1.41
|
||||
|
||||
# homeassistant.components.adax
|
||||
Adax-local==0.1.4
|
||||
@ -152,7 +152,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.2.3
|
||||
aiohomekit==1.2.5
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@ -326,7 +326,7 @@ bellows==0.31.2
|
||||
bimmer_connected==0.10.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.15.0
|
||||
bleak==0.15.1
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.0.2
|
||||
@ -784,7 +784,7 @@ nexia==2.0.2
|
||||
nextcord==2.0.0a8
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==1.0.1
|
||||
nextdns==1.0.2
|
||||
|
||||
# homeassistant.components.nfandroidtv
|
||||
notifications-android-tv==0.1.5
|
||||
@ -1228,7 +1228,7 @@ pyps4-2ndscreen==1.3.1
|
||||
pyqwikswitch==0.93
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.5.0
|
||||
pyrisco==0.5.2
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
|
@ -590,7 +590,7 @@ async def test_report_climate_state(hass):
|
||||
{"value": 34.0, "scale": "CELSIUS"},
|
||||
)
|
||||
|
||||
for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY):
|
||||
for off_modes in [climate.HVAC_MODE_OFF]:
|
||||
hass.states.async_set(
|
||||
"climate.downstairs",
|
||||
off_modes,
|
||||
@ -626,6 +626,23 @@ async def test_report_climate_state(hass):
|
||||
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
|
||||
)
|
||||
|
||||
# assert fan_only is reported as CUSTOM
|
||||
hass.states.async_set(
|
||||
"climate.downstairs",
|
||||
"fan_only",
|
||||
{
|
||||
"friendly_name": "Climate Downstairs",
|
||||
"supported_features": 91,
|
||||
climate.ATTR_CURRENT_TEMPERATURE: 31,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
},
|
||||
)
|
||||
properties = await reported_properties(hass, "climate.downstairs")
|
||||
properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM")
|
||||
properties.assert_equal(
|
||||
"Alexa.TemperatureSensor", "temperature", {"value": 31.0, "scale": "CELSIUS"}
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
"climate.heat",
|
||||
"heat",
|
||||
|
@ -2030,7 +2030,7 @@ async def test_thermostat(hass):
|
||||
"current_temperature": 75.0,
|
||||
"friendly_name": "Test Thermostat",
|
||||
"supported_features": 1 | 2 | 4 | 128,
|
||||
"hvac_modes": ["off", "heat", "cool", "auto", "dry"],
|
||||
"hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"],
|
||||
"preset_mode": None,
|
||||
"preset_modes": ["eco"],
|
||||
"min_temp": 50,
|
||||
@ -2220,7 +2220,7 @@ async def test_thermostat(hass):
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT")
|
||||
|
||||
# Assert we can call custom modes
|
||||
# Assert we can call custom modes for dry and fan_only
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.ThermostatController",
|
||||
"SetThermostatMode",
|
||||
@ -2233,6 +2233,18 @@ async def test_thermostat(hass):
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM")
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
"Alexa.ThermostatController",
|
||||
"SetThermostatMode",
|
||||
"climate#test_thermostat",
|
||||
"climate.set_hvac_mode",
|
||||
hass,
|
||||
payload={"thermostatMode": {"value": "CUSTOM", "customName": "FAN"}},
|
||||
)
|
||||
assert call.data["hvac_mode"] == "fan_only"
|
||||
properties = ReportedProperties(msg["context"]["properties"])
|
||||
properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM")
|
||||
|
||||
# assert unsupported custom mode
|
||||
msg = await assert_request_fails(
|
||||
"Alexa.ThermostatController",
|
||||
|
@ -856,6 +856,9 @@ async def test_process_advertisements_bail_on_good_advertisement(
|
||||
)
|
||||
|
||||
_get_underlying_scanner()._callback(device, adv)
|
||||
_get_underlying_scanner()._callback(device, adv)
|
||||
_get_underlying_scanner()._callback(device, adv)
|
||||
|
||||
await asyncio.sleep(0)
|
||||
|
||||
result = await handle
|
||||
|
@ -695,3 +695,30 @@ async def test_options(hass: HomeAssistant):
|
||||
assert result2["data"] == user_input
|
||||
assert result2["data"] == config_entry.options
|
||||
assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source, data",
|
||||
[
|
||||
(config_entries.SOURCE_DHCP, DHCP_DISCOVERY),
|
||||
(config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY),
|
||||
],
|
||||
)
|
||||
async def test_discovered_can_be_ignored(hass, source, data):
|
||||
"""Test we abort if the mac was already ignored."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
unique_id=MAC_ADDRESS,
|
||||
source=config_entries.SOURCE_IGNORE,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with _patch_discovery(), _patch_wifibulb():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": source}, data=data
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
@ -8,7 +8,12 @@ import pytest
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY
|
||||
from homeassistant.components.hassio import (
|
||||
ADDONS_COORDINATOR,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
async_get_addon_store_info,
|
||||
)
|
||||
from homeassistant.components.hassio.handler import HassioAPIError
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.helpers.device_registry import async_get
|
||||
@ -748,3 +753,16 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration):
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_get_store_addon_info(hass, hassio_stubs, aioclient_mock):
|
||||
"""Test get store add-on info from Supervisor API."""
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/store/addons/test",
|
||||
json={"result": "ok", "data": {"name": "bla"}},
|
||||
)
|
||||
|
||||
data = await async_get_addon_store_info(hass, "test")
|
||||
assert data["name"] == "bla"
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
@ -14,6 +14,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.homekit_controller import config_flow
|
||||
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
|
||||
from homeassistant.components.homekit_controller.storage import async_get_entity_storage
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
RESULT_TYPE_FORM,
|
||||
@ -1071,6 +1072,8 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller):
|
||||
async def test_bluetooth_valid_device_discovery_unpaired(hass, controller):
|
||||
"""Test bluetooth discovery with a homekit device and discovery works."""
|
||||
setup_mock_accessory(controller)
|
||||
storage = await async_get_entity_storage(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED",
|
||||
True,
|
||||
@ -1083,6 +1086,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller):
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pair"
|
||||
assert storage.get_map("00:00:00:00:00:00") is None
|
||||
|
||||
assert get_flow_context(hass, result) == {
|
||||
"source": config_entries.SOURCE_BLUETOOTH,
|
||||
@ -1098,3 +1102,5 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller):
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "Koogeek-LS1-20833F"
|
||||
assert result3["data"] == {}
|
||||
|
||||
assert storage.get_map("00:00:00:00:00:00") is not None
|
||||
|
@ -88,10 +88,12 @@ async def test_temperature_sensor_not_added_twice(hass, utcnow):
|
||||
hass, create_temperature_sensor_service, suffix="temperature"
|
||||
)
|
||||
|
||||
created_sensors = set()
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id.startswith("button"):
|
||||
continue
|
||||
assert state.entity_id == helper.entity_id
|
||||
if state.attributes.get("device_class") == SensorDeviceClass.TEMPERATURE:
|
||||
created_sensors.add(state.entity_id)
|
||||
|
||||
assert created_sensors == {helper.entity_id}
|
||||
|
||||
|
||||
async def test_humidity_sensor_read_state(hass, utcnow):
|
||||
|
@ -48,11 +48,13 @@ async def test_entry_diagnostics(
|
||||
}
|
||||
assert result["protocols_coordinator_data"] == {
|
||||
"doh_queries": 20,
|
||||
"doh3_queries": 0,
|
||||
"doq_queries": 10,
|
||||
"dot_queries": 30,
|
||||
"tcp_queries": 0,
|
||||
"udp_queries": 40,
|
||||
"doh_queries_ratio": 20.0,
|
||||
"doh3_queries_ratio": 0.0,
|
||||
"doq_queries_ratio": 10.0,
|
||||
"dot_queries_ratio": 30.0,
|
||||
"tcp_queries_ratio": 0.0,
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Define tests for the SimpliSafe config flow."""
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@ -10,6 +11,8 @@ from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME
|
||||
|
||||
VALID_AUTH_CODE = "code12345123451234512345123451234512345123451"
|
||||
|
||||
|
||||
async def test_duplicate_error(config_entry, hass, setup_simplisafe):
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
@ -23,12 +26,27 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_invalid_auth_code_length(hass):
|
||||
"""Test that an invalid auth code length show the correct error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"}
|
||||
|
||||
|
||||
async def test_invalid_credentials(hass):
|
||||
"""Test that invalid credentials show the correct error."""
|
||||
with patch(
|
||||
@ -42,10 +60,11 @@ async def test_invalid_credentials(hass):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"],
|
||||
user_input={CONF_AUTH_CODE: VALID_AUTH_CODE},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"}
|
||||
|
||||
|
||||
async def test_options_flow(config_entry, hass):
|
||||
@ -80,7 +99,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe):
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
), patch("homeassistant.config_entries.ConfigEntries.async_reload"):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
@ -104,14 +123,29 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe):
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
), patch("homeassistant.config_entries.ConfigEntries.async_reload"):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "wrong_account"
|
||||
|
||||
|
||||
async def test_step_user(hass, setup_simplisafe):
|
||||
"""Test the user step."""
|
||||
@pytest.mark.parametrize(
|
||||
"auth_code,log_statement",
|
||||
[
|
||||
(
|
||||
VALID_AUTH_CODE,
|
||||
None,
|
||||
),
|
||||
(
|
||||
f"={VALID_AUTH_CODE}",
|
||||
'Stripping "=" from the start of the authorization code',
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_step_user(auth_code, caplog, hass, log_statement, setup_simplisafe):
|
||||
"""Test successfully completion of the user step."""
|
||||
caplog.set_level = logging.DEBUG
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
@ -121,10 +155,13 @@ async def test_step_user(hass, setup_simplisafe):
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
), patch("homeassistant.config_entries.ConfigEntries.async_reload"):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: auth_code}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
if log_statement:
|
||||
assert any(m for m in caplog.messages if log_statement in m)
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
[config_entry] = hass.config_entries.async_entries(DOMAIN)
|
||||
assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"}
|
||||
@ -143,7 +180,7 @@ async def test_unknown_error(hass, setup_simplisafe):
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: "code123"}
|
||||
result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
@ -38,18 +38,56 @@ def mock_addon_info(addon_info_side_effect):
|
||||
yield addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_store_info_side_effect")
|
||||
def addon_store_info_side_effect_fixture():
|
||||
"""Return the add-on store info side effect."""
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_store_info")
|
||||
def mock_addon_store_info(addon_store_info_side_effect):
|
||||
"""Mock Supervisor add-on info."""
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.addon.async_get_addon_store_info",
|
||||
side_effect=addon_store_info_side_effect,
|
||||
) as addon_store_info:
|
||||
addon_store_info.return_value = {
|
||||
"installed": None,
|
||||
"state": None,
|
||||
"version": "1.0.0",
|
||||
}
|
||||
yield addon_store_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_running")
|
||||
def mock_addon_running(addon_info):
|
||||
def mock_addon_running(addon_store_info, addon_info):
|
||||
"""Mock add-on already running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "started",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["state"] = "started"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_installed")
|
||||
def mock_addon_installed(addon_info):
|
||||
def mock_addon_installed(addon_store_info, addon_info):
|
||||
"""Mock add-on already installed but not running."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_not_installed")
|
||||
def mock_addon_not_installed(addon_store_info, addon_info):
|
||||
"""Mock add-on not installed."""
|
||||
return addon_info
|
||||
|
||||
|
||||
@ -81,13 +119,18 @@ def mock_set_addon_options(set_addon_options_side_effect):
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon_side_effect")
|
||||
def install_addon_side_effect_fixture(addon_info):
|
||||
def install_addon_side_effect_fixture(addon_store_info, addon_info):
|
||||
"""Return the install add-on side effect."""
|
||||
|
||||
async def install_addon(hass, slug):
|
||||
"""Mock install add-on."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
|
||||
return install_addon
|
||||
|
||||
@ -112,11 +155,16 @@ def mock_update_addon():
|
||||
|
||||
|
||||
@pytest.fixture(name="start_addon_side_effect")
|
||||
def start_addon_side_effect_fixture(addon_info):
|
||||
def start_addon_side_effect_fixture(addon_store_info, addon_info):
|
||||
"""Return the start add-on options side effect."""
|
||||
|
||||
async def start_addon(hass, slug):
|
||||
"""Mock start add-on."""
|
||||
addon_store_info.return_value = {
|
||||
"installed": "1.0.0",
|
||||
"state": "started",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["state"] = "started"
|
||||
|
||||
return start_addon
|
||||
|
@ -422,7 +422,7 @@ async def test_abort_discovery_with_existing_entry(
|
||||
|
||||
|
||||
async def test_abort_hassio_discovery_with_existing_flow(
|
||||
hass, supervisor, addon_options
|
||||
hass, supervisor, addon_installed, addon_options
|
||||
):
|
||||
"""Test hassio discovery flow is aborted when another discovery has happened."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -701,15 +701,13 @@ async def test_discovery_addon_not_running(
|
||||
async def test_discovery_addon_not_installed(
|
||||
hass,
|
||||
supervisor,
|
||||
addon_installed,
|
||||
addon_not_installed,
|
||||
install_addon,
|
||||
addon_options,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
):
|
||||
"""Test discovery with add-on not installed."""
|
||||
addon_installed.return_value["version"] = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_HASSIO},
|
||||
@ -1443,7 +1441,7 @@ async def test_addon_installed_already_configured(
|
||||
async def test_addon_not_installed(
|
||||
hass,
|
||||
supervisor,
|
||||
addon_installed,
|
||||
addon_not_installed,
|
||||
install_addon,
|
||||
addon_options,
|
||||
set_addon_options,
|
||||
@ -1451,8 +1449,6 @@ async def test_addon_not_installed(
|
||||
get_addon_discovery_info,
|
||||
):
|
||||
"""Test add-on not installed."""
|
||||
addon_installed.return_value["version"] = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -1533,9 +1529,10 @@ async def test_addon_not_installed(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon):
|
||||
async def test_install_addon_failure(
|
||||
hass, supervisor, addon_not_installed, install_addon
|
||||
):
|
||||
"""Test add-on install failure."""
|
||||
addon_installed.return_value["version"] = None
|
||||
install_addon.side_effect = HassioAPIError()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -2292,7 +2289,7 @@ async def test_options_addon_not_installed(
|
||||
hass,
|
||||
client,
|
||||
supervisor,
|
||||
addon_installed,
|
||||
addon_not_installed,
|
||||
install_addon,
|
||||
integration,
|
||||
addon_options,
|
||||
@ -2306,7 +2303,6 @@ async def test_options_addon_not_installed(
|
||||
disconnect_calls,
|
||||
):
|
||||
"""Test options flow and add-on not installed on Supervisor."""
|
||||
addon_installed.return_value["version"] = None
|
||||
addon_options.update(old_addon_options)
|
||||
entry = integration
|
||||
entry.unique_id = "1234"
|
||||
|
@ -432,10 +432,14 @@ async def test_start_addon(
|
||||
|
||||
|
||||
async def test_install_addon(
|
||||
hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon
|
||||
hass,
|
||||
addon_not_installed,
|
||||
install_addon,
|
||||
addon_options,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
):
|
||||
"""Test install and start the Z-Wave JS add-on during entry setup."""
|
||||
addon_installed.return_value["version"] = None
|
||||
device = "/test"
|
||||
s0_legacy_key = "s0_legacy"
|
||||
s2_access_control_key = "s2_access_control"
|
||||
@ -583,10 +587,10 @@ async def test_addon_options_changed(
|
||||
"addon_version, update_available, update_calls, backup_calls, "
|
||||
"update_addon_side_effect, create_backup_side_effect",
|
||||
[
|
||||
("1.0", True, 1, 1, None, None),
|
||||
("1.0", False, 0, 0, None, None),
|
||||
("1.0", True, 1, 1, HassioAPIError("Boom"), None),
|
||||
("1.0", True, 0, 1, None, HassioAPIError("Boom")),
|
||||
("1.0.0", True, 1, 1, None, None),
|
||||
("1.0.0", False, 0, 0, None, None),
|
||||
("1.0.0", True, 1, 1, HassioAPIError("Boom"), None),
|
||||
("1.0.0", True, 0, 1, None, HassioAPIError("Boom")),
|
||||
],
|
||||
)
|
||||
async def test_update_addon(
|
||||
@ -720,7 +724,7 @@ async def test_remove_entry(
|
||||
assert create_backup.call_count == 1
|
||||
assert create_backup.call_args == call(
|
||||
hass,
|
||||
{"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
|
||||
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
|
||||
partial=True,
|
||||
)
|
||||
assert uninstall_addon.call_count == 1
|
||||
@ -762,7 +766,7 @@ async def test_remove_entry(
|
||||
assert create_backup.call_count == 1
|
||||
assert create_backup.call_args == call(
|
||||
hass,
|
||||
{"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
|
||||
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
|
||||
partial=True,
|
||||
)
|
||||
assert uninstall_addon.call_count == 0
|
||||
@ -786,7 +790,7 @@ async def test_remove_entry(
|
||||
assert create_backup.call_count == 1
|
||||
assert create_backup.call_args == call(
|
||||
hass,
|
||||
{"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]},
|
||||
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
|
||||
partial=True,
|
||||
)
|
||||
assert uninstall_addon.call_count == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user