forked from home-assistant/core
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a411cd9c20 | ||
|
|
da81dbe6ac | ||
|
|
f5c30ab10a | ||
|
|
454675d86b | ||
|
|
cce4496ad6 | ||
|
|
ebeebeaec1 | ||
|
|
c8d16175da | ||
|
|
a2aa0e608d | ||
|
|
7eb98ffbd1 | ||
|
|
6e62080cd9 | ||
|
|
39dee6d426 | ||
|
|
3a89a49d4a | ||
|
|
ef66d8e705 | ||
|
|
c1809681b6 | ||
|
|
050c09df62 | ||
|
|
e0b63ac488 | ||
|
|
ed6575fefb | ||
|
|
318ae7750a | ||
|
|
0525a1cd97 | ||
|
|
d31d4e2916 | ||
|
|
40c5689507 | ||
|
|
a4749178f1 | ||
|
|
8229e241f1 | ||
|
|
2b40f3f1e5 | ||
|
|
e839849456 | ||
|
|
e711758cfd | ||
|
|
896955e4df | ||
|
|
7b83807baa | ||
|
|
6a197332c7 | ||
|
|
1955ff9e0d | ||
|
|
29caf06439 | ||
|
|
0b5953038e | ||
|
|
f07e1bc500 | ||
|
|
843d5f101a | ||
|
|
d98ed5c6f6 | ||
|
|
8599472880 | ||
|
|
04d6bb085b | ||
|
|
6f9a311cec | ||
|
|
336179df6d | ||
|
|
9459af30b0 | ||
|
|
ee07ca8caa | ||
|
|
3beed13586 | ||
|
|
f0753f7a97 | ||
|
|
dd007cd765 | ||
|
|
7cdac3ee8c | ||
|
|
cd7f65bb6a | ||
|
|
b21a37cad5 | ||
|
|
bfcb9402ef | ||
|
|
ad396f0538 | ||
|
|
12edfb3929 | ||
|
|
47f6be77cc | ||
|
|
9acf74d783 | ||
|
|
0aa2685e0c | ||
|
|
a90b6d37bf | ||
|
|
d6bf1a8caa | ||
|
|
95a89448e0 | ||
|
|
f6d26476b5 | ||
|
|
9640553b52 | ||
|
|
3129114d07 | ||
|
|
184a1c95f0 | ||
|
|
f18ab504a5 | ||
|
|
2bd71f62ea | ||
|
|
296db8b2af | ||
|
|
a277664187 | ||
|
|
1b7a06912a | ||
|
|
e7986a54a5 | ||
|
|
de8b066a1d | ||
|
|
4d4a87ba05 | ||
|
|
4b79e82e31 | ||
|
|
1e8f461270 | ||
|
|
6e88b8d3d5 | ||
|
|
a626ab4f1a | ||
|
|
c7cb0d1a07 | ||
|
|
183c61b6ca | ||
|
|
95c20df367 | ||
|
|
a969ce273a | ||
|
|
5f90760176 | ||
|
|
795be361b4 | ||
|
|
cdd5c809bb | ||
|
|
c731e2f125 | ||
|
|
1789a8a385 | ||
|
|
57717f13fc | ||
|
|
e4aab6a818 | ||
|
|
258791626e | ||
|
|
78802c8480 | ||
|
|
b24f3725d6 | ||
|
|
06116f76fa | ||
|
|
27c0a37053 | ||
|
|
2b961fd327 | ||
|
|
125afb39f0 | ||
|
|
3ee62d619f | ||
|
|
dc7c860c6a | ||
|
|
f042cc5d7b | ||
|
|
4c0872b4e4 | ||
|
|
21f6b50f7c | ||
|
|
d670df74cb |
19
.github/workflows/ci.yaml
vendored
19
.github/workflows/ci.yaml
vendored
@@ -169,7 +169,6 @@ jobs:
|
||||
uses: actions/setup-python@v4.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache: "pip"
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.0.8
|
||||
@@ -484,7 +483,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore pip wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -492,10 +491,10 @@ jobs:
|
||||
with:
|
||||
path: ${{ env.PIP_CACHE }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
steps.generate-pip-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
|
||||
${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
|
||||
- name: Install additional OS dependencies
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -542,7 +541,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -574,7 +573,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -607,7 +606,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -651,7 +650,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -699,7 +698,7 @@ jobs:
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -752,7 +751,7 @@ jobs:
|
||||
uses: actions/cache@v3.0.8
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -867,8 +867,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse
|
||||
/homeassistant/components/qingping/ @bdraco
|
||||
/tests/components/qingping/ @bdraco
|
||||
/homeassistant/components/qingping/ @bdraco @skgsergio
|
||||
/tests/components/qingping/ @bdraco @skgsergio
|
||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||
/tests/components/qld_bushfire/ @exxamalte
|
||||
/homeassistant/components/qnap_qsw/ @Noltari
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
},
|
||||
"description": "Select the NMI of the site you would like to add"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"no_site": "No site provided",
|
||||
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
},
|
||||
"description": "Go to {api_url} to generate an API key"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_token": "Invalid API key",
|
||||
"no_site": "No site provided",
|
||||
"unknown_error": "Unexpected error"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
@@ -20,6 +21,7 @@ from homeassistant.const import (
|
||||
CONF_EVENT_DATA,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORM,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
@@ -224,6 +226,21 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
return list(automation_entity.referenced_areas)
|
||||
|
||||
|
||||
@callback
|
||||
def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
|
||||
"""Return all automations that reference the blueprint."""
|
||||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
automation_entity.entity_id
|
||||
for automation_entity in component.entities
|
||||
if automation_entity.referenced_blueprint == blueprint_path
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up all automations."""
|
||||
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
||||
@@ -346,7 +363,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
return self.action_script.referenced_areas
|
||||
|
||||
@property
|
||||
def referenced_devices(self):
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
if self._blueprint_inputs is None:
|
||||
return None
|
||||
return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
|
||||
|
||||
@property
|
||||
def referenced_devices(self) -> set[str]:
|
||||
"""Return a set of referenced devices."""
|
||||
if self._referenced_devices is not None:
|
||||
return self._referenced_devices
|
||||
|
||||
@@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER
|
||||
DATA_BLUEPRINTS = "automation_blueprints"
|
||||
|
||||
|
||||
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
||||
"""Return True if any automation references the blueprint."""
|
||||
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
|
||||
|
||||
return len(automations_with_blueprint(hass, blueprint_path)) > 0
|
||||
|
||||
|
||||
@singleton(DATA_BLUEPRINTS)
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
||||
"""Get automation blueprints."""
|
||||
return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER)
|
||||
return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "blink",
|
||||
"name": "Blink",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"requirements": ["blinkpy==0.19.0"],
|
||||
"requirements": ["blinkpy==0.19.2"],
|
||||
"codeowners": ["@fronzbot"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import websocket_api
|
||||
from .const import DOMAIN # noqa: F401
|
||||
from .const import CONF_USE_BLUEPRINT, DOMAIN # noqa: F401
|
||||
from .errors import ( # noqa: F401
|
||||
BlueprintException,
|
||||
BlueprintWithNameException,
|
||||
|
||||
@@ -91,3 +91,11 @@ class FileAlreadyExists(BlueprintWithNameException):
|
||||
def __init__(self, domain: str, blueprint_name: str) -> None:
|
||||
"""Initialize blueprint exception."""
|
||||
super().__init__(domain, blueprint_name, "Blueprint already exists")
|
||||
|
||||
|
||||
class BlueprintInUse(BlueprintWithNameException):
|
||||
"""Error when a blueprint is in use."""
|
||||
|
||||
def __init__(self, domain: str, blueprint_name: str) -> None:
|
||||
"""Initialize blueprint exception."""
|
||||
super().__init__(domain, blueprint_name, "Blueprint in use")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
import pathlib
|
||||
import shutil
|
||||
@@ -35,6 +36,7 @@ from .const import (
|
||||
)
|
||||
from .errors import (
|
||||
BlueprintException,
|
||||
BlueprintInUse,
|
||||
FailedToLoad,
|
||||
FileAlreadyExists,
|
||||
InvalidBlueprint,
|
||||
@@ -183,11 +185,13 @@ class DomainBlueprints:
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
logger: logging.Logger,
|
||||
blueprint_in_use: Callable[[HomeAssistant, str], bool],
|
||||
) -> None:
|
||||
"""Initialize a domain blueprints instance."""
|
||||
self.hass = hass
|
||||
self.domain = domain
|
||||
self.logger = logger
|
||||
self._blueprint_in_use = blueprint_in_use
|
||||
self._blueprints: dict[str, Blueprint | None] = {}
|
||||
self._load_lock = asyncio.Lock()
|
||||
|
||||
@@ -302,6 +306,8 @@ class DomainBlueprints:
|
||||
|
||||
async def async_remove_blueprint(self, blueprint_path: str) -> None:
|
||||
"""Remove a blueprint file."""
|
||||
if self._blueprint_in_use(self.hass, blueprint_path):
|
||||
raise BlueprintInUse(self.domain, blueprint_path)
|
||||
path = self.blueprint_folder / blueprint_path
|
||||
await self.hass.async_add_executor_job(path.unlink)
|
||||
self._blueprints[blueprint_path] = None
|
||||
|
||||
@@ -384,11 +384,11 @@ class BluetoothManager:
|
||||
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
|
||||
|
||||
connectable = callback_matcher[CONNECTABLE]
|
||||
self._callback_index.add_with_address(callback_matcher)
|
||||
self._callback_index.add_callback_matcher(callback_matcher)
|
||||
|
||||
@hass_callback
|
||||
def _async_remove_callback() -> None:
|
||||
self._callback_index.remove_with_address(callback_matcher)
|
||||
self._callback_index.remove_callback_matcher(callback_matcher)
|
||||
|
||||
# If we have history for the subscriber, we can trigger the callback
|
||||
# immediately with the last packet so the subscriber can see the
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
"dependencies": ["usb"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.16.0",
|
||||
"bluetooth-adapters==0.3.5",
|
||||
"bluetooth-auto-recovery==0.3.2"
|
||||
"bleak==0.17.0",
|
||||
"bleak-retry-connector==1.17.1",
|
||||
"bluetooth-adapters==0.4.1",
|
||||
"bluetooth-auto-recovery==0.3.3",
|
||||
"dbus-fast==1.4.0"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -173,7 +173,7 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
||||
self.service_data_uuid_set: set[str] = set()
|
||||
self.manufacturer_id_set: set[int] = set()
|
||||
|
||||
def add(self, matcher: _T) -> None:
|
||||
def add(self, matcher: _T) -> bool:
|
||||
"""Add a matcher to the index.
|
||||
|
||||
Matchers must end up only in one bucket.
|
||||
@@ -185,26 +185,28 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
||||
self.local_name.setdefault(
|
||||
_local_name_to_index_key(matcher[LOCAL_NAME]), []
|
||||
).append(matcher)
|
||||
return
|
||||
return True
|
||||
|
||||
# Manufacturer data is 2nd cheapest since its all ints
|
||||
if MANUFACTURER_ID in matcher:
|
||||
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
|
||||
matcher
|
||||
)
|
||||
return
|
||||
return True
|
||||
|
||||
if SERVICE_UUID in matcher:
|
||||
self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher)
|
||||
return
|
||||
return True
|
||||
|
||||
if SERVICE_DATA_UUID in matcher:
|
||||
self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append(
|
||||
matcher
|
||||
)
|
||||
return
|
||||
return True
|
||||
|
||||
def remove(self, matcher: _T) -> None:
|
||||
return False
|
||||
|
||||
def remove(self, matcher: _T) -> bool:
|
||||
"""Remove a matcher from the index.
|
||||
|
||||
Matchers only end up in one bucket, so once we have
|
||||
@@ -214,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
||||
self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove(
|
||||
matcher
|
||||
)
|
||||
return
|
||||
return True
|
||||
|
||||
if MANUFACTURER_ID in matcher:
|
||||
self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher)
|
||||
return
|
||||
return True
|
||||
|
||||
if SERVICE_UUID in matcher:
|
||||
self.service_uuid[matcher[SERVICE_UUID]].remove(matcher)
|
||||
return
|
||||
return True
|
||||
|
||||
if SERVICE_DATA_UUID in matcher:
|
||||
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher)
|
||||
return
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def build(self) -> None:
|
||||
"""Rebuild the index sets."""
|
||||
@@ -284,8 +288,11 @@ class BluetoothCallbackMatcherIndex(
|
||||
"""Initialize the matcher index."""
|
||||
super().__init__()
|
||||
self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {}
|
||||
self.connectable: list[BluetoothCallbackMatcherWithCallback] = []
|
||||
|
||||
def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None:
|
||||
def add_callback_matcher(
|
||||
self, matcher: BluetoothCallbackMatcherWithCallback
|
||||
) -> None:
|
||||
"""Add a matcher to the index.
|
||||
|
||||
Matchers must end up only in one bucket.
|
||||
@@ -296,10 +303,15 @@ class BluetoothCallbackMatcherIndex(
|
||||
self.address.setdefault(matcher[ADDRESS], []).append(matcher)
|
||||
return
|
||||
|
||||
super().add(matcher)
|
||||
self.build()
|
||||
if super().add(matcher):
|
||||
self.build()
|
||||
return
|
||||
|
||||
def remove_with_address(
|
||||
if CONNECTABLE in matcher:
|
||||
self.connectable.append(matcher)
|
||||
return
|
||||
|
||||
def remove_callback_matcher(
|
||||
self, matcher: BluetoothCallbackMatcherWithCallback
|
||||
) -> None:
|
||||
"""Remove a matcher from the index.
|
||||
@@ -311,8 +323,13 @@ class BluetoothCallbackMatcherIndex(
|
||||
self.address[matcher[ADDRESS]].remove(matcher)
|
||||
return
|
||||
|
||||
super().remove(matcher)
|
||||
self.build()
|
||||
if super().remove(matcher):
|
||||
self.build()
|
||||
return
|
||||
|
||||
if CONNECTABLE in matcher:
|
||||
self.connectable.remove(matcher)
|
||||
return
|
||||
|
||||
def match_callbacks(
|
||||
self, service_info: BluetoothServiceInfoBleak
|
||||
@@ -322,6 +339,9 @@ class BluetoothCallbackMatcherIndex(
|
||||
for matcher in self.address.get(service_info.address, []):
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
for matcher in self.connectable:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
return matches
|
||||
|
||||
|
||||
@@ -355,7 +375,6 @@ def ble_device_matches(
|
||||
# Don't check address here since all callers already
|
||||
# check the address and we don't want to double check
|
||||
# since it would result in an unreachable reject case.
|
||||
|
||||
if matcher.get(CONNECTABLE, True) and not service_info.connectable:
|
||||
return False
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
||||
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from dbus_next import InvalidMessageError
|
||||
from dbus_fast import InvalidMessageError
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import (
|
||||
|
||||
@@ -47,7 +47,7 @@ SERVICE_CONFIGURE = "configure"
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int,
|
||||
vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)),
|
||||
@@ -57,16 +57,6 @@ CREATE_FIELDS = {
|
||||
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
|
||||
}
|
||||
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MAXIMUM): vol.Any(None, vol.Coerce(int)),
|
||||
vol.Optional(CONF_MINIMUM): vol.Any(None, vol.Coerce(int)),
|
||||
vol.Optional(CONF_RESTORE): cv.boolean,
|
||||
vol.Optional(CONF_STEP): cv.positive_int,
|
||||
}
|
||||
|
||||
|
||||
def _none_to_empty_dict(value):
|
||||
if value is None:
|
||||
@@ -128,7 +118,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment")
|
||||
@@ -152,12 +142,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class CounterStorageCollection(collection.StorageCollection):
|
||||
"""Input storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.CREATE_UPDATE_SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
@@ -166,8 +155,8 @@ class CounterStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return {**data, **update_data}
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
class Counter(RestoreEntity):
|
||||
|
||||
@@ -43,6 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
except AuthenticationRequired as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
async_setup_services(hass)
|
||||
|
||||
gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway(
|
||||
hass, config_entry, api
|
||||
)
|
||||
@@ -53,9 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
await async_setup_events(gateway)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
if len(hass.data[DOMAIN]) == 1:
|
||||
async_setup_services(hass)
|
||||
|
||||
api.start()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "dhcp",
|
||||
"name": "DHCP Discovery",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
||||
"requirements": ["scapy==2.4.5", "aiodiscover==1.4.11"],
|
||||
"requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -44,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
||||
@@ -68,4 +68,4 @@ class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.ecowitt.value > 0
|
||||
return bool(self.ecowitt.value)
|
||||
|
||||
@@ -25,13 +25,13 @@ async def async_get_device_diagnostics(
|
||||
"device": {
|
||||
"name": station.station,
|
||||
"model": station.model,
|
||||
"frequency": station.frequency,
|
||||
"frequency": station.frequence,
|
||||
"version": station.version,
|
||||
},
|
||||
"raw": ecowitt.last_values[station_id],
|
||||
"sensors": {
|
||||
sensor.key: sensor.value
|
||||
for sensor in station.sensors
|
||||
for sensor in ecowitt.sensors.values()
|
||||
if sensor.station.key == station_id
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"name": "Ecowitt",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"requirements": ["aioecowitt==2022.08.3"],
|
||||
"dependencies": ["webhook"],
|
||||
"requirements": ["aioecowitt==2022.09.1"],
|
||||
"codeowners": ["@pvizeli"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""Support for Ecowitt Weather Stations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from datetime import datetime
|
||||
from typing import Final
|
||||
|
||||
from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
|
||||
@@ -242,6 +245,6 @@ class EcowittSensorEntity(EcowittEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
return self.ecowitt.value
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
ENERGY_MEGA_WATT_HOUR,
|
||||
ENERGY_WATT_HOUR,
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -44,7 +45,7 @@ SUPPORTED_STATE_CLASSES = [
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
]
|
||||
VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR]
|
||||
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
|
||||
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220907.0"],
|
||||
"requirements": ["home-assistant-frontend==20220907.2"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 57391,
|
||||
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 18994,
|
||||
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb",
|
||||
@@ -53,7 +58,7 @@
|
||||
"connectable": false
|
||||
}
|
||||
],
|
||||
"requirements": ["govee-ble==0.17.2"],
|
||||
"requirements": ["govee-ble==0.17.3"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -423,7 +423,7 @@ class HKDevice:
|
||||
if self._polling_interval_remover:
|
||||
self._polling_interval_remover()
|
||||
|
||||
await self.pairing.close()
|
||||
await self.pairing.shutdown()
|
||||
|
||||
await self.hass.config_entries.async_unload_platforms(
|
||||
self.config_entry, self.platforms
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==1.5.2"],
|
||||
"requirements": ["aiohomekit==1.5.9"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -37,20 +37,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INITIAL = "initial"
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional(CONF_INITIAL): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))},
|
||||
{
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
vol.Any(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
},
|
||||
None,
|
||||
)
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@@ -62,12 +67,11 @@ STORAGE_VERSION = 1
|
||||
class InputBooleanStorageCollection(collection.StorageCollection):
|
||||
"""Input boolean collection stored in storage."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.CREATE_UPDATE_SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
@@ -76,8 +80,8 @@ class InputBooleanStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return {**data, **update_data}
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -118,7 +122,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
|
||||
@@ -30,18 +30,23 @@ DOMAIN = "input_button"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))},
|
||||
{
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
vol.Any(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
},
|
||||
None,
|
||||
)
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@@ -53,12 +58,11 @@ STORAGE_VERSION = 1
|
||||
class InputButtonStorageCollection(collection.StorageCollection):
|
||||
"""Input button collection stored in storage."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
|
||||
|
||||
async def _process_create_data(self, data: dict) -> vol.Schema:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.CREATE_UPDATE_SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
@@ -67,8 +71,8 @@ class InputButtonStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return {**data, **update_data}
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -103,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
|
||||
@@ -61,20 +61,13 @@ def validate_set_datetime_attrs(config):
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional(CONF_HAS_DATE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_HAS_TIME, default=False): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
}
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HAS_DATE): cv.boolean,
|
||||
vol.Optional(CONF_HAS_TIME): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
}
|
||||
|
||||
|
||||
def has_date_or_time(conf):
|
||||
@@ -167,7 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
@@ -213,12 +206,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class DateTimeStorageCollection(collection.StorageCollection):
|
||||
"""Input storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time))
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, has_date_or_time))
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.CREATE_UPDATE_SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
@@ -227,8 +219,8 @@ class DateTimeStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return has_date_or_time({**data, **update_data})
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
class InputDatetime(RestoreEntity):
|
||||
|
||||
@@ -65,7 +65,7 @@ def _cv_input_number(cfg):
|
||||
return cfg
|
||||
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Required(CONF_MIN): vol.Coerce(float),
|
||||
vol.Required(CONF_MAX): vol.Coerce(float),
|
||||
@@ -76,17 +76,6 @@ CREATE_FIELDS = {
|
||||
vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]),
|
||||
}
|
||||
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MIN): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX): vol.Coerce(float),
|
||||
vol.Optional(CONF_INITIAL): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-9)),
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
@@ -148,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
@@ -184,22 +173,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class NumberStorageCollection(collection.StorageCollection):
|
||||
"""Input storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number))
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_number))
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
"""Suggest an ID based on the config."""
|
||||
return info[CONF_NAME]
|
||||
|
||||
async def _async_load_data(self) -> dict | None:
|
||||
"""Load the data.
|
||||
|
||||
A past bug caused frontend to add initial value to all input numbers.
|
||||
This drops that.
|
||||
"""
|
||||
data = await super()._async_load_data()
|
||||
|
||||
if data is None:
|
||||
return data
|
||||
|
||||
for number in data["items"]:
|
||||
number.pop(CONF_INITIAL, None)
|
||||
|
||||
return data
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return _cv_input_number({**data, **update_data})
|
||||
update_data = self.SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
class InputNumber(RestoreEntity):
|
||||
|
||||
@@ -56,7 +56,7 @@ def _unique(options: Any) -> Any:
|
||||
raise HomeAssistantError("Duplicate options are not allowed") from exc
|
||||
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
|
||||
@@ -64,14 +64,6 @@ CREATE_FIELDS = {
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIONS): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
|
||||
),
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}
|
||||
|
||||
|
||||
def _remove_duplicates(options: list[str], name: str | None) -> list[str]:
|
||||
@@ -172,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
@@ -238,12 +230,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class InputSelectStorageCollection(collection.StorageCollection):
|
||||
"""Input storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select))
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_select))
|
||||
|
||||
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the config is valid."""
|
||||
return cast(dict[str, Any], self.CREATE_SCHEMA(data))
|
||||
return cast(dict[str, Any], self.CREATE_UPDATE_SCHEMA(data))
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict[str, Any]) -> str:
|
||||
@@ -254,8 +245,8 @@ class InputSelectStorageCollection(collection.StorageCollection):
|
||||
self, data: dict[str, Any], update_data: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return _cv_input_select({**data, **update_data})
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
class InputSelect(SelectEntity, RestoreEntity):
|
||||
|
||||
@@ -51,7 +51,7 @@ SERVICE_SET_VALUE = "set_value"
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
|
||||
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
|
||||
@@ -61,16 +61,6 @@ CREATE_FIELDS = {
|
||||
vol.Optional(CONF_PATTERN): cv.string,
|
||||
vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]),
|
||||
}
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MIN): vol.Coerce(int),
|
||||
vol.Optional(CONF_MAX): vol.Coerce(int),
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_PATTERN): cv.string,
|
||||
vol.Optional(CONF_MODE): vol.In([MODE_TEXT, MODE_PASSWORD]),
|
||||
}
|
||||
|
||||
|
||||
def _cv_input_text(cfg):
|
||||
@@ -147,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
@@ -177,12 +167,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class InputTextStorageCollection(collection.StorageCollection):
|
||||
"""Input storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text))
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text))
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
return self.CREATE_SCHEMA(data)
|
||||
return self.CREATE_UPDATE_SCHEMA(data)
|
||||
|
||||
@callback
|
||||
def _get_suggested_id(self, info: dict) -> str:
|
||||
@@ -191,8 +180,8 @@ class InputTextStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
update_data = self.UPDATE_SCHEMA(update_data)
|
||||
return _cv_input_text({**data, **update_data})
|
||||
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
return {CONF_ID: data[CONF_ID]} | update_data
|
||||
|
||||
|
||||
class InputText(RestoreEntity):
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
"""Component for the Portuguese weather service - IPMA."""
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from pyipma import IPMAException
|
||||
from pyipma.api import IPMA_API
|
||||
from pyipma.location import Location
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .config_flow import IpmaFlowHandler # noqa: F401
|
||||
from .const import DOMAIN # noqa: F401
|
||||
from .const import DATA_API, DATA_LOCATION, DOMAIN
|
||||
|
||||
DEFAULT_NAME = "ipma"
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_get_api(hass):
|
||||
"""Get the pyipma api object."""
|
||||
websession = async_get_clientsession(hass)
|
||||
return IPMA_API(websession)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up IPMA station as config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
|
||||
api = await async_get_api(hass)
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
location = await Location.get(api, float(latitude), float(longitude))
|
||||
|
||||
_LOGGER.debug(
|
||||
"Initializing for coordinates %s, %s -> station %s (%d, %d)",
|
||||
latitude,
|
||||
longitude,
|
||||
location.station,
|
||||
location.id_station,
|
||||
location.global_id_local,
|
||||
)
|
||||
except IPMAException as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not get location for ({latitude},{longitude})"
|
||||
) from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -6,3 +6,6 @@ DOMAIN = "ipma"
|
||||
HOME_LOCATION_NAME = "Home"
|
||||
|
||||
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}"
|
||||
|
||||
DATA_LOCATION = "location"
|
||||
DATA_API = "api"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ipma",
|
||||
"requirements": ["pyipma==3.0.2"],
|
||||
"requirements": ["pyipma==3.0.4"],
|
||||
"codeowners": ["@dgomes", "@abmantis"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["geopy", "pyipma"]
|
||||
|
||||
@@ -48,11 +48,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.sun import is_up
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DATA_API, DATA_LOCATION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
|
||||
@@ -95,13 +96,10 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a weather entity from a config_entry."""
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
api = hass.data[DOMAIN][config_entry.entry_id][DATA_API]
|
||||
location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION]
|
||||
mode = config_entry.data[CONF_MODE]
|
||||
|
||||
api = await async_get_api(hass)
|
||||
location = await async_get_location(hass, api, latitude, longitude)
|
||||
|
||||
# Migrate old unique_id
|
||||
@callback
|
||||
def _async_migrator(entity_entry: entity_registry.RegistryEntry):
|
||||
@@ -127,29 +125,6 @@ async def async_setup_entry(
|
||||
async_add_entities([IPMAWeather(location, api, config_entry.data)], True)
|
||||
|
||||
|
||||
async def async_get_api(hass):
|
||||
"""Get the pyipma api object."""
|
||||
websession = async_get_clientsession(hass)
|
||||
return IPMA_API(websession)
|
||||
|
||||
|
||||
async def async_get_location(hass, api, latitude, longitude):
|
||||
"""Retrieve pyipma location, location name to be used as the entity name."""
|
||||
async with async_timeout.timeout(30):
|
||||
location = await Location.get(api, float(latitude), float(longitude))
|
||||
|
||||
_LOGGER.debug(
|
||||
"Initializing for coordinates %s, %s -> station %s (%d, %d)",
|
||||
latitude,
|
||||
longitude,
|
||||
location.station,
|
||||
location.id_station,
|
||||
location.global_id_local,
|
||||
)
|
||||
|
||||
return location
|
||||
|
||||
|
||||
class IPMAWeather(WeatherEntity):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "lametric",
|
||||
"name": "LaMetric",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lametric",
|
||||
"requirements": ["demetriek==0.2.2"],
|
||||
"requirements": ["demetriek==0.2.4"],
|
||||
"codeowners": ["@robbiet480", "@frenck"],
|
||||
"iot_class": "local_polling",
|
||||
"dependencies": ["application_credentials"],
|
||||
|
||||
@@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.info("Polling on %s", entry.data[CONF_DEVICE])
|
||||
return await hass.async_add_executor_job(api.read)
|
||||
|
||||
# No automatic polling and no initial refresh of data is being done at this point,
|
||||
# to prevent battery drain. The user will have to do it manually.
|
||||
|
||||
# Polling is only daily to prevent battery drain.
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, ULTRAHEAT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,6 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
dev_path = await self.hass.async_add_executor_job(
|
||||
get_serial_by_id, user_input[CONF_DEVICE]
|
||||
)
|
||||
_LOGGER.debug("Using this path : %s", dev_path)
|
||||
|
||||
try:
|
||||
return await self.validate_and_create_entry(dev_path)
|
||||
@@ -76,6 +77,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Try to connect to the device path and return an entry."""
|
||||
model, device_number = await self.validate_ultraheat(dev_path)
|
||||
|
||||
_LOGGER.debug("Got model %s and device_number %s", model, device_number)
|
||||
await self.async_set_unique_id(device_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
data = {
|
||||
@@ -94,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
reader = UltraheatReader(port)
|
||||
heat_meter = HeatMeterService(reader)
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
async with async_timeout.timeout(ULTRAHEAT_TIMEOUT):
|
||||
# validate and retrieve the model and device number for a unique id
|
||||
data = await self.hass.async_add_executor_job(heat_meter.read)
|
||||
_LOGGER.debug("Got data from Ultraheat API: %s", data)
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.helpers.entity import EntityCategory
|
||||
DOMAIN = "landisgyr_heat_meter"
|
||||
|
||||
GJ_TO_MWH = 0.277778 # conversion factor
|
||||
ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time
|
||||
|
||||
HEAT_METER_SENSOR_TYPES = (
|
||||
SensorEntityDescription(
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from led_ble import BLEAK_EXCEPTIONS, LEDBLE
|
||||
from led_ble import BLEAK_EXCEPTIONS, LEDBLE, get_device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
|
||||
@@ -27,7 +27,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up LED BLE from a config entry."""
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, address.upper(), True
|
||||
) or await get_device(address)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find LED BLE device with address {address}"
|
||||
|
||||
@@ -48,12 +48,12 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity):
|
||||
"""Initialize an ledble light."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_unique_id = device._address
|
||||
self._attr_unique_id = device.address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=name,
|
||||
model=hex(device.model_num),
|
||||
sw_version=hex(device.version_num),
|
||||
connections={(dr.CONNECTION_BLUETOOTH, device._address)},
|
||||
connections={(dr.CONNECTION_BLUETOOTH, device.address)},
|
||||
)
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LED BLE",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ble_ble",
|
||||
"requirements": ["led-ble==0.7.1"],
|
||||
"requirements": ["led-ble==0.10.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
||||
@@ -219,15 +219,10 @@ class LIFXLight(LIFXEntity, LightEntity):
|
||||
elif power_on:
|
||||
await self.set_power(True, duration=fade)
|
||||
else:
|
||||
if power_on:
|
||||
await self.set_power(True)
|
||||
if hsbk:
|
||||
await self.set_color(hsbk, kwargs, duration=fade)
|
||||
# The response from set_color will tell us if the
|
||||
# bulb is actually on or not, so we don't need to
|
||||
# call power_on if its already on
|
||||
if power_on and self.bulb.power_level == 0:
|
||||
await self.set_power(True)
|
||||
elif power_on:
|
||||
await self.set_power(True)
|
||||
if power_off:
|
||||
await self.set_power(False, duration=fade)
|
||||
|
||||
|
||||
@@ -20,7 +20,13 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@@ -68,9 +74,11 @@ from .const import ( # noqa: F401
|
||||
CONFIG_ENTRY_IS_SETUP,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_RELOAD_ENTRY,
|
||||
DATA_MQTT_RELOAD_NEEDED,
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_QOS,
|
||||
@@ -314,7 +322,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Bail out
|
||||
return False
|
||||
|
||||
hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {}
|
||||
hass.data[DATA_MQTT] = MQTT(hass, entry, conf)
|
||||
# Restore saved subscriptions
|
||||
if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data:
|
||||
hass.data[DATA_MQTT].subscriptions = hass.data.pop(
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE
|
||||
)
|
||||
entry.add_update_listener(_async_config_entry_updated)
|
||||
|
||||
await hass.data[DATA_MQTT].async_connect()
|
||||
@@ -438,6 +452,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_forward_entry_setup_and_setup_discovery(config_entry):
|
||||
"""Forward the config entry setup to the platforms and set up discovery."""
|
||||
reload_manual_setup: bool = False
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation, tag
|
||||
@@ -460,8 +475,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await _async_setup_discovery(hass, conf, entry)
|
||||
# Setup reload service after all platforms have loaded
|
||||
await async_setup_reload_service()
|
||||
# When the entry is reloaded, also reload manual set up items to enable MQTT
|
||||
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||
hass.data.pop(DATA_MQTT_RELOAD_ENTRY)
|
||||
reload_manual_setup = True
|
||||
|
||||
# When the entry was disabled before, reload manual set up items to enable MQTT again
|
||||
if DATA_MQTT_RELOAD_NEEDED in hass.data:
|
||||
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
|
||||
reload_manual_setup = True
|
||||
|
||||
if reload_manual_setup:
|
||||
await async_reload_manual_mqtt_items(hass)
|
||||
|
||||
await async_forward_entry_setup_and_setup_discovery(entry)
|
||||
@@ -613,8 +637,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
mqtt_client.cleanup()
|
||||
|
||||
# Trigger reload manual MQTT items at entry setup
|
||||
# Reload the legacy yaml platform
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False:
|
||||
# The entry is disabled reload legacy manual items when the entry is enabled again
|
||||
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
|
||||
@@ -622,7 +644,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# The entry is reloaded:
|
||||
# Trigger re-fetching the yaml config at entry setup
|
||||
hass.data[DATA_MQTT_RELOAD_ENTRY] = True
|
||||
# Stop the loop
|
||||
# Reload the legacy yaml platform to make entities unavailable
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
# Cleanup entity registry hooks
|
||||
registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS
|
||||
]
|
||||
while registry_hooks:
|
||||
registry_hooks.popitem()[1]()
|
||||
# Wait for all ACKs and stop the loop
|
||||
await mqtt_client.async_disconnect()
|
||||
# Store remaining subscriptions to be able to restore or reload them
|
||||
# when the entry is set up again
|
||||
if mqtt_client.subscriptions:
|
||||
hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions
|
||||
|
||||
return True
|
||||
|
||||
@@ -309,7 +309,7 @@ class MQTT:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
hass,
|
||||
config_entry,
|
||||
conf,
|
||||
) -> None:
|
||||
@@ -435,12 +435,13 @@ class MQTT:
|
||||
"""Return False if there are unprocessed ACKs."""
|
||||
return not bool(self._pending_operations)
|
||||
|
||||
# wait for ACK-s to be processesed (unsubscribe only)
|
||||
# wait for ACKs to be processed
|
||||
async with self._pending_operations_condition:
|
||||
await self._pending_operations_condition.wait_for(no_more_acks)
|
||||
|
||||
# stop the MQTT loop
|
||||
await self.hass.async_add_executor_job(stop)
|
||||
async with self._paho_lock:
|
||||
await self.hass.async_add_executor_job(stop)
|
||||
|
||||
async def async_subscribe(
|
||||
self,
|
||||
@@ -501,7 +502,8 @@ class MQTT:
|
||||
async with self._paho_lock:
|
||||
mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic)
|
||||
await self._register_mid(mid)
|
||||
self.hass.async_create_task(self._wait_for_mid(mid))
|
||||
|
||||
self.hass.async_create_task(self._wait_for_mid(mid))
|
||||
|
||||
async def _async_perform_subscriptions(
|
||||
self, subscriptions: Iterable[tuple[str, int]]
|
||||
|
||||
@@ -32,6 +32,8 @@ CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
||||
DATA_MQTT = "mqtt"
|
||||
DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions"
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks"
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy"
|
||||
DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers"
|
||||
|
||||
@@ -249,7 +249,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid RGB color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid RGB color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -259,7 +261,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid XY color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid XY color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -269,12 +273,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid HS color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid HS color value received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
else:
|
||||
color_mode = values["color_mode"]
|
||||
if not self._supports_color_mode(color_mode):
|
||||
_LOGGER.warning("Invalid color mode received")
|
||||
_LOGGER.warning(
|
||||
"Invalid color mode received for entity %s", self.entity_id
|
||||
)
|
||||
return
|
||||
try:
|
||||
if color_mode == ColorMode.COLOR_TEMP:
|
||||
@@ -314,7 +322,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._color_mode = ColorMode.XY
|
||||
self._xy = (x, y)
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.warning("Invalid or incomplete color value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid or incomplete color value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
def _prepare_subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
@@ -351,7 +362,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.warning("Invalid brightness value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid brightness value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
if (
|
||||
self._supported_features
|
||||
@@ -366,7 +380,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color temp value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid color temp value received for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
|
||||
if self._supported_features and LightEntityFeature.EFFECT:
|
||||
with suppress(KeyError):
|
||||
|
||||
@@ -28,7 +28,13 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, async_get_hass, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
HomeAssistant,
|
||||
async_get_hass,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@@ -48,6 +54,7 @@ from homeassistant.helpers.entity import (
|
||||
async_generate_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.json import json_loads
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -64,7 +71,9 @@ from .const import (
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_RELOAD_ENTRY,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_PAYLOAD_AVAILABLE,
|
||||
@@ -363,6 +372,12 @@ async def async_setup_platform_helper(
|
||||
async_setup_entities: SetupEntity,
|
||||
) -> None:
|
||||
"""Help to set up the platform for manual configured MQTT entities."""
|
||||
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||
_LOGGER.debug(
|
||||
"MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry",
|
||||
platform_domain,
|
||||
)
|
||||
return
|
||||
if not (entry_status := mqtt_config_entry_enabled(hass)):
|
||||
_LOGGER.warning(
|
||||
"MQTT integration is %s, skipping setup of manually configured MQTT %s",
|
||||
@@ -647,6 +662,17 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di
|
||||
await async_publish(hass, discovery_topic, "", retain=True)
|
||||
|
||||
|
||||
async def async_clear_discovery_topic_if_entity_removed(
|
||||
hass: HomeAssistant,
|
||||
discovery_data: dict[str, Any],
|
||||
event: Event,
|
||||
) -> None:
|
||||
"""Clear the discovery topic if the entity is removed."""
|
||||
if event.data["action"] == "remove":
|
||||
# publish empty payload to config topic to avoid re-adding
|
||||
await async_remove_discovery_payload(hass, discovery_data)
|
||||
|
||||
|
||||
class MqttDiscoveryDeviceUpdate:
|
||||
"""Add support for auto discovery for platforms without an entity."""
|
||||
|
||||
@@ -780,7 +806,8 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
discovery_data: dict,
|
||||
hass: HomeAssistant,
|
||||
discovery_data: dict | None,
|
||||
discovery_update: Callable | None = None,
|
||||
) -> None:
|
||||
"""Initialize the discovery update mixin."""
|
||||
@@ -788,6 +815,14 @@ class MqttDiscoveryUpdate(Entity):
|
||||
self._discovery_update = discovery_update
|
||||
self._remove_discovery_updated: Callable | None = None
|
||||
self._removed_from_hass = False
|
||||
if discovery_data is None:
|
||||
return
|
||||
self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[
|
||||
DATA_MQTT_DISCOVERY_REGISTRY_HOOKS
|
||||
]
|
||||
discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
|
||||
if discovery_hash in self._registry_hooks:
|
||||
self._registry_hooks.pop(discovery_hash)()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to discovery updates."""
|
||||
@@ -850,7 +885,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
async def async_removed_from_registry(self) -> None:
|
||||
"""Clear retained discovery topic in broker."""
|
||||
if not self._removed_from_hass:
|
||||
if not self._removed_from_hass and self._discovery_data is not None:
|
||||
# Stop subscribing to discovery updates to not trigger when we clear the
|
||||
# discovery topic
|
||||
self._cleanup_discovery_on_remove()
|
||||
@@ -861,7 +896,20 @@ class MqttDiscoveryUpdate(Entity):
|
||||
@callback
|
||||
def add_to_platform_abort(self) -> None:
|
||||
"""Abort adding an entity to a platform."""
|
||||
if self._discovery_data:
|
||||
if self._discovery_data is not None:
|
||||
discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH]
|
||||
if self.registry_entry is not None:
|
||||
self._registry_hooks[
|
||||
discovery_hash
|
||||
] = async_track_entity_registry_updated_event(
|
||||
self.hass,
|
||||
self.entity_id,
|
||||
partial(
|
||||
async_clear_discovery_topic_if_entity_removed,
|
||||
self.hass,
|
||||
self._discovery_data,
|
||||
),
|
||||
)
|
||||
stop_discovery_updates(self.hass, self._discovery_data)
|
||||
send_discovery_done(self.hass, self._discovery_data)
|
||||
super().add_to_platform_abort()
|
||||
@@ -969,7 +1017,7 @@ class MqttEntity(
|
||||
# Initialize mixin classes
|
||||
MqttAttributes.__init__(self, config)
|
||||
MqttAvailability.__init__(self, config)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
|
||||
MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update)
|
||||
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
|
||||
|
||||
def _init_entity_id(self):
|
||||
|
||||
@@ -91,7 +91,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.discovery_info = discovery_info
|
||||
_properties = discovery_info.properties
|
||||
|
||||
unique_id = discovery_info.hostname.split(".")[0]
|
||||
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
|
||||
if config_entry := await self.async_set_unique_id(unique_id):
|
||||
try:
|
||||
await validate_gw_input(
|
||||
|
||||
@@ -128,7 +128,7 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
self.pushover.send_message(
|
||||
self._user_key,
|
||||
message,
|
||||
kwargs.get(ATTR_TARGET),
|
||||
",".join(kwargs.get(ATTR_TARGET, [])),
|
||||
title,
|
||||
url,
|
||||
url_title,
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
"connectable": false
|
||||
}
|
||||
],
|
||||
"requirements": ["qingping-ble==0.6.0"],
|
||||
"requirements": ["qingping-ble==0.7.0"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"codeowners": ["@bdraco", "@skgsergio"],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==2022.09.0"],
|
||||
"requirements": ["regenmaschine==2022.09.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "local_polling",
|
||||
"homekit": {
|
||||
|
||||
@@ -99,4 +99,11 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity):
|
||||
UpdateStates.UPGRADING,
|
||||
UpdateStates.REBOOT,
|
||||
)
|
||||
self._attr_latest_version = data["packageDetails"]["newVersion"]
|
||||
|
||||
# The RainMachine API docs say that multiple "packages" can be updated, but
|
||||
# don't give details on what types exist (which makes it impossible to have
|
||||
# update entities per update type); so, we use the first one (with the idea that
|
||||
# after it succeeds, the entity will show the next update):
|
||||
package_details = data["packageDetails"][0]
|
||||
self._attr_latest_version = package_details["newVersion"]
|
||||
self._attr_title = package_details["packageName"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Risco",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"requirements": ["pyrisco==0.5.4"],
|
||||
"requirements": ["pyrisco==0.5.5"],
|
||||
"codeowners": ["@OnFreund"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any, cast
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components.blueprint import BlueprintInputs
|
||||
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
CONF_ICON,
|
||||
CONF_MODE,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_SEQUENCE,
|
||||
CONF_VARIABLES,
|
||||
SERVICE_RELOAD,
|
||||
@@ -165,6 +166,21 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
return list(script_entity.script.referenced_areas)
|
||||
|
||||
|
||||
@callback
|
||||
def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
|
||||
"""Return all scripts that reference the blueprint."""
|
||||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
script_entity.entity_id
|
||||
for script_entity in component.entities
|
||||
if script_entity.referenced_blueprint == blueprint_path
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Load the scripts from the configuration."""
|
||||
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
||||
@@ -372,6 +388,13 @@ class ScriptEntity(ToggleEntity, RestoreEntity):
|
||||
"""Return true if script is on."""
|
||||
return self.script.is_running
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self):
|
||||
"""Return referenced blueprint or None."""
|
||||
if self._blueprint_inputs is None:
|
||||
return None
|
||||
return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]
|
||||
|
||||
@callback
|
||||
def async_change_listener(self):
|
||||
"""Update state."""
|
||||
|
||||
@@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER
|
||||
DATA_BLUEPRINTS = "script_blueprints"
|
||||
|
||||
|
||||
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
||||
"""Return True if any script references the blueprint."""
|
||||
from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel
|
||||
|
||||
return len(scripts_with_blueprint(hass, blueprint_path)) > 0
|
||||
|
||||
|
||||
@singleton(DATA_BLUEPRINTS)
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints:
|
||||
"""Get script blueprints."""
|
||||
return DomainBlueprints(hass, DOMAIN, LOGGER)
|
||||
return DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sensibo",
|
||||
"name": "Sensibo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
||||
"requirements": ["pysensibo==1.0.19"],
|
||||
"requirements": ["pysensibo==1.0.20"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Sony Songpal",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/songpal",
|
||||
"requirements": ["python-songpal==0.15"],
|
||||
"requirements": ["python-songpal==0.15.1"],
|
||||
"codeowners": ["@rytilahti", "@shenxn"],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -84,11 +84,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, address.upper(), connectable
|
||||
)
|
||||
) or await switchbot.get_device(address)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Switchbot {sensor_type} with address {address}"
|
||||
)
|
||||
|
||||
await switchbot.close_stale_connections(ble_device)
|
||||
cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice)
|
||||
device = cls(
|
||||
device=ble_device,
|
||||
@@ -108,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
if not await coordinator.async_wait_ready():
|
||||
raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready")
|
||||
raise ConfigEntryNotReady(f"{address} is not advertising state")
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
|
||||
@@ -69,13 +69,16 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
self.ble_device = service_info.device
|
||||
if adv := switchbot.parse_advertisement_data(
|
||||
service_info.device, service_info.advertisement
|
||||
):
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
if "modelName" in self.data:
|
||||
if "modelName" in adv.data:
|
||||
self._ready_event.set()
|
||||
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
||||
if not self.device.advertisement_changed(adv):
|
||||
return
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
self.device.update_from_advertisement(adv)
|
||||
super()._async_handle_bluetooth_event(service_info, change)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.18.27"],
|
||||
"requirements": ["PySwitchbot==0.19.9"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": [
|
||||
|
||||
@@ -71,14 +71,16 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Switchbot sensor based on a config entry."""
|
||||
coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
entities = [
|
||||
SwitchBotSensor(
|
||||
coordinator,
|
||||
sensor,
|
||||
)
|
||||
for sensor in coordinator.data["data"]
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
]
|
||||
entities.append(SwitchbotRSSISensor(coordinator, "rssi"))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
@@ -98,6 +100,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
self.entity_description = SENSOR_TYPES[sensor]
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
def native_value(self) -> str | int:
|
||||
"""Return the state of the sensor."""
|
||||
return self.data["data"][self._sensor]
|
||||
|
||||
|
||||
class SwitchbotRSSISensor(SwitchBotSensor):
|
||||
"""Representation of a Switchbot RSSI sensor."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.ble_device.rssi
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
{ "local_name": "ThermoBeacon", "connectable": false }
|
||||
],
|
||||
"requirements": ["thermobeacon-ble==0.3.1"],
|
||||
"requirements": ["thermobeacon-ble==0.3.2"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -30,13 +30,19 @@ OPTIONS_SCHEMA = vol.Schema(
|
||||
vol.Required(
|
||||
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_LOWER): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_UPPER): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step=1e-3
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -61,18 +61,12 @@ SERVICE_FINISH = "finish"
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
CREATE_FIELDS = {
|
||||
STORAGE_FIELDS = {
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period,
|
||||
vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean,
|
||||
}
|
||||
UPDATE_FIELDS = {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_DURATION): cv.time_period,
|
||||
vol.Optional(CONF_RESTORE): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
def _format_timedelta(delta: timedelta):
|
||||
@@ -137,7 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await storage_collection.async_load()
|
||||
|
||||
collection.StorageCollectionWebsocket(
|
||||
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
|
||||
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
|
||||
).async_setup(hass)
|
||||
|
||||
async def reload_service_handler(service_call: ServiceCall) -> None:
|
||||
@@ -171,12 +165,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class TimerStorageCollection(collection.StorageCollection):
|
||||
"""Timer storage based collection."""
|
||||
|
||||
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
|
||||
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
|
||||
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
|
||||
|
||||
async def _process_create_data(self, data: dict) -> dict:
|
||||
"""Validate the config is valid."""
|
||||
data = self.CREATE_SCHEMA(data)
|
||||
data = self.CREATE_UPDATE_SCHEMA(data)
|
||||
# make duration JSON serializeable
|
||||
data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION])
|
||||
return data
|
||||
@@ -188,7 +181,7 @@ class TimerStorageCollection(collection.StorageCollection):
|
||||
|
||||
async def _update_data(self, data: dict, update_data: dict) -> dict:
|
||||
"""Return a new updated data object."""
|
||||
data = {**data, **self.UPDATE_SCHEMA(update_data)}
|
||||
data = {CONF_ID: data[CONF_ID]} | self.CREATE_UPDATE_SCHEMA(update_data)
|
||||
# make duration JSON serializeable
|
||||
if CONF_DURATION in update_data:
|
||||
data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Viessmann ViCare",
|
||||
"documentation": "https://www.home-assistant.io/integrations/vicare",
|
||||
"codeowners": ["@oischinger"],
|
||||
"requirements": ["PyViCare==2.16.2"],
|
||||
"requirements": ["PyViCare==2.17.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
|
||||
@@ -616,8 +616,7 @@ async def handle_test_condition(
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
# Do static + dynamic validation of the condition
|
||||
config = cv.CONDITION_SCHEMA(msg["condition"])
|
||||
config = await condition.async_validate_condition_config(hass, config)
|
||||
config = await condition.async_validate_condition_config(hass, msg["condition"])
|
||||
# Test the condition
|
||||
check_condition = await condition.async_from_config(hass, config)
|
||||
connection.send_result(
|
||||
@@ -722,6 +721,9 @@ async def handle_supported_brands(
|
||||
for int_or_exc in ints_or_excs.values():
|
||||
if isinstance(int_or_exc, Exception):
|
||||
raise int_or_exc
|
||||
# Happens if a custom component without supported brands overrides a built-in one with supported brands
|
||||
if "supported_brands" not in int_or_exc.manifest:
|
||||
continue
|
||||
data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"]
|
||||
connection.send_result(msg["id"], data)
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ BINARY_SENSOR_DESCRIPTIONS = {
|
||||
key=XiaomiBinarySensorDeviceClass.SMOKE,
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
),
|
||||
XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription(
|
||||
key=XiaomiBinarySensorDeviceClass.MOISTURE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"requirements": ["xiaomi-ble==0.9.0"],
|
||||
"requirements": ["xiaomi-ble==0.10.0"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@Jc2k", "@Ernst79"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -112,15 +112,6 @@ MODELS_FAN_MIOT = [
|
||||
MODEL_FAN_ZA5,
|
||||
]
|
||||
|
||||
# number of speed levels each fan has
|
||||
SPEEDS_FAN_MIOT = {
|
||||
MODEL_FAN_1C: 3,
|
||||
MODEL_FAN_P10: 4,
|
||||
MODEL_FAN_P11: 4,
|
||||
MODEL_FAN_P9: 4,
|
||||
MODEL_FAN_ZA5: 4,
|
||||
}
|
||||
|
||||
MODELS_PURIFIER_MIOT = [
|
||||
MODEL_AIRPURIFIER_3,
|
||||
MODEL_AIRPURIFIER_3C,
|
||||
|
||||
@@ -85,7 +85,6 @@ from .const import (
|
||||
MODELS_PURIFIER_MIOT,
|
||||
SERVICE_RESET_FILTER,
|
||||
SERVICE_SET_EXTRA_FEATURES,
|
||||
SPEEDS_FAN_MIOT,
|
||||
)
|
||||
from .device import XiaomiCoordinatedMiioEntity
|
||||
|
||||
@@ -235,13 +234,11 @@ async def async_setup_entry(
|
||||
elif model in MODELS_FAN_MIIO:
|
||||
entity = XiaomiFan(device, config_entry, unique_id, coordinator)
|
||||
elif model == MODEL_FAN_ZA5:
|
||||
speed_count = SPEEDS_FAN_MIOT[model]
|
||||
entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count)
|
||||
entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator)
|
||||
elif model == MODEL_FAN_1C:
|
||||
entity = XiaomiFan1C(device, config_entry, unique_id, coordinator)
|
||||
elif model in MODELS_FAN_MIOT:
|
||||
speed_count = SPEEDS_FAN_MIOT[model]
|
||||
entity = XiaomiFanMiot(
|
||||
device, config_entry, unique_id, coordinator, speed_count
|
||||
)
|
||||
entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator)
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -1049,11 +1046,6 @@ class XiaomiFanP5(XiaomiGenericFan):
|
||||
class XiaomiFanMiot(XiaomiGenericFan):
|
||||
"""Representation of a Xiaomi Fan Miot."""
|
||||
|
||||
def __init__(self, device, entry, unique_id, coordinator, speed_count):
|
||||
"""Initialize MIOT fan with speed count."""
|
||||
super().__init__(device, entry, unique_id, coordinator)
|
||||
self._speed_count = speed_count
|
||||
|
||||
@property
|
||||
def operation_mode_class(self):
|
||||
"""Hold operation mode class."""
|
||||
@@ -1071,9 +1063,7 @@ class XiaomiFanMiot(XiaomiGenericFan):
|
||||
self._preset_mode = self.coordinator.data.mode.name
|
||||
self._oscillating = self.coordinator.data.oscillate
|
||||
if self.coordinator.data.is_on:
|
||||
self._percentage = ranged_value_to_percentage(
|
||||
(1, self._speed_count), self.coordinator.data.speed
|
||||
)
|
||||
self._percentage = self.coordinator.data.speed
|
||||
else:
|
||||
self._percentage = 0
|
||||
|
||||
@@ -1092,6 +1082,59 @@ class XiaomiFanMiot(XiaomiGenericFan):
|
||||
self._preset_mode = preset_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan."""
|
||||
if percentage == 0:
|
||||
self._percentage = 0
|
||||
await self.async_turn_off()
|
||||
return
|
||||
|
||||
result = await self._try_command(
|
||||
"Setting fan speed percentage of the miio device failed.",
|
||||
self._device.set_speed,
|
||||
percentage,
|
||||
)
|
||||
if result:
|
||||
self._percentage = percentage
|
||||
|
||||
if not self.is_on:
|
||||
await self.async_turn_on()
|
||||
elif result:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class XiaomiFanZA5(XiaomiFanMiot):
|
||||
"""Representation of a Xiaomi Fan ZA5."""
|
||||
|
||||
@property
|
||||
def operation_mode_class(self):
|
||||
"""Hold operation mode class."""
|
||||
return FanZA5OperationMode
|
||||
|
||||
|
||||
class XiaomiFan1C(XiaomiFanMiot):
|
||||
"""Representation of a Xiaomi Fan 1C (Standing Fan 2 Lite)."""
|
||||
|
||||
def __init__(self, device, entry, unique_id, coordinator):
|
||||
"""Initialize MIOT fan with speed count."""
|
||||
super().__init__(device, entry, unique_id, coordinator)
|
||||
self._speed_count = 3
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
"""Fetch state from the device."""
|
||||
self._state = self.coordinator.data.is_on
|
||||
self._preset_mode = self.coordinator.data.mode.name
|
||||
self._oscillating = self.coordinator.data.oscillate
|
||||
if self.coordinator.data.is_on:
|
||||
self._percentage = ranged_value_to_percentage(
|
||||
(1, self._speed_count), self.coordinator.data.speed
|
||||
)
|
||||
else:
|
||||
self._percentage = 0
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan."""
|
||||
if percentage == 0:
|
||||
@@ -1116,12 +1159,3 @@ class XiaomiFanMiot(XiaomiGenericFan):
|
||||
if result:
|
||||
self._percentage = ranged_value_to_percentage((1, self._speed_count), speed)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class XiaomiFanZA5(XiaomiFanMiot):
|
||||
"""Representation of a Xiaomi Fan ZA5."""
|
||||
|
||||
@property
|
||||
def operation_mode_class(self):
|
||||
"""Hold operation mode class."""
|
||||
return FanZA5OperationMode
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
"name": "Yale Access Bluetooth",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"requirements": ["yalexs-ble==1.6.4"],
|
||||
"requirements": ["yalexs-ble==1.9.2"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [{ "manufacturer_id": 465 }],
|
||||
"bluetooth": [
|
||||
{
|
||||
"manufacturer_id": 465,
|
||||
"service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"iot_class": "local_push",
|
||||
"supported_brands": {
|
||||
"august_ble": "August Bluetooth"
|
||||
|
||||
@@ -84,7 +84,7 @@ PARALLEL_UPDATES = 0
|
||||
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
|
||||
SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start"
|
||||
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished"
|
||||
DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"}
|
||||
DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"}
|
||||
|
||||
COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY}
|
||||
SUPPORT_GROUP_LIGHT = (
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.79",
|
||||
"zigpy-deconz==0.18.0",
|
||||
"zigpy==0.50.2",
|
||||
"zigpy-deconz==0.18.1",
|
||||
"zigpy==0.50.3",
|
||||
"zigpy-xbee==0.15.0",
|
||||
"zigpy-zigate==0.9.2",
|
||||
"zigpy-znp==0.8.2"
|
||||
|
||||
@@ -51,6 +51,9 @@ VALUES_TO_REDACT = (
|
||||
|
||||
def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType:
|
||||
"""Redact value of a Z-Wave value."""
|
||||
# If the value has no value, there is nothing to redact
|
||||
if zwave_value.get("value") in (None, ""):
|
||||
return zwave_value
|
||||
for value_to_redact in VALUES_TO_REDACT:
|
||||
command_class = None
|
||||
if "commandClass" in zwave_value:
|
||||
|
||||
@@ -12,7 +12,12 @@ from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import NodeStatus
|
||||
from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress
|
||||
from zwave_js_server.model.firmware import (
|
||||
FirmwareUpdateFinished,
|
||||
FirmwareUpdateInfo,
|
||||
FirmwareUpdateProgress,
|
||||
FirmwareUpdateStatus,
|
||||
)
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@@ -82,13 +87,16 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self._status_unsub: Callable[[], None] | None = None
|
||||
self._poll_unsub: Callable[[], None] | None = None
|
||||
self._progress_unsub: Callable[[], None] | None = None
|
||||
self._finished_unsub: Callable[[], None] | None = None
|
||||
self._num_files_installed: int = 0
|
||||
self._finished_event = asyncio.Event()
|
||||
self._finished_status: FirmwareUpdateStatus | None = None
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_name = "Firmware"
|
||||
self._base_unique_id = get_valueless_base_unique_id(driver, node)
|
||||
self._attr_unique_id = f"{self._base_unique_id}.firmware_update"
|
||||
self._attr_installed_version = self._attr_latest_version = node.firmware_version
|
||||
self._attr_installed_version = node.firmware_version
|
||||
# device may not be precreated in main handler yet
|
||||
self._attr_device_info = get_device_info(driver, node)
|
||||
|
||||
@@ -119,18 +127,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _reset_progress(self) -> None:
|
||||
"""Reset update install progress."""
|
||||
def _update_finished(self, event: dict[str, Any]) -> None:
|
||||
"""Update install progress on event."""
|
||||
finished: FirmwareUpdateFinished = event["firmware_update_finished"]
|
||||
self._finished_status = finished.status
|
||||
self._finished_event.set()
|
||||
|
||||
@callback
|
||||
def _unsub_firmware_events_and_reset_progress(
|
||||
self, write_state: bool = True
|
||||
) -> None:
|
||||
"""Unsubscribe from firmware events and reset update install progress."""
|
||||
if self._progress_unsub:
|
||||
self._progress_unsub()
|
||||
self._progress_unsub = None
|
||||
|
||||
if self._finished_unsub:
|
||||
self._finished_unsub()
|
||||
self._finished_unsub = None
|
||||
|
||||
self._finished_status = None
|
||||
self._finished_event.clear()
|
||||
self._num_files_installed = 0
|
||||
self._attr_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
self._attr_in_progress = 0
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
|
||||
"""Update the entity."""
|
||||
self._poll_unsub = None
|
||||
|
||||
# If device is asleep/dead, wait for it to wake up/become alive before
|
||||
# attempting an update
|
||||
for status, event_name in (
|
||||
(NodeStatus.ASLEEP, "wake up"),
|
||||
(NodeStatus.DEAD, "alive"),
|
||||
@@ -156,20 +184,26 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
err,
|
||||
)
|
||||
else:
|
||||
if available_firmware_updates:
|
||||
self._latest_version_firmware = latest_firmware = max(
|
||||
available_firmware_updates,
|
||||
key=lambda x: AwesomeVersion(x.version),
|
||||
# If we have an available firmware update that is a higher version than
|
||||
# what's on the node, we should advertise it, otherwise the installed
|
||||
# version is the latest.
|
||||
if (
|
||||
available_firmware_updates
|
||||
and (
|
||||
latest_firmware := max(
|
||||
available_firmware_updates,
|
||||
key=lambda x: AwesomeVersion(x.version),
|
||||
)
|
||||
)
|
||||
|
||||
# If we have an available firmware update that is a higher version than
|
||||
# what's on the node, we should advertise it, otherwise there is
|
||||
# nothing to do.
|
||||
new_version = latest_firmware.version
|
||||
current_version = self.node.firmware_version
|
||||
if AwesomeVersion(new_version) > AwesomeVersion(current_version):
|
||||
self._attr_latest_version = new_version
|
||||
self.async_write_ha_state()
|
||||
and AwesomeVersion(latest_firmware.version)
|
||||
> AwesomeVersion(self.node.firmware_version)
|
||||
):
|
||||
self._latest_version_firmware = latest_firmware
|
||||
self._attr_latest_version = latest_firmware.version
|
||||
self.async_write_ha_state()
|
||||
elif self._attr_latest_version != self._attr_installed_version:
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
self.async_write_ha_state()
|
||||
finally:
|
||||
self._poll_unsub = async_call_later(
|
||||
self.hass, timedelta(days=1), self._async_update
|
||||
@@ -187,28 +221,57 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
"""Install an update."""
|
||||
firmware = self._latest_version_firmware
|
||||
assert firmware
|
||||
self._attr_in_progress = 0
|
||||
self._unsub_firmware_events_and_reset_progress(False)
|
||||
self._attr_in_progress = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._progress_unsub = self.node.on(
|
||||
"firmware update progress", self._update_progress
|
||||
)
|
||||
self._finished_unsub = self.node.on(
|
||||
"firmware update finished", self._update_finished
|
||||
)
|
||||
|
||||
for file in firmware.files:
|
||||
try:
|
||||
await self.driver.controller.async_begin_ota_firmware_update(
|
||||
self.node, file
|
||||
)
|
||||
except BaseZwaveJSServerError as err:
|
||||
self._reset_progress()
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
raise HomeAssistantError(err) from err
|
||||
|
||||
# We need to block until we receive the `firmware update finished` event
|
||||
await self._finished_event.wait()
|
||||
# Clear the event so that a second firmware update blocks again
|
||||
self._finished_event.clear()
|
||||
assert self._finished_status is not None
|
||||
|
||||
# If status is not OK, we should throw an error to let the user know
|
||||
if self._finished_status not in (
|
||||
FirmwareUpdateStatus.OK_NO_RESTART,
|
||||
FirmwareUpdateStatus.OK_RESTART_PENDING,
|
||||
FirmwareUpdateStatus.OK_WAITING_FOR_ACTIVATION,
|
||||
):
|
||||
status = self._finished_status
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
raise HomeAssistantError(status.name.replace("_", " ").title())
|
||||
|
||||
# If we get here, the firmware installation was successful and we need to
|
||||
# update progress accordingly
|
||||
self._num_files_installed += 1
|
||||
self._attr_in_progress = floor(
|
||||
100 * self._num_files_installed / len(firmware.files)
|
||||
)
|
||||
|
||||
# Clear the status so we can get a new one
|
||||
self._finished_status = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
# If we get here, all files were installed successfully
|
||||
self._attr_installed_version = self._attr_latest_version = firmware.version
|
||||
self._latest_version_firmware = None
|
||||
self._reset_progress()
|
||||
self._unsub_firmware_events_and_reset_progress()
|
||||
|
||||
async def async_poll_value(self, _: bool) -> None:
|
||||
"""Poll a value."""
|
||||
@@ -255,6 +318,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
|
||||
self._poll_unsub()
|
||||
self._poll_unsub = None
|
||||
|
||||
if self._progress_unsub:
|
||||
self._progress_unsub()
|
||||
self._progress_unsub = None
|
||||
self._unsub_firmware_events_and_reset_progress(False)
|
||||
|
||||
@@ -1024,7 +1024,10 @@ class ConfigEntries:
|
||||
raise UnknownEntry
|
||||
|
||||
if entry.state is not ConfigEntryState.NOT_LOADED:
|
||||
raise OperationNotAllowed
|
||||
raise OperationNotAllowed(
|
||||
f"The config entry {entry.title} ({entry.domain}) with entry_id {entry.entry_id}"
|
||||
f" cannot be setup because is already loaded in the {entry.state} state"
|
||||
)
|
||||
|
||||
# Setup Component if not set up yet
|
||||
if entry.domain in self.hass.config.components:
|
||||
@@ -1046,7 +1049,10 @@ class ConfigEntries:
|
||||
raise UnknownEntry
|
||||
|
||||
if not entry.state.recoverable:
|
||||
raise OperationNotAllowed
|
||||
raise OperationNotAllowed(
|
||||
f"The config entry {entry.title} ({entry.domain}) with entry_id "
|
||||
f"{entry.entry_id} cannot be unloaded because it is not in a recoverable state ({entry.state})"
|
||||
)
|
||||
|
||||
return await entry.async_unload(self.hass)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "5"
|
||||
__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)
|
||||
|
||||
@@ -62,6 +62,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
|
||||
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
|
||||
"connectable": False
|
||||
},
|
||||
{
|
||||
"domain": "govee_ble",
|
||||
"manufacturer_id": 57391,
|
||||
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
|
||||
"connectable": False
|
||||
},
|
||||
{
|
||||
"domain": "govee_ble",
|
||||
"manufacturer_id": 18994,
|
||||
@@ -281,6 +287,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
|
||||
},
|
||||
{
|
||||
"domain": "yalexs_ble",
|
||||
"manufacturer_id": 465
|
||||
"manufacturer_id": 465,
|
||||
"service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -454,6 +454,22 @@ class EntityPlatform:
|
||||
self.scan_interval,
|
||||
)
|
||||
|
||||
def _entity_id_already_exists(self, entity_id: str) -> tuple[bool, bool]:
|
||||
"""Check if an entity_id already exists.
|
||||
|
||||
Returns a tuple [already_exists, restored]
|
||||
"""
|
||||
already_exists = entity_id in self.entities
|
||||
restored = False
|
||||
|
||||
if not already_exists and not self.hass.states.async_available(entity_id):
|
||||
existing = self.hass.states.get(entity_id)
|
||||
if existing is not None and ATTR_RESTORED in existing.attributes:
|
||||
restored = True
|
||||
else:
|
||||
already_exists = True
|
||||
return (already_exists, restored)
|
||||
|
||||
async def _async_add_entity( # noqa: C901
|
||||
self,
|
||||
entity: Entity,
|
||||
@@ -480,12 +496,31 @@ class EntityPlatform:
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
requested_entity_id = None
|
||||
suggested_object_id: str | None = None
|
||||
generate_new_entity_id = False
|
||||
|
||||
# Get entity_id from unique ID registration
|
||||
if entity.unique_id is not None:
|
||||
registered_entity_id = entity_registry.async_get_entity_id(
|
||||
self.domain, self.platform_name, entity.unique_id
|
||||
)
|
||||
if registered_entity_id:
|
||||
already_exists, _ = self._entity_id_already_exists(registered_entity_id)
|
||||
|
||||
if already_exists:
|
||||
# If there's a collision, the entry belongs to another entity
|
||||
entity.registry_entry = None
|
||||
msg = (
|
||||
f"Platform {self.platform_name} does not generate unique IDs. "
|
||||
)
|
||||
if entity.entity_id:
|
||||
msg += f"ID {entity.unique_id} is already used by {registered_entity_id} - ignoring {entity.entity_id}"
|
||||
else:
|
||||
msg += f"ID {entity.unique_id} already exists - ignoring {registered_entity_id}"
|
||||
self.logger.error(msg)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
if self.config_entry is not None:
|
||||
config_entry_id: str | None = self.config_entry.entry_id
|
||||
else:
|
||||
@@ -541,7 +576,6 @@ class EntityPlatform:
|
||||
pass
|
||||
|
||||
if entity.entity_id is not None:
|
||||
requested_entity_id = entity.entity_id
|
||||
suggested_object_id = split_entity_id(entity.entity_id)[1]
|
||||
else:
|
||||
if device and entity.has_entity_name: # type: ignore[unreachable]
|
||||
@@ -592,16 +626,6 @@ class EntityPlatform:
|
||||
entity.registry_entry = entry
|
||||
entity.entity_id = entry.entity_id
|
||||
|
||||
if entry.disabled:
|
||||
self.logger.debug(
|
||||
"Not adding entity %s because it's disabled",
|
||||
entry.name
|
||||
or entity.name
|
||||
or f'"{self.platform_name} {entity.unique_id}"',
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
# We won't generate an entity ID if the platform has already set one
|
||||
# We will however make sure that platform cannot pick a registered ID
|
||||
elif entity.entity_id is not None and entity_registry.async_is_registered(
|
||||
@@ -628,28 +652,22 @@ class EntityPlatform:
|
||||
entity.add_to_platform_abort()
|
||||
raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}")
|
||||
|
||||
already_exists = entity.entity_id in self.entities
|
||||
restored = False
|
||||
|
||||
if not already_exists and not self.hass.states.async_available(
|
||||
entity.entity_id
|
||||
):
|
||||
existing = self.hass.states.get(entity.entity_id)
|
||||
if existing is not None and ATTR_RESTORED in existing.attributes:
|
||||
restored = True
|
||||
else:
|
||||
already_exists = True
|
||||
already_exists, restored = self._entity_id_already_exists(entity.entity_id)
|
||||
|
||||
if already_exists:
|
||||
if entity.unique_id is not None:
|
||||
msg = f"Platform {self.platform_name} does not generate unique IDs. "
|
||||
if requested_entity_id:
|
||||
msg += f"ID {entity.unique_id} is already used by {entity.entity_id} - ignoring {requested_entity_id}"
|
||||
else:
|
||||
msg += f"ID {entity.unique_id} already exists - ignoring {entity.entity_id}"
|
||||
else:
|
||||
msg = f"Entity id already exists - ignoring: {entity.entity_id}"
|
||||
self.logger.error(msg)
|
||||
self.logger.error(
|
||||
f"Entity id already exists - ignoring: {entity.entity_id}"
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
if entity.registry_entry and entity.registry_entry.disabled:
|
||||
self.logger.debug(
|
||||
"Not adding entity %s because it's disabled",
|
||||
entry.name
|
||||
or entity.name
|
||||
or f'"{self.platform_name} {entity.unique_id}"',
|
||||
)
|
||||
entity.add_to_platform_abort()
|
||||
return
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
PyJWT==2.4.0
|
||||
PyNaCl==1.5.0
|
||||
aiodiscover==1.4.11
|
||||
aiodiscover==1.4.13
|
||||
aiohttp==3.8.1
|
||||
aiohttp_cors==0.7.0
|
||||
astral==2.2
|
||||
@@ -10,16 +10,18 @@ atomicwrites-homeassistant==1.4.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.8.0
|
||||
bcrypt==3.1.7
|
||||
bleak==0.16.0
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
bleak-retry-connector==1.17.1
|
||||
bleak==0.17.0
|
||||
bluetooth-adapters==0.4.1
|
||||
bluetooth-auto-recovery==0.3.3
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
cryptography==37.0.4
|
||||
dbus-fast==1.4.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.55.0
|
||||
home-assistant-bluetooth==1.3.0
|
||||
home-assistant-frontend==20220907.0
|
||||
home-assistant-frontend==20220907.2
|
||||
httpx==0.23.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.9.1"
|
||||
version = "2022.9.5"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.18.27
|
||||
PySwitchbot==0.19.9
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -47,7 +47,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.6.7
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.16.2
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.13.4
|
||||
@@ -134,7 +134,7 @@ aiobafi6==0.7.2
|
||||
aiobotocore==2.1.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.4.11
|
||||
aiodiscover==1.4.13
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -147,7 +147,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
aioecowitt==2022.09.1
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
@@ -171,7 +171,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.5.2
|
||||
aiohomekit==1.5.9
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -408,13 +408,16 @@ bimmer_connected==0.10.2
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.16.0
|
||||
bleak-retry-connector==1.17.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.17.0
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.0.2
|
||||
|
||||
# homeassistant.components.blink
|
||||
blinkpy==0.19.0
|
||||
blinkpy==0.19.2
|
||||
|
||||
# homeassistant.components.blinksticklight
|
||||
blinkstick==1.2.0
|
||||
@@ -430,10 +433,10 @@ bluemaestro-ble==0.2.0
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-adapters==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
bluetooth-auto-recovery==0.3.3
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-async==0.1.22
|
||||
@@ -534,6 +537,9 @@ datadog==0.15.0
|
||||
# homeassistant.components.metoffice
|
||||
datapoint==0.9.8
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==1.4.0
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.6.3
|
||||
|
||||
@@ -552,7 +558,7 @@ defusedxml==0.7.1
|
||||
deluge-client==1.7.1
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==0.2.2
|
||||
demetriek==0.2.4
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==0.10.11
|
||||
@@ -772,7 +778,7 @@ googlemaps==2.5.1
|
||||
goslide-api==0.5.1
|
||||
|
||||
# homeassistant.components.govee_ble
|
||||
govee-ble==0.17.2
|
||||
govee-ble==0.17.3
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
@@ -851,7 +857,7 @@ hole==0.7.0
|
||||
holidays==0.14.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220907.0
|
||||
home-assistant-frontend==20220907.2
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -968,7 +974,7 @@ lakeside==0.12
|
||||
laundrify_aio==1.1.2
|
||||
|
||||
# homeassistant.components.led_ble
|
||||
led-ble==0.7.1
|
||||
led-ble==0.10.1
|
||||
|
||||
# homeassistant.components.foscam
|
||||
libpyfoscam==1.0
|
||||
@@ -1608,7 +1614,7 @@ pyinsteon==1.2.0
|
||||
pyintesishome==1.8.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.2
|
||||
pyipma==3.0.4
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.11.0
|
||||
@@ -1820,7 +1826,7 @@ pyrecswitch==1.0.2
|
||||
pyrepetierng==0.1.0
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.5.4
|
||||
pyrisco==0.5.5
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
@@ -1838,7 +1844,7 @@ pysaj==0.0.16
|
||||
pysdcp==1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.19
|
||||
pysensibo==1.0.20
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@@ -1999,7 +2005,7 @@ python-ripple-api==0.0.3
|
||||
python-smarttub==0.0.33
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.15
|
||||
python-songpal==0.15.1
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.12.0
|
||||
@@ -2094,7 +2100,7 @@ pyzbar==0.1.7
|
||||
pyzerproc==0.4.8
|
||||
|
||||
# homeassistant.components.qingping
|
||||
qingping-ble==0.6.0
|
||||
qingping-ble==0.7.0
|
||||
|
||||
# homeassistant.components.qnap
|
||||
qnapstats==0.4.0
|
||||
@@ -2118,7 +2124,7 @@ raincloudy==0.0.7
|
||||
raspyrfm-client==1.2.8
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2022.09.0
|
||||
regenmaschine==2022.09.1
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.11
|
||||
@@ -2363,7 +2369,7 @@ tesla-wall-connector==1.0.2
|
||||
# tf-models-official==2.5.0
|
||||
|
||||
# homeassistant.components.thermobeacon
|
||||
thermobeacon-ble==0.3.1
|
||||
thermobeacon-ble==0.3.2
|
||||
|
||||
# homeassistant.components.thermopro
|
||||
thermopro-ble==0.4.3
|
||||
@@ -2522,7 +2528,7 @@ xbox-webapi==2.0.11
|
||||
xboxapi==2.0.1
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.9.0
|
||||
xiaomi-ble==0.10.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==1.0.2
|
||||
@@ -2542,7 +2548,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.6.4
|
||||
yalexs-ble==1.9.2
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.1
|
||||
@@ -2578,7 +2584,7 @@ zhong_hong_hvac==1.0.9
|
||||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.18.0
|
||||
zigpy-deconz==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.15.0
|
||||
@@ -2590,7 +2596,7 @@ zigpy-zigate==0.9.2
|
||||
zigpy-znp==0.8.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.50.2
|
||||
zigpy==0.50.3
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.18.27
|
||||
PySwitchbot==0.19.9
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -43,7 +43,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.6.7
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.16.2
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.13.4
|
||||
@@ -121,7 +121,7 @@ aiobafi6==0.7.2
|
||||
aiobotocore==2.1.0
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.4.11
|
||||
aiodiscover==1.4.13
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
# homeassistant.components.minecraft_server
|
||||
@@ -134,7 +134,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2022.08.3
|
||||
aioecowitt==2022.09.1
|
||||
|
||||
# homeassistant.components.emonitor
|
||||
aioemonitor==1.0.5
|
||||
@@ -155,7 +155,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==1.5.2
|
||||
aiohomekit==1.5.9
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -329,22 +329,25 @@ bellows==0.33.1
|
||||
bimmer_connected==0.10.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.16.0
|
||||
bleak-retry-connector==1.17.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.17.0
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.0.2
|
||||
|
||||
# homeassistant.components.blink
|
||||
blinkpy==0.19.0
|
||||
blinkpy==0.19.2
|
||||
|
||||
# homeassistant.components.bluemaestro
|
||||
bluemaestro-ble==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.3.5
|
||||
bluetooth-adapters==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.2
|
||||
bluetooth-auto-recovery==0.3.3
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-async==0.1.22
|
||||
@@ -411,6 +414,9 @@ datadog==0.15.0
|
||||
# homeassistant.components.metoffice
|
||||
datapoint==0.9.8
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==1.4.0
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.6.3
|
||||
|
||||
@@ -423,7 +429,7 @@ defusedxml==0.7.1
|
||||
deluge-client==1.7.1
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==0.2.2
|
||||
demetriek==0.2.4
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==0.10.11
|
||||
@@ -573,7 +579,7 @@ google-nest-sdm==2.0.0
|
||||
googlemaps==2.5.1
|
||||
|
||||
# homeassistant.components.govee_ble
|
||||
govee-ble==0.17.2
|
||||
govee-ble==0.17.3
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==1.3.0
|
||||
@@ -628,7 +634,7 @@ hole==0.7.0
|
||||
holidays==0.14.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20220907.0
|
||||
home-assistant-frontend==20220907.2
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -706,7 +712,7 @@ lacrosse-view==0.0.9
|
||||
laundrify_aio==1.1.2
|
||||
|
||||
# homeassistant.components.led_ble
|
||||
led-ble==0.7.1
|
||||
led-ble==0.10.1
|
||||
|
||||
# homeassistant.components.foscam
|
||||
libpyfoscam==1.0
|
||||
@@ -1121,7 +1127,7 @@ pyicloud==1.0.0
|
||||
pyinsteon==1.2.0
|
||||
|
||||
# homeassistant.components.ipma
|
||||
pyipma==3.0.2
|
||||
pyipma==3.0.4
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.11.0
|
||||
@@ -1273,7 +1279,7 @@ pyps4-2ndscreen==1.3.1
|
||||
pyqwikswitch==0.93
|
||||
|
||||
# homeassistant.components.risco
|
||||
pyrisco==0.5.4
|
||||
pyrisco==0.5.5
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
@@ -1285,7 +1291,7 @@ pyruckus==0.16
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.19
|
||||
pysensibo==1.0.20
|
||||
|
||||
# homeassistant.components.serial
|
||||
# homeassistant.components.zha
|
||||
@@ -1371,7 +1377,7 @@ python-picnic-api==1.1.0
|
||||
python-smarttub==0.0.33
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.15
|
||||
python-songpal==0.15.1
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.12.0
|
||||
@@ -1439,7 +1445,7 @@ pyws66i==1.1
|
||||
pyzerproc==0.4.8
|
||||
|
||||
# homeassistant.components.qingping
|
||||
qingping-ble==0.6.0
|
||||
qingping-ble==0.7.0
|
||||
|
||||
# homeassistant.components.rachio
|
||||
rachiopy==1.0.3
|
||||
@@ -1451,7 +1457,7 @@ radios==0.1.1
|
||||
radiotherm==2.1.0
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==2022.09.0
|
||||
regenmaschine==2022.09.1
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.11
|
||||
@@ -1612,7 +1618,7 @@ tesla-powerwall==0.3.18
|
||||
tesla-wall-connector==1.0.2
|
||||
|
||||
# homeassistant.components.thermobeacon
|
||||
thermobeacon-ble==0.3.1
|
||||
thermobeacon-ble==0.3.2
|
||||
|
||||
# homeassistant.components.thermopro
|
||||
thermopro-ble==0.4.3
|
||||
@@ -1729,7 +1735,7 @@ wolf_smartset==0.1.11
|
||||
xbox-webapi==2.0.11
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.9.0
|
||||
xiaomi-ble==0.10.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==1.0.2
|
||||
@@ -1746,7 +1752,7 @@ xmltodict==0.13.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.6.4
|
||||
yalexs-ble==1.9.2
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.1
|
||||
@@ -1767,7 +1773,7 @@ zeroconf==0.39.1
|
||||
zha-quirks==0.0.79
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.18.0
|
||||
zigpy-deconz==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee==0.15.0
|
||||
@@ -1779,7 +1785,7 @@ zigpy-zigate==0.9.2
|
||||
zigpy-znp==0.8.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.50.2
|
||||
zigpy==0.50.3
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.41.1
|
||||
|
||||
@@ -47,7 +47,9 @@ def blueprint_2():
|
||||
@pytest.fixture
|
||||
def domain_bps(hass):
|
||||
"""Domain blueprints fixture."""
|
||||
return models.DomainBlueprints(hass, "automation", logging.getLogger(__name__))
|
||||
return models.DomainBlueprints(
|
||||
hass, "automation", logging.getLogger(__name__), None
|
||||
)
|
||||
|
||||
|
||||
def test_blueprint_model_init():
|
||||
|
||||
@@ -8,13 +8,26 @@ from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.yaml import parse_yaml
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def automation_config():
|
||||
"""Automation config."""
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def script_config():
|
||||
"""Script config."""
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_bp(hass):
|
||||
async def setup_bp(hass, automation_config, script_config):
|
||||
"""Fixture to set up the blueprint component."""
|
||||
assert await async_setup_component(hass, "blueprint", {})
|
||||
|
||||
# Trigger registration of automation blueprints
|
||||
await async_setup_component(hass, "automation", {})
|
||||
# Trigger registration of automation and script blueprints
|
||||
await async_setup_component(hass, "automation", automation_config)
|
||||
await async_setup_component(hass, "script", script_config)
|
||||
|
||||
|
||||
async def test_list_blueprints(hass, hass_ws_client):
|
||||
@@ -251,3 +264,89 @@ async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_cli
|
||||
|
||||
assert msg["id"] == 9
|
||||
assert not msg["success"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"automation_config",
|
||||
(
|
||||
{
|
||||
"automation": {
|
||||
"use_blueprint": {
|
||||
"path": "test_event_service.yaml",
|
||||
"input": {
|
||||
"trigger_event": "blueprint_event",
|
||||
"service_to_call": "test.automation",
|
||||
"a_number": 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
async def test_delete_blueprint_in_use_by_automation(
|
||||
hass, aioclient_mock, hass_ws_client
|
||||
):
|
||||
"""Test deleting a blueprint which is in use."""
|
||||
|
||||
with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock:
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 9,
|
||||
"type": "blueprint/delete",
|
||||
"path": "test_event_service.yaml",
|
||||
"domain": "automation",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not unlink_mock.mock_calls
|
||||
assert msg["id"] == 9
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == {
|
||||
"code": "unknown_error",
|
||||
"message": "Blueprint in use",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"script_config",
|
||||
(
|
||||
{
|
||||
"script": {
|
||||
"test_script": {
|
||||
"use_blueprint": {
|
||||
"path": "test_service.yaml",
|
||||
"input": {
|
||||
"service_to_call": "test.automation",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
async def test_delete_blueprint_in_use_by_script(hass, aioclient_mock, hass_ws_client):
|
||||
"""Test deleting a blueprint which is in use."""
|
||||
|
||||
with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock:
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 9,
|
||||
"type": "blueprint/delete",
|
||||
"path": "test_service.yaml",
|
||||
"domain": "script",
|
||||
}
|
||||
)
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not unlink_mock.mock_calls
|
||||
assert msg["id"] == 9
|
||||
assert not msg["success"]
|
||||
assert msg["error"] == {
|
||||
"code": "unknown_error",
|
||||
"message": "Blueprint in use",
|
||||
}
|
||||
|
||||
@@ -1321,6 +1321,61 @@ async def test_register_callback_by_manufacturer_id(
|
||||
assert service_info.manufacturer_id == 21
|
||||
|
||||
|
||||
async def test_register_callback_by_connectable(
|
||||
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||
):
|
||||
"""Test registering a callback by connectable."""
|
||||
mock_bt = []
|
||||
callbacks = []
|
||||
|
||||
def _fake_subscriber(
|
||||
service_info: BluetoothServiceInfo, change: BluetoothChange
|
||||
) -> None:
|
||||
"""Fake subscriber for the BleakScanner."""
|
||||
callbacks.append((service_info, change))
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
|
||||
):
|
||||
await async_setup_with_default_adapter(hass)
|
||||
|
||||
with patch.object(hass.config_entries.flow, "async_init"):
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cancel = bluetooth.async_register_callback(
|
||||
hass,
|
||||
_fake_subscriber,
|
||||
{CONNECTABLE: False},
|
||||
BluetoothScanningMode.ACTIVE,
|
||||
)
|
||||
|
||||
assert len(mock_bleak_scanner_start.mock_calls) == 1
|
||||
|
||||
apple_device = BLEDevice("44:44:33:11:23:45", "rtx")
|
||||
apple_adv = AdvertisementData(
|
||||
local_name="rtx",
|
||||
manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"},
|
||||
)
|
||||
|
||||
inject_advertisement(hass, apple_device, apple_adv)
|
||||
|
||||
empty_device = BLEDevice("11:22:33:44:55:66", "empty")
|
||||
empty_adv = AdvertisementData(local_name="empty")
|
||||
|
||||
inject_advertisement(hass, empty_device, empty_adv)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
cancel()
|
||||
|
||||
assert len(callbacks) == 2
|
||||
|
||||
service_info: BluetoothServiceInfo = callbacks[0][0]
|
||||
assert service_info.name == "rtx"
|
||||
service_info: BluetoothServiceInfo = callbacks[1][0]
|
||||
assert service_info.name == "empty"
|
||||
|
||||
|
||||
async def test_filtering_noisy_apple_devices(
|
||||
hass, mock_bleak_scanner_start, enable_bluetooth
|
||||
):
|
||||
|
||||
@@ -7,7 +7,7 @@ from bleak.backends.scanner import (
|
||||
AdvertisementDataCallback,
|
||||
BLEDevice,
|
||||
)
|
||||
from dbus_next import InvalidMessageError
|
||||
from dbus_fast import InvalidMessageError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
|
||||
@@ -591,17 +591,15 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||
async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
"""Test updating min/max updates the state."""
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": "from_storage",
|
||||
"initial": 15,
|
||||
"name": "from storage",
|
||||
"maximum": 100,
|
||||
"minimum": 10,
|
||||
"step": 3,
|
||||
"restore": True,
|
||||
}
|
||||
]
|
||||
settings = {
|
||||
"initial": 15,
|
||||
"name": "from storage",
|
||||
"maximum": 100,
|
||||
"minimum": 10,
|
||||
"step": 3,
|
||||
"restore": True,
|
||||
}
|
||||
items = [{"id": "from_storage"} | settings]
|
||||
assert await storage_setup(items)
|
||||
|
||||
input_id = "from_storage"
|
||||
@@ -618,16 +616,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = settings | {"minimum": 19}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"minimum": 19,
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert int(state.state) == 19
|
||||
@@ -635,18 +635,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
assert state.attributes[ATTR_MAXIMUM] == 100
|
||||
assert state.attributes[ATTR_STEP] == 3
|
||||
|
||||
updated_settings = settings | {"maximum": 5, "minimum": 2, "step": 5}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"maximum": 5,
|
||||
"minimum": 2,
|
||||
"step": 5,
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert int(state.state) == 5
|
||||
@@ -654,18 +654,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
assert state.attributes[ATTR_MAXIMUM] == 5
|
||||
assert state.attributes[ATTR_STEP] == 5
|
||||
|
||||
updated_settings = settings | {"maximum": None, "minimum": None, "step": 6}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 8,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"maximum": None,
|
||||
"minimum": None,
|
||||
"step": 6,
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert int(state.state) == 5
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
ENERGY_MEGA_WATT_HOUR,
|
||||
ENERGY_WATT_HOUR,
|
||||
STATE_UNKNOWN,
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -841,10 +842,13 @@ async def test_cost_sensor_handle_price_units(
|
||||
assert state.state == "20.0"
|
||||
|
||||
|
||||
async def test_cost_sensor_handle_gas(hass, hass_storage, setup_integration) -> None:
|
||||
@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
|
||||
async def test_cost_sensor_handle_gas(
|
||||
hass, hass_storage, setup_integration, unit
|
||||
) -> None:
|
||||
"""Test gas cost price from sensor entity."""
|
||||
energy_attributes = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
|
||||
ATTR_UNIT_OF_MEASUREMENT: unit,
|
||||
ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
energy_data = data.EnergyManager.default_preferences()
|
||||
|
||||
@@ -40,7 +40,11 @@ def storage_setup(hass, hass_storage):
|
||||
"data": {"items": [{"id": "from_storage", "name": "from storage"}]},
|
||||
}
|
||||
else:
|
||||
hass_storage[DOMAIN] = items
|
||||
hass_storage[DOMAIN] = {
|
||||
"key": DOMAIN,
|
||||
"version": 1,
|
||||
"data": {"items": items},
|
||||
}
|
||||
if config is None:
|
||||
config = {DOMAIN: {}}
|
||||
return await async_setup_component(hass, DOMAIN, config)
|
||||
@@ -332,6 +336,89 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None
|
||||
|
||||
|
||||
async def test_ws_update(hass, hass_ws_client, storage_setup):
|
||||
"""Test update WS."""
|
||||
|
||||
settings = {
|
||||
"name": "from storage",
|
||||
}
|
||||
items = [{"id": "from_storage"} | settings]
|
||||
assert await storage_setup(items)
|
||||
|
||||
input_id = "from_storage"
|
||||
input_entity_id = f"{DOMAIN}.{input_id}"
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state is not None
|
||||
assert state.state
|
||||
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = settings | {"name": "new_name", "icon": "mdi:blah"}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state.attributes["icon"] == "mdi:blah"
|
||||
assert state.attributes["friendly_name"] == "new_name"
|
||||
|
||||
updated_settings = settings | {"name": "new_name_2"}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert "icon" not in state.attributes
|
||||
assert state.attributes["friendly_name"] == "new_name_2"
|
||||
|
||||
|
||||
async def test_ws_create(hass, hass_ws_client, storage_setup):
|
||||
"""Test create WS."""
|
||||
assert await storage_setup(items=[])
|
||||
|
||||
input_id = "new_input"
|
||||
input_entity_id = f"{DOMAIN}.{input_id}"
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state is None
|
||||
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/create",
|
||||
"name": "New Input",
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state.state
|
||||
|
||||
|
||||
async def test_setup_no_config(hass, hass_admin_user):
|
||||
"""Test component setup with no config."""
|
||||
count_start = len(hass.states.async_entity_ids())
|
||||
|
||||
@@ -305,6 +305,7 @@ async def test_ws_create_update(hass, hass_ws_client, storage_setup):
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "new", "name": "newer"}
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new")
|
||||
assert state is not None
|
||||
|
||||
@@ -583,17 +583,23 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = {
|
||||
CONF_NAME: "even newer name",
|
||||
CONF_HAS_DATE: False,
|
||||
CONF_HAS_TIME: True,
|
||||
CONF_INITIAL: INITIAL_DATETIME,
|
||||
}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
ATTR_NAME: "even newer name",
|
||||
CONF_HAS_DATE: False,
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state.state == INITIAL_TIME
|
||||
|
||||
@@ -416,7 +416,7 @@ async def test_load_from_storage(hass, storage_setup):
|
||||
"""Test set up from storage."""
|
||||
assert await storage_setup()
|
||||
state = hass.states.get(f"{DOMAIN}.from_storage")
|
||||
assert float(state.state) == 10
|
||||
assert float(state.state) == 0 # initial is not supported when loading from storage
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage"
|
||||
assert state.attributes.get(ATTR_EDITABLE)
|
||||
|
||||
@@ -438,7 +438,7 @@ async def test_editable_state_attribute(hass, storage_setup):
|
||||
)
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.from_storage")
|
||||
assert float(state.state) == 10
|
||||
assert float(state.state) == 0
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage"
|
||||
assert state.attributes.get(ATTR_EDITABLE)
|
||||
|
||||
@@ -507,16 +507,14 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||
async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
"""Test updating min/max updates the state."""
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": "from_storage",
|
||||
"name": "from storage",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"step": 1,
|
||||
"mode": "slider",
|
||||
}
|
||||
]
|
||||
settings = {
|
||||
"name": "from storage",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"step": 1,
|
||||
"mode": "slider",
|
||||
}
|
||||
items = [{"id": "from_storage"} | settings]
|
||||
assert await storage_setup(items)
|
||||
|
||||
input_id = "from_storage"
|
||||
@@ -530,26 +528,34 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = settings | {"min": 9}
|
||||
await client.send_json(
|
||||
{"id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", "min": 9}
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert float(state.state) == 9
|
||||
|
||||
updated_settings = settings | {"max": 5}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"max": 5,
|
||||
"min": 0,
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert float(state.state) == 5
|
||||
|
||||
@@ -628,13 +628,11 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup):
|
||||
async def test_update(hass, hass_ws_client, storage_setup):
|
||||
"""Test updating options updates the state."""
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": "from_storage",
|
||||
"name": "from storage",
|
||||
"options": ["yaml update 1", "yaml update 2"],
|
||||
}
|
||||
]
|
||||
settings = {
|
||||
"name": "from storage",
|
||||
"options": ["yaml update 1", "yaml update 2"],
|
||||
}
|
||||
items = [{"id": "from_storage"} | settings]
|
||||
assert await storage_setup(items)
|
||||
|
||||
input_id = "from_storage"
|
||||
@@ -647,28 +645,36 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = settings | {
|
||||
"options": ["new option", "newer option"],
|
||||
CONF_INITIAL: "newer option",
|
||||
}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"options": ["new option", "newer option"],
|
||||
CONF_INITIAL: "newer option",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"]
|
||||
|
||||
# Should fail because the initial state is now invalid
|
||||
updated_settings = settings | {
|
||||
"options": ["new option", "no newer option"],
|
||||
CONF_INITIAL: "newer option",
|
||||
}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 7,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"options": ["new option", "no newer option"],
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
@@ -678,13 +684,11 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||
async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog):
|
||||
"""Test updating options updates the state."""
|
||||
|
||||
items = [
|
||||
{
|
||||
"id": "from_storage",
|
||||
"name": "from storage",
|
||||
"options": ["yaml update 1", "yaml update 2"],
|
||||
}
|
||||
]
|
||||
settings = {
|
||||
"name": "from storage",
|
||||
"options": ["yaml update 1", "yaml update 2"],
|
||||
}
|
||||
items = [{"id": "from_storage"} | settings]
|
||||
assert await storage_setup(items)
|
||||
|
||||
input_id = "from_storage"
|
||||
@@ -697,13 +701,16 @@ async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = settings | {
|
||||
"options": ["new option", "newer option", "newer option"],
|
||||
CONF_INITIAL: "newer option",
|
||||
}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
"options": ["new option", "newer option", "newer option"],
|
||||
CONF_INITIAL: "newer option",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
|
||||
@@ -432,19 +432,24 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
updated_settings = {
|
||||
ATTR_NAME: "even newer name",
|
||||
CONF_INITIAL: "newer option",
|
||||
ATTR_MAX: TEST_VAL_MAX,
|
||||
ATTR_MIN: 6,
|
||||
ATTR_MODE: "password",
|
||||
}
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 6,
|
||||
"type": f"{DOMAIN}/update",
|
||||
f"{DOMAIN}_id": f"{input_id}",
|
||||
ATTR_NAME: "even newer name",
|
||||
CONF_INITIAL: "newer option",
|
||||
ATTR_MIN: 6,
|
||||
ATTR_MODE: "password",
|
||||
**updated_settings,
|
||||
}
|
||||
)
|
||||
resp = await client.receive_json()
|
||||
assert resp["success"]
|
||||
assert resp["result"] == {"id": "from_storage"} | updated_settings
|
||||
|
||||
state = hass.states.get(input_entity_id)
|
||||
assert state.state == "loaded from storage"
|
||||
|
||||
@@ -168,7 +168,7 @@ async def test_config_entry_migration(hass):
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ipma.weather.async_get_location",
|
||||
"pyipma.location.Location.get",
|
||||
return_value=MockLocation(),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_WIND_BEARING,
|
||||
ATTR_WEATHER_WIND_SPEED,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
@@ -181,7 +180,8 @@ async def test_setup_config_flow(hass):
|
||||
return_value=MockLocation(),
|
||||
):
|
||||
entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG)
|
||||
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.hometown")
|
||||
@@ -203,7 +203,8 @@ async def test_daily_forecast(hass):
|
||||
return_value=MockLocation(),
|
||||
):
|
||||
entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG)
|
||||
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.hometown")
|
||||
@@ -227,7 +228,8 @@ async def test_hourly_forecast(hass):
|
||||
return_value=MockLocation(),
|
||||
):
|
||||
entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG_HOURLY)
|
||||
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.hometown")
|
||||
@@ -248,7 +250,8 @@ async def test_failed_get_observation_forecast(hass):
|
||||
return_value=MockBadLocation(),
|
||||
):
|
||||
entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG)
|
||||
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("weather.hometown")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user