Compare commits

..

50 Commits

Author SHA1 Message Date
Paulus Schoutsen
0fd38ef9f8 Merge pull request #76535 from home-assistant/rc 2022-08-09 21:55:49 -04:00
Paulus Schoutsen
56c80cd31a Bumped version to 2022.8.3 2022-08-09 17:18:19 -04:00
J. Nick Koston
303d8b05d1 Bump aiohomekit to 1.2.8 (#76532) 2022-08-09 17:18:12 -04:00
Oscar Calvo
972c05eac8 Fix #76283 (#76531) 2022-08-09 17:18:11 -04:00
J. Nick Koston
e5088d7e84 Fix pairing with HK accessories that do not provide format for vendor chars (#76502) 2022-08-09 17:18:10 -04:00
J. Nick Koston
6c1597ff98 Bump govee-ble to 0.14.0 to fix H5052 sensors (#76497) 2022-08-09 17:18:09 -04:00
J. Nick Koston
edac82487d Fix inkbird ibbq2s that identify with xbbq (#76492) 2022-08-09 17:18:09 -04:00
J. Nick Koston
5213148fa8 Bump aiohomekit to 1.2.6 (#76488) 2022-08-09 17:18:08 -04:00
Christopher Bailey
8e3f5ec470 Bump version of pyunifiprotect to 4.0.12 (#76465) 2022-08-09 17:14:01 -04:00
epenet
af90159e7c Fix iCloud listeners (#76437) 2022-08-09 17:14:01 -04:00
J. Nick Koston
f01b0a1a62 Fix Govee 5185 Meat Thermometers with older firmware not being discovered (#76414) 2022-08-09 17:13:02 -04:00
Aaron Bach
ee2acabcbe Fix bug where RainMachine entity states don't populate on startup (#76412) 2022-08-09 17:12:35 -04:00
Aaron Bach
79b371229d Automatically enable common RainMachine restriction entities (#76405)
Automatically enable common delay-related RainMachine entities
2022-08-09 17:09:05 -04:00
Aaron Bach
0f6b059e3e Add debug logging for unknown Notion errors (#76395)
* Add debug logging for unknown Notion errors

* Remove unused constant

* Code review
2022-08-09 17:09:04 -04:00
Paulus Schoutsen
bfb2867e8d Merge pull request #76398 from home-assistant/rc 2022-08-07 12:59:26 -04:00
Paulus Schoutsen
c9581f6a2e Bumped version to 2022.8.2 2022-08-07 12:13:08 -04:00
Joakim Plate
e96903fddf Postpone broadlink platform switch until config entry is ready (#76371) 2022-08-07 12:13:03 -04:00
Jean-François Roy
5026bff426 Bump aiobafi6 to 0.7.2 to unblock #76328 (#76330) 2022-08-07 12:13:02 -04:00
J. Nick Koston
4b63aa7f15 Bump pySwitchbot to 0.18.4 (#76322)
* Bump pySwitchbot to 0.18.3

Fixes #76321

Changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.17.3...0.18.3

* bump
2022-08-07 12:13:01 -04:00
David F. Mulcahey
1c2dd78e4c Fix ZHA light color temp support (#76305) 2022-08-07 12:13:00 -04:00
Robert Svensson
9cf11cf6ed Bump pydeconz to v102 (#76287) 2022-08-07 12:13:00 -04:00
puddly
8971a2073e Bump ZHA dependencies (#76275) 2022-08-07 12:12:59 -04:00
Maciej Bieniek
bfa64d2e01 Fix default sensor names in NextDNS integration (#76264) 2022-08-07 12:12:58 -04:00
J. Nick Koston
9c21d56539 Ensure bluetooth recovers if Dbus gets restarted (#76249) 2022-08-07 12:12:58 -04:00
Joakim Plate
8bfc352524 Use stored philips_js system data on start (#75981)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-08-07 12:12:57 -04:00
Jc2k
0e7bf35e4a Update gree to use the network component to set discovery interfaces (#75812) 2022-08-07 12:12:56 -04:00
Franck Nijhof
1dd701a89a Merge pull request #76245 from home-assistant/rc 2022-08-04 23:22:00 +02:00
Jc2k
d266b1ced6 Fix some homekit_controller pylint warnings and (local only) test failures (#76122) 2022-08-04 22:34:39 +02:00
Franck Nijhof
6727dab330 Bumped version to 2022.8.1 2022-08-04 21:51:02 +02:00
Jc2k
42509056bd Enable strict typing for HomeKit Controller config flow module (#76233) 2022-08-04 21:50:48 +02:00
Aaron Bach
a370e4f4b0 More explicitly call out special cases with SimpliSafe authorization code (#76232) 2022-08-04 21:50:44 +02:00
Phil Bruckner
a17e99f714 Fix Life360 recovery from server errors (#76231) 2022-08-04 21:50:41 +02:00
Franck Nijhof
db227a888d Fix spelling of OpenWrt in luci integration manifest (#76219) 2022-08-04 21:50:37 +02:00
mkmer
1808dd3d84 Bump AIOAladdin Connect to 0.1.41 (#76217) 2022-08-04 21:50:34 +02:00
Maciej Bieniek
31fed328ce Bump NextDNS library (#76207) 2022-08-04 21:50:30 +02:00
J. Nick Koston
1a030f118a BLE pairing reliablity fixes for HomeKit Controller (#76199)
- Remove the cached map from memory when unpairing so
  we do not reuse it again if they unpair/repair

- Fixes for accessories that use a config number of
  0

- General reliablity improvements to the pairing process
  under the hood of aiohomekit
2022-08-04 21:50:27 +02:00
Franck Nijhof
a4049e93d8 Mark RPI Power binary sensor as diagnostic (#76198) 2022-08-04 21:50:23 +02:00
Rami Mosleh
854ca853dc Fix nullable ip_address in mikrotik (#76197) 2022-08-04 21:50:20 +02:00
On Freund
2710e4b5ec Fix arm away in Risco (#76188) 2022-08-04 21:50:16 +02:00
Aaron Bach
450af52bac Add repair item to remove no-longer-functioning Flu Near You integration (#76177)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-08-04 21:50:13 +02:00
J. Nick Koston
60da54558e Fix race in bluetooth async_process_advertisements (#76176) 2022-08-04 21:50:09 +02:00
J. Nick Koston
11319defae Fix flux_led ignored entries not being respected (#76173) 2022-08-04 21:50:06 +02:00
Diogo Gomes
6340da72a5 Remove icon attribute if device class is set (#76161) 2022-08-04 21:50:02 +02:00
Jan Bouwhuis
5c9d557b10 Allow climate operation mode fan_only as custom mode in Alexa (#76148)
* Add support for FAN_ONLY mode

* Tests for fan_only as custom mode
2022-08-04 21:49:21 +02:00
J. Nick Koston
d2955a48b0 Bump bleak to 0.15.1 (#76136) 2022-08-04 21:47:15 +02:00
Martin Hjelmare
d2b98fa285 Fix zwave_js addon info (#76044)
* Add add-on store info command

* Use add-on store info command in zwave_js

* Fix init tests

* Update tests

* Fix method for addon store info

* Fix response parsing

* Fix store addon installed response parsing

* Remove addon info log that can contain network keys

* Add supervisor store addon info test

* Default to version None if add-on not installed

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2022-08-04 21:47:10 +02:00
Franck Nijhof
8ef3ca2daf Merge pull request #76119 from home-assistant/rc 2022-08-03 14:47:19 +02:00
Franck Nijhof
80a053a4cd Bumped version to 2022.8.0 2022-08-03 11:42:18 +02:00
Robert Svensson
81ee24738b Fix deconz group log warning (#76114) 2022-08-03 11:25:07 +02:00
Heine Furubotten
29f6d7818a Bump azure-servicebus to support py3.10 (#76092)
Bump azure-servicebus
2022-08-03 11:25:03 +02:00
76 changed files with 1181 additions and 294 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"domain": "azure_service_bus",
"name": "Azure Service Bus",
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
"requirements": ["azure-servicebus==0.50.3"],
"requirements": ["azure-servicebus==7.8.0"],
"codeowners": ["@hfurubotten"],
"iot_class": "cloud_push",
"loggers": ["azure"]

View File

@@ -2,11 +2,12 @@
import json
import logging
from azure.servicebus.aio import Message, ServiceBusClient
from azure.servicebus.common.errors import (
MessageSendFailed,
from azure.servicebus import ServiceBusMessage
from azure.servicebus.aio import ServiceBusClient
from azure.servicebus.exceptions import (
MessagingEntityNotFoundError,
ServiceBusConnectionError,
ServiceBusResourceNotFound,
ServiceBusError,
)
import voluptuous as vol
@@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None):
try:
if queue_name:
client = servicebus.get_queue(queue_name)
client = servicebus.get_queue_sender(queue_name)
else:
client = servicebus.get_topic(topic_name)
except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err:
client = servicebus.get_topic_sender(topic_name)
except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err:
_LOGGER.error(
"Connection error while creating client for queue/topic '%s'. %s",
queue_name or topic_name,
@@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService):
if data := kwargs.get(ATTR_DATA):
dto.update(data)
queue_message = Message(json.dumps(dto))
queue_message.properties.content_type = CONTENT_TYPE_JSON
queue_message = ServiceBusMessage(
json.dumps(dto), content_type=CONTENT_TYPE_JSON
)
try:
await self._client.send(queue_message)
except MessageSendFailed as err:
await self._client.send_messages(queue_message)
except ServiceBusError as err:
_LOGGER.error(
"Could not send service bus notification to %s. %s",
self._client.name,

View File

@@ -3,7 +3,7 @@
"name": "Big Ass Fans",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf",
"requirements": ["aiobafi6==0.7.0"],
"requirements": ["aiobafi6==0.7.2"],
"codeowners": ["@bdraco", "@jfroy"],
"iot_class": "local_push",
"zeroconf": [

View File

@@ -8,6 +8,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
import logging
import time
from typing import TYPE_CHECKING, Final
import async_timeout
@@ -56,6 +57,10 @@ START_TIMEOUT = 9
SOURCE_LOCAL: Final = "local"
SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT)
MONOTONIC_TIME = time.monotonic
@dataclass
class BluetoothServiceInfoBleak(BluetoothServiceInfo):
@@ -188,7 +193,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)
@@ -252,9 +257,10 @@ async def async_setup_entry(
) -> bool:
"""Set up the bluetooth integration from a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_start(
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
async with manager.start_stop_lock:
await manager.async_start(
BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER)
)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
@@ -263,8 +269,6 @@ async def _async_update_listener(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> None:
"""Handle options update."""
manager: BluetoothManager = hass.data[DOMAIN]
manager.async_start_reload()
await hass.config_entries.async_reload(entry.entry_id)
@@ -273,7 +277,9 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
manager: BluetoothManager = hass.data[DOMAIN]
await manager.async_stop()
async with manager.start_stop_lock:
manager.async_start_reload()
await manager.async_stop()
return True
@@ -289,13 +295,19 @@ class BluetoothManager:
self.hass = hass
self._integration_matcher = integration_matcher
self.scanner: HaBleakScanner | None = None
self.start_stop_lock = asyncio.Lock()
self._cancel_device_detected: CALLBACK_TYPE | None = None
self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
self._cancel_stop: CALLBACK_TYPE | None = None
self._cancel_watchdog: CALLBACK_TYPE | None = None
self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {}
self._callbacks: list[
tuple[BluetoothCallback, BluetoothCallbackMatcher | None]
] = []
self._last_detection = 0.0
self._reloading = False
self._adapter: str | None = None
self._scanning_mode = BluetoothScanningMode.ACTIVE
@hass_callback
def async_setup(self) -> None:
@@ -317,6 +329,8 @@ class BluetoothManager:
) -> None:
"""Set up BT Discovery."""
assert self.scanner is not None
self._adapter = adapter
self._scanning_mode = scanning_mode
if self._reloading:
# On reload, we need to reset the scanner instance
# since the devices in its history may not be reachable
@@ -381,7 +395,32 @@ class BluetoothManager:
_LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True)
raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex
self.async_setup_unavailable_tracking()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
self._async_setup_scanner_watchdog()
self._cancel_stop = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping
)
@hass_callback
def _async_setup_scanner_watchdog(self) -> None:
"""If Dbus gets restarted or updated, we need to restart the scanner."""
self._last_detection = MONOTONIC_TIME()
self._cancel_watchdog = async_track_time_interval(
self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL
)
async def _async_scanner_watchdog(self, now: datetime) -> None:
"""Check if the scanner is running."""
time_since_last_detection = MONOTONIC_TIME() - self._last_detection
if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT:
return
_LOGGER.info(
"Bluetooth scanner has gone quiet for %s, restarting",
SCANNER_WATCHDOG_INTERVAL,
)
async with self.start_stop_lock:
self.async_start_reload()
await self.async_stop()
await self.async_start(self._scanning_mode, self._adapter)
@hass_callback
def async_setup_unavailable_tracking(self) -> None:
@@ -416,6 +455,7 @@ class BluetoothManager:
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""Handle a detected device."""
self._last_detection = MONOTONIC_TIME()
matched_domains = self._integration_matcher.match_domains(
device, advertisement_data
)
@@ -528,14 +568,26 @@ class BluetoothManager:
for device_adv in self.scanner.history.values()
]
async def async_stop(self, event: Event | None = None) -> None:
async def _async_hass_stopping(self, event: Event) -> None:
"""Stop the Bluetooth integration at shutdown."""
self._cancel_stop = None
await self.async_stop()
async def async_stop(self) -> None:
"""Stop bluetooth discovery."""
_LOGGER.debug("Stopping bluetooth discovery")
if self._cancel_watchdog:
self._cancel_watchdog()
self._cancel_watchdog = None
if self._cancel_device_detected:
self._cancel_device_detected()
self._cancel_device_detected = None
if self._cancel_unavailable_tracking:
self._cancel_unavailable_tracking()
self._cancel_unavailable_tracking = None
if self._cancel_stop:
self._cancel_stop()
self._cancel_stop = None
if self.scanner:
try:
await self.scanner.stop() # type: ignore[no-untyped-call]

View File

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

View File

@@ -26,11 +26,13 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import BroadlinkDevice
from .const import DOMAIN
from .entity import BroadlinkEntity
from .helpers import data_packet, import_device, mac_address
@@ -80,8 +82,18 @@ async def async_setup_platform(
host = config.get(CONF_HOST)
if switches := config.get(CONF_SWITCHES):
platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {})
platform_data.setdefault(mac_addr, []).extend(switches)
platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {})
async_add_entities_config_entry: AddEntitiesCallback
device: BroadlinkDevice
async_add_entities_config_entry, device = platform_data.get(
mac_addr, (None, None)
)
if not async_add_entities_config_entry:
raise PlatformNotReady
async_add_entities_config_entry(
BroadlinkRMSwitch(device, config) for config in switches
)
else:
_LOGGER.warning(
@@ -104,12 +116,8 @@ async def async_setup_entry(
switches: list[BroadlinkSwitch] = []
if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}:
platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {})
user_defined_switches = platform_data.get(device.api.mac, {})
switches.extend(
BroadlinkRMSwitch(device, config) for config in user_defined_switches
)
platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {})
platform_data[device.api.mac] = async_add_entities, device
elif device.api.type == "SP1":
switches.append(BroadlinkSP1Switch(device))

View File

@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==100"],
"requirements": ["pydeconz==102"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,10 @@
"manufacturer_id": 18994,
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
},
{
"manufacturer_id": 818,
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
},
{
"manufacturer_id": 14474,
"service_uuid": "00008151-0000-1000-8000-00805f9b34fb"
@@ -24,7 +28,7 @@
"service_uuid": "00008251-0000-1000-8000-00805f9b34fb"
}
],
"requirements": ["govee-ble==0.12.6"],
"requirements": ["govee-ble==0.14.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"

View File

@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_scan_update(_=None):
await gree_discovery.discovery.scan()
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr)
_LOGGER.debug("Scanning network for Gree devices")
await _async_scan_update()

View File

@@ -1,6 +1,7 @@
"""Config flow for Gree."""
from greeclimate.discovery import Discovery
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow
@@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN
async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
gree_discovery = Discovery(DISCOVERY_TIMEOUT)
devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT)
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
devices = await gree_discovery.scan(
wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr
)
return len(devices) > 0

View File

@@ -3,7 +3,8 @@
"name": "Gree Climate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gree",
"requirements": ["greeclimate==1.2.0"],
"requirements": ["greeclimate==1.3.0"],
"dependencies": ["network"],
"codeowners": ["@cmroche"],
"iot_class": "local_polling",
"loggers": ["greeclimate"]

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.8"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],

View File

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

View File

@@ -17,7 +17,7 @@ from pyicloud.services.findmyiphone import AppleDevice
from homeassistant.components.zone import async_active_zone
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_point_in_utc_time
@@ -104,6 +104,8 @@ class IcloudAccount:
self._retried_fetch = False
self._config_entry = config_entry
self.listeners: list[CALLBACK_TYPE] = []
def setup(self) -> None:
"""Set up an iCloud account."""
try:

View File

@@ -35,7 +35,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up device tracker for iCloud component."""
account = hass.data[DOMAIN][entry.unique_id]
account: IcloudAccount = hass.data[DOMAIN][entry.unique_id]
tracked = set[str]()
@callback

View File

@@ -20,7 +20,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up device tracker for iCloud component."""
account = hass.data[DOMAIN][entry.unique_id]
account: IcloudAccount = hass.data[DOMAIN][entry.unique_id]
tracked = set[str]()
@callback

View File

@@ -7,9 +7,10 @@
{ "local_name": "sps" },
{ "local_name": "Inkbird*" },
{ "local_name": "iBBQ*" },
{ "local_name": "xBBQ*" },
{ "local_name": "tps" }
],
"requirements": ["inkbird-ble==0.5.1"],
"requirements": ["inkbird-ble==0.5.2"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries",
name="TCP queries",
native_unit_of_measurement="queries",
state_class=SensorStateClass.TOTAL,
value=lambda data: data.tcp_queries,
@@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
icon="mdi:dns",
name="TCP Queries Ratio",
name="TCP queries ratio",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.tcp_queries_ratio,

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
import traceback
from typing import Any
from aionotion import async_get_client
@@ -31,7 +33,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
ATTR_SYSTEM_MODE = "system_mode"
ATTR_SYSTEM_NAME = "system_name"
DEFAULT_ATTRIBUTION = "Data provided by Notion"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -75,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"There was a Notion error while updating {attr}: {result}"
) from result
if isinstance(result, Exception):
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("".join(traceback.format_tb(result.__traceback__)))
raise UpdateFailed(
f"There was an unknown error while updating {attr}: {result}"
) from result

View File

@@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Philips TV from a config entry."""
system: SystemType | None = entry.data.get(CONF_SYSTEM)
tvapi = PhilipsTV(
entry.data[CONF_HOST],
entry.data[CONF_API_VERSION],
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
system=system,
)
coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options)
await coordinator.async_refresh()
if (actual_system := tvapi.system) and actual_system != system:
data = {**entry.data, CONF_SYSTEM: actual_system}
hass.config_entries.async_update_entry(entry, data=data)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator

View File

@@ -437,7 +437,7 @@ class RainMachineEntity(CoordinatorEntity):
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
"""When entity is added to hass."""
await super().async_added_to_hass()
self.update_from_latest_data()

View File

@@ -77,7 +77,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Hourly restrictions",
icon="mdi:cancel",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
data_key="hourly",
),
@@ -86,7 +85,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Month restrictions",
icon="mdi:cancel",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
data_key="month",
),
@@ -95,7 +93,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Rain delay restrictions",
icon="mdi:cancel",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
data_key="rainDelay",
),
@@ -113,7 +110,6 @@ BINARY_SENSOR_DESCRIPTIONS = (
name="Weekday restrictions",
icon="mdi:cancel",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
api_category=DATA_RESTRICTIONS_CURRENT,
data_key="weekDay",
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,31 +20,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the SMS notification service."""
if SMS_GATEWAY not in hass.data[DOMAIN]:
_LOGGER.error("SMS gateway not found, cannot initialize service")
return
gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY]
if discovery_info is None:
number = config[CONF_RECIPIENT]
else:
number = discovery_info[CONF_RECIPIENT]
return SMSNotificationService(gateway, number)
return SMSNotificationService(hass, number)
class SMSNotificationService(BaseNotificationService):
"""Implement the notification service for SMS."""
def __init__(self, gateway, number):
def __init__(self, hass, number):
"""Initialize the service."""
self.gateway = gateway
self.hass = hass
self.number = number
async def async_send_message(self, message="", **kwargs):
"""Send SMS message."""
if SMS_GATEWAY not in self.hass.data[DOMAIN]:
_LOGGER.error("SMS gateway not found, cannot send message")
return
gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY]
targets = kwargs.get(CONF_TARGET, [self.number])
smsinfo = {
"Class": -1,
@@ -67,6 +68,6 @@ class SMSNotificationService(BaseNotificationService):
encoded_message["Number"] = target
try:
# Actually send the message
await self.gateway.send_sms_async(encoded_message)
await gateway.send_sms_async(encoded_message)
except gammu.GSMError as exc:
_LOGGER.error("Sending to %s failed: %s", target, exc)

View File

@@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.17.3"],
"requirements": ["PySwitchbot==0.18.4"],
"config_flow": true,
"dependencies": ["bluetooth"],
"codeowners": [

View File

@@ -3,7 +3,7 @@
"name": "UniFi Protect",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"],
"requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"],
"dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum",

View File

@@ -1,7 +1,7 @@
"""Lighting channels module for Zigbee Home Automation."""
from __future__ import annotations
from contextlib import suppress
from functools import cached_property
from zigpy.zcl.clusters import lighting
@@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel):
"color_loop_active": False,
}
@property
def color_capabilities(self) -> int:
"""Return color capabilities of the light."""
with suppress(KeyError):
return self.cluster["color_capabilities"]
if self.cluster.get("color_temperature") is not None:
return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
return self.CAPABILITIES_COLOR_XY
@property
def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities:
@cached_property
def color_capabilities(self) -> lighting.Color.ColorCapabilities:
"""Return ZCL color capabilities of the light."""
color_capabilities = self.cluster.get("color_capabilities")
if color_capabilities is None:
@@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel):
def hs_supported(self) -> bool:
"""Return True if the channel supports hue and saturation."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Hue_and_saturation
in self.zcl_color_capabilities
in self.color_capabilities
)
@property
def enhanced_hue_supported(self) -> bool:
"""Return True if the channel supports enhanced hue and saturation."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue
in self.zcl_color_capabilities
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities
)
@property
def xy_supported(self) -> bool:
"""Return True if the channel supports xy."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.XY_attributes
in self.zcl_color_capabilities
in self.color_capabilities
)
@property
def color_temp_supported(self) -> bool:
"""Return True if the channel supports color temperature."""
return (
self.zcl_color_capabilities is not None
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_temperature
in self.zcl_color_capabilities
)
in self.color_capabilities
) or self.color_temperature is not None
@property
def color_loop_supported(self) -> bool:
"""Return True if the channel supports color loop."""
return (
self.zcl_color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop
in self.zcl_color_capabilities
self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
)

View File

@@ -4,14 +4,14 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.31.2",
"bellows==0.31.3",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.78",
"zigpy-deconz==0.18.0",
"zigpy==0.48.0",
"zigpy==0.49.0",
"zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.0",
"zigpy-zigate==0.9.1",
"zigpy-znp==0.8.1"
],
"usb": [

View File

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

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "0b7"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -41,6 +41,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [
"manufacturer_id": 18994,
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
},
{
"domain": "govee_ble",
"manufacturer_id": 818,
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb"
},
{
"domain": "govee_ble",
"manufacturer_id": 14474,
@@ -70,6 +75,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [
"domain": "inkbird",
"local_name": "iBBQ*"
},
{
"domain": "inkbird",
"local_name": "xBBQ*"
},
{
"domain": "inkbird",
"local_name": "tps"

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2022.8.0b7"
version = "2022.8.3"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@@ -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
@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.17.3
PySwitchbot==0.18.4
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -128,7 +128,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5
# homeassistant.components.baf
aiobafi6==0.7.0
aiobafi6==0.7.2
# homeassistant.components.aws
aiobotocore==2.1.0
@@ -168,7 +168,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==1.2.3
aiohomekit==1.2.8
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -375,7 +375,7 @@ axis==44
azure-eventhub==5.7.0
# homeassistant.components.azure_service_bus
azure-servicebus==0.50.3
azure-servicebus==7.8.0
# homeassistant.components.baidu
baidu-aip==1.6.6
@@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
# beewi_smartclim==0.0.10
# homeassistant.components.zha
bellows==0.31.2
bellows==0.31.3
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.1
@@ -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
@@ -760,7 +760,7 @@ googlemaps==2.5.1
goslide-api==0.5.1
# homeassistant.components.govee_ble
govee-ble==0.12.6
govee-ble==0.14.0
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@@ -769,7 +769,7 @@ gpiozero==1.6.2
gps3==0.33.3
# homeassistant.components.gree
greeclimate==1.2.0
greeclimate==1.3.0
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -902,7 +902,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.inkbird
inkbird-ble==0.5.1
inkbird-ble==0.5.2
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.2.0
@@ -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
@@ -1455,7 +1455,7 @@ pydaikin==2.7.0
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==100
pydeconz==102
# homeassistant.components.delijn
pydelijn==1.0.0
@@ -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
@@ -2003,7 +2003,7 @@ pytrafikverket==0.2.0.1
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.0.11
pyunifiprotect==4.0.12
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@@ -2529,13 +2529,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0
# homeassistant.components.zha
zigpy-zigate==0.9.0
zigpy-zigate==0.9.1
# homeassistant.components.zha
zigpy-znp==0.8.1
# homeassistant.components.zha
zigpy==0.48.0
zigpy==0.49.0
# homeassistant.components.zoneminder
zm-py==0.5.2

View File

@@ -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
@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1
# homeassistant.components.switchbot
PySwitchbot==0.17.3
PySwitchbot==0.18.4
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
@@ -115,7 +115,7 @@ aioasuswrt==1.4.0
aioazuredevops==1.3.5
# homeassistant.components.baf
aiobafi6==0.7.0
aiobafi6==0.7.2
# homeassistant.components.aws
aiobotocore==2.1.0
@@ -152,7 +152,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==1.2.3
aiohomekit==1.2.8
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -320,13 +320,13 @@ base36==0.1.1
beautifulsoup4==4.11.1
# homeassistant.components.zha
bellows==0.31.2
bellows==0.31.3
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.1
# homeassistant.components.bluetooth
bleak==0.15.0
bleak==0.15.1
# homeassistant.components.blebox
blebox_uniapi==2.0.2
@@ -561,10 +561,10 @@ google-nest-sdm==2.0.0
googlemaps==2.5.1
# homeassistant.components.govee_ble
govee-ble==0.12.6
govee-ble==0.14.0
# homeassistant.components.gree
greeclimate==1.2.0
greeclimate==1.3.0
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -655,7 +655,7 @@ influxdb-client==1.24.0
influxdb==5.3.1
# homeassistant.components.inkbird
inkbird-ble==0.5.1
inkbird-ble==0.5.2
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.2.0
@@ -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
@@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2
pydaikin==2.7.0
# homeassistant.components.deconz
pydeconz==100
pydeconz==102
# homeassistant.components.dexcom
pydexcom==0.2.3
@@ -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
@@ -1354,7 +1354,7 @@ pytrafikverket==0.2.0.1
pyudev==0.23.2
# homeassistant.components.unifiprotect
pyunifiprotect==4.0.11
pyunifiprotect==4.0.12
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@@ -1703,13 +1703,13 @@ zigpy-deconz==0.18.0
zigpy-xbee==0.15.0
# homeassistant.components.zha
zigpy-zigate==0.9.0
zigpy-zigate==0.9.1
# homeassistant.components.zha
zigpy-znp==0.8.1
# homeassistant.components.zha
zigpy==0.48.0
zigpy==0.49.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.39.0

View File

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

View File

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

View File

@@ -10,6 +10,8 @@ import pytest
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import (
SCANNER_WATCHDOG_INTERVAL,
SCANNER_WATCHDOG_TIMEOUT,
SOURCE_LOCAL,
UNAVAILABLE_TRACK_SECONDS,
BluetoothChange,
@@ -856,6 +858,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
@@ -1519,3 +1524,57 @@ async def test_invalid_dbus_message(hass, caplog):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert "dbus" in caplog.text
async def test_recovery_from_dbus_restart(
hass, mock_bleak_scanner_start, enable_bluetooth
):
"""Test we can recover when DBus gets restarted out from under us."""
assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}})
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
start_time_monotonic = 1000
scanner = _get_underlying_scanner()
mock_discovered = [MagicMock()]
type(scanner).discovered_devices = mock_discovered
# Ensure we don't restart the scanner if we don't need to
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + 10,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
# Fire a callback to reset the timer
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic,
):
scanner._callback(
BLEDevice("44:44:33:11:23:42", "any_name"),
AdvertisementData(local_name="any_name"),
)
# Ensure we don't restart the scanner if we don't need to
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + 20,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 1
# We hit the timer, so we restart the scanner
with patch(
"homeassistant.components.bluetooth.MONOTONIC_TIME",
return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL)
await hass.async_block_till_done()
assert len(mock_bleak_scanner_start.mock_calls) == 2

View File

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

View File

@@ -28,7 +28,7 @@ class FakeDiscovery:
"""Add an event listener."""
self._listeners.append(listener)
async def scan(self, wait_for: int = 0):
async def scan(self, wait_for: int = 0, bcast_ifaces=None):
"""Search for devices, return mocked data."""
self.scan_count += 1
_LOGGER.info("CALLED SCAN %d TIMES", self.scan_count)

View File

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

View File

@@ -0,0 +1,356 @@
[
{
"aid": 1,
"services": [
{
"iid": 1,
"type": "0000003E-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000020-0000-1000-8000-0026BB765291",
"iid": 4,
"perms": ["pr"],
"format": "string",
"value": "Schlage ",
"description": "Manufacturer",
"maxLen": 64
},
{
"type": "00000021-0000-1000-8000-0026BB765291",
"iid": 5,
"perms": ["pr"],
"format": "string",
"value": "BE479CAM619",
"description": "Model",
"maxLen": 64
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 6,
"perms": ["pr"],
"format": "string",
"value": "SENSE ",
"description": "Name",
"maxLen": 64
},
{
"type": "00000030-0000-1000-8000-0026BB765291",
"iid": 7,
"perms": ["pr"],
"format": "string",
"value": "AAAAAAA000",
"description": "Serial Number",
"maxLen": 64
},
{
"type": "00000014-0000-1000-8000-0026BB765291",
"iid": 3,
"perms": ["pw"],
"format": "bool",
"description": "Identify"
},
{
"type": "00000052-0000-1000-8000-0026BB765291",
"iid": 8,
"perms": ["pr"],
"format": "string",
"value": "004.027.000",
"description": "Firmware Revision",
"maxLen": 64
},
{
"type": "00000053-0000-1000-8000-0026BB765291",
"iid": 51,
"perms": ["pr"],
"format": "string",
"value": "1.3.0",
"description": "Hardware Revision",
"maxLen": 64
},
{
"type": "00000054-0000-1000-8000-0026BB765291",
"iid": 50,
"perms": ["pr"],
"format": "string",
"value": "002.001.000",
"maxLen": 64
}
]
},
{
"iid": 10,
"type": "7F0DEE73-4A3F-4103-98E6-A46CD301BDFB",
"characteristics": [
{
"type": "44FF6853-58DB-4956-B298-5F6650DD61F6",
"iid": 25,
"perms": ["pw"],
"format": "data"
},
{
"type": "CF68C40F-DC6F-4F7E-918C-4C536B643A2B",
"iid": 26,
"perms": ["pr", "pw"],
"format": "uint8",
"value": 0,
"minValue": 0,
"maxValue": 3,
"minStep": 1
},
{
"type": "4058C2B8-4545-4E77-B6B7-157C38F9718B",
"iid": 27,
"perms": ["pr", "pw"],
"format": "uint8",
"value": 0,
"minValue": 1,
"maxValue": 5,
"minStep": 1
},
{
"type": "B498F4B5-6364-4F79-B5CC-1563ADE070DF",
"iid": 28,
"perms": ["pr", "pw"],
"format": "uint8",
"value": 1,
"minValue": 0,
"maxValue": 1
},
{
"type": "AFAE7AD2-8DD3-4B20-BAE0-C0B18B79EDB5",
"iid": 29,
"perms": ["pw"],
"format": "data"
},
{
"type": "87D91EC6-C508-4CAD-89F1-A21B0BF179A0",
"iid": 30,
"perms": ["pr"],
"format": "data",
"value": "000a00000000000000000000"
},
{
"type": "4C3E2641-F57F-11E3-A3AC-0800200C9A66",
"iid": 31,
"perms": ["pr"],
"format": "uint64",
"value": 3468600224
},
{
"type": "EEC26990-F628-11E3-A3AC-0800200C9A66",
"iid": 32,
"perms": ["pr", "pw"],
"format": "uint8",
"value": 4,
"minValue": 4,
"maxValue": 8,
"minStep": 1
},
{
"type": "BCDE3B9E-3963-4123-B24D-42ECCBB3A9C4",
"iid": 33,
"perms": ["pr"],
"format": "data",
"value": "4e6f6e65"
},
{
"type": "A9464D14-6806-4375-BA53-E14F7E0A6BEE",
"iid": 34,
"perms": ["pr", "pw"],
"format": null,
"value": "ff"
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 35,
"perms": ["pr"],
"format": "string",
"value": "Additional Settings",
"description": "Name",
"maxLen": 64
},
{
"type": "63D23C2F-2FBB-45E8-8540-47CC26C517D0",
"iid": 36,
"perms": ["pr"],
"format": "uint8",
"value": 100
}
]
},
{
"iid": 23,
"type": "00000044-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000019-0000-1000-8000-0026BB765291",
"iid": 16,
"perms": ["pw"],
"format": "data",
"description": "Lock Control Point"
},
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 17,
"perms": ["pr"],
"format": "string",
"value": "02.00.00",
"description": "Version",
"maxLen": 64
},
{
"type": "0000001F-0000-1000-8000-0026BB765291",
"iid": 18,
"perms": ["pr"],
"format": "data",
"value": "012431443133423434392d423941312d334135392d463042412d3245393030304233453430450208000000000000000003010404030001ff",
"description": "Logs"
},
{
"type": "00000005-0000-1000-8000-0026BB765291",
"iid": 19,
"perms": ["pr", "pw"],
"format": "bool",
"value": true,
"description": "Audio Feedback"
},
{
"type": "0000001A-0000-1000-8000-0026BB765291",
"iid": 20,
"perms": ["pr", "pw"],
"format": "uint32",
"value": 0,
"description": "Lock Management Auto Security Timeout",
"unit": "seconds"
},
{
"type": "00000001-0000-1000-8000-0026BB765291",
"iid": 21,
"perms": ["pr", "pw"],
"format": "bool",
"value": false,
"description": "Administrator Only Access"
}
]
},
{
"iid": 30,
"type": "00000045-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "0000001D-0000-1000-8000-0026BB765291",
"iid": 11,
"perms": ["pr", "ev"],
"format": "uint8",
"value": 3,
"description": "Lock Current State",
"minValue": 0,
"maxValue": 3,
"minStep": 1
},
{
"type": "0000001E-0000-1000-8000-0026BB765291",
"iid": 12,
"perms": ["pr", "pw", "ev"],
"format": "uint8",
"value": 1,
"description": "Lock Target State",
"minValue": 0,
"maxValue": 1,
"minStep": 1
},
{
"type": "00000023-0000-1000-8000-0026BB765291",
"iid": 13,
"perms": ["pr"],
"format": "string",
"value": "Lock Mechanism",
"description": "Name",
"maxLen": 64
}
]
},
{
"iid": 34,
"type": "1F6B43AA-94DE-4BA9-981C-DA38823117BD",
"characteristics": [
{
"type": "048D8799-695B-4A7F-A7F7-A4A1301587FE",
"iid": 39,
"perms": ["pw"],
"format": "data"
},
{
"type": "66B7C7FD-95A7-4F89-B0AD-38073A67C46C",
"iid": 40,
"perms": ["pw"],
"format": "data"
},
{
"type": "507EFC3F-9231-438C-976A-FA04427F1F8F",
"iid": 41,
"perms": ["pw"],
"format": "data"
},
{
"type": "1DC15719-0882-4BAD-AB0F-9AEAB0600C90",
"iid": 42,
"perms": ["pr"],
"format": "data",
"value": "03"
}
]
},
{
"iid": 39,
"type": "00000055-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "0000004C-0000-1000-8000-0026BB765291",
"iid": 45,
"perms": [],
"format": "data",
"description": "Pair Setup"
},
{
"type": "0000004E-0000-1000-8000-0026BB765291",
"iid": 46,
"perms": [],
"format": "data",
"description": "Pair Verify"
},
{
"type": "0000004F-0000-1000-8000-0026BB765291",
"iid": 47,
"perms": [],
"format": "uint8",
"description": "Pairing Features"
},
{
"type": "00000050-0000-1000-8000-0026BB765291",
"iid": 48,
"perms": ["pr", "pw"],
"format": "data",
"value": null,
"description": "Pairing Pairings"
}
]
},
{
"iid": 44,
"type": "000000A2-0000-1000-8000-0026BB765291",
"characteristics": [
{
"type": "00000037-0000-1000-8000-0026BB765291",
"iid": 62,
"perms": ["pr"],
"format": "string",
"value": "02.00.00",
"description": "Version",
"maxLen": 64
}
]
}
]
}
]

View File

@@ -0,0 +1,40 @@
"""Make sure that Schlage Sense is enumerated properly."""
from tests.components.homekit_controller.common import (
HUB_TEST_ACCESSORY_ID,
DeviceTestInfo,
EntityTestInfo,
assert_devices_and_entities_created,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_schlage_sense_setup(hass):
"""Test that the accessory can be correctly setup in HA."""
accessories = await setup_accessories_from_file(hass, "schlage_sense.json")
await setup_test_accessories(hass, accessories)
await assert_devices_and_entities_created(
hass,
DeviceTestInfo(
unique_id=HUB_TEST_ACCESSORY_ID,
name="SENSE ",
model="BE479CAM619",
manufacturer="Schlage ",
sw_version="004.027.000",
hw_version="1.3.0",
serial_number="AAAAAAA000",
devices=[],
entities=[
EntityTestInfo(
entity_id="lock.sense_lock_mechanism",
friendly_name="SENSE Lock Mechanism",
unique_id="homekit-AAAAAAA000-30",
supported_features=0,
state="unknown",
),
],
),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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