mirror of
https://github.com/home-assistant/core.git
synced 2026-05-04 20:04:35 +02:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4f1683c40 | |||
| 7be5fde8d6 | |||
| d44ff16f9d | |||
| b51dc0884e | |||
| f3451858ef | |||
| 103f490519 | |||
| 68fa40c0fa | |||
| 5c294550e8 | |||
| 72769130f9 | |||
| 8f21e7775b | |||
| 6704efd1ef | |||
| 829777a211 | |||
| 48c6fbf22e | |||
| fac2a46781 | |||
| a688b4c581 | |||
| 8c9e0a8239 | |||
| 91398b6a75 | |||
| dea221b155 | |||
| bfcb333227 | |||
| 3dd0dbf38f | |||
| 4894e2e5a4 | |||
| e7a616e8e4 | |||
| b3a4838978 | |||
| 933dde1d1e | |||
| 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 | |||
| 0a7f3f6ced | |||
| fee9a303ff | |||
| a4f398a750 | |||
| c873eae79c | |||
| d559b6482a | |||
| 760853f615 | |||
| cfe8ebdad4 | |||
| 2ddd1b516c | |||
| 3b025b211e | |||
| 4009a32fb5 | |||
| 6f3b49601e | |||
| 31858ad779 | |||
| ab9d9d599e | |||
| ce6d337bd5 | |||
| 3fd887b1f2 | |||
| 996a3477b0 | |||
| 910f27f3a2 | |||
| 4ab5cdcb79 | |||
| e69fde6875 | |||
| 10f7e2ff8a | |||
| 3acc3af38c | |||
| a3edbfc601 | |||
| 941a5e3820 | |||
| 2eeab820b7 | |||
| 8d0ebdd1f9 | |||
| 9901b31316 | |||
| a4f528e908 | |||
| 9aa87761cf | |||
| d1b637ea7a |
@@ -23,7 +23,8 @@ env:
|
||||
CACHE_VERSION: 1
|
||||
PIP_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: 2022.9
|
||||
DEFAULT_PYTHON: 3.9
|
||||
DEFAULT_PYTHON: 3.9.14
|
||||
ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']"
|
||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||
PIP_CACHE: /tmp/pip-cache
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
@@ -46,6 +47,7 @@ jobs:
|
||||
pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }}
|
||||
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
|
||||
requirements: ${{ steps.core.outputs.requirements }}
|
||||
python_versions: ${{ steps.info.outputs.python_versions }}
|
||||
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
|
||||
test_group_count: ${{ steps.info.outputs.test_group_count }}
|
||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||
@@ -143,6 +145,8 @@ jobs:
|
||||
fi
|
||||
|
||||
# Output & sent to GitHub Actions
|
||||
echo "python_versions: ${ALL_PYTHON_VERSIONS}"
|
||||
echo "::set-output name=python_versions::${ALL_PYTHON_VERSIONS}"
|
||||
echo "test_full_suite: ${test_full_suite}"
|
||||
echo "::set-output name=test_full_suite::${test_full_suite}"
|
||||
echo "integrations_glob: ${integrations_glob}"
|
||||
@@ -169,7 +173,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
|
||||
@@ -464,7 +467,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.0.2
|
||||
@@ -683,7 +686,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
name: Run pip check ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -730,7 +733,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
python-version: ["3.9", "3.10"]
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
name: >-
|
||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||
steps:
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -58,7 +58,7 @@ class AdapterDetails(TypedDict, total=False):
|
||||
|
||||
address: str
|
||||
sw_version: str
|
||||
hw_version: str
|
||||
hw_version: str | None
|
||||
passive_scan: bool
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker
|
||||
APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE}
|
||||
|
||||
RSSI_SWITCH_THRESHOLD = 6
|
||||
NO_RSSI_VALUE = -1000
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,7 +84,7 @@ def _prefer_previous_adv(
|
||||
STALE_ADVERTISEMENT_SECONDS,
|
||||
)
|
||||
return False
|
||||
if new.device.rssi - RSSI_SWITCH_THRESHOLD > old.device.rssi:
|
||||
if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
|
||||
# If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
|
||||
if new.source != old.source:
|
||||
_LOGGER.debug(
|
||||
@@ -384,11 +385,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.4",
|
||||
"bluetooth-auto-recovery==0.3.1"
|
||||
"bleak==0.17.0",
|
||||
"bleak-retry-connector==1.17.1",
|
||||
"bluetooth-adapters==0.4.1",
|
||||
"bluetooth-auto-recovery==0.3.3",
|
||||
"dbus-fast==1.5.1"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -173,36 +173,40 @@ 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.
|
||||
|
||||
We put them in the bucket that they are most likely to match.
|
||||
"""
|
||||
# Local name is the cheapest to match since its just a dict lookup
|
||||
if LOCAL_NAME in matcher:
|
||||
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 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
|
||||
|
||||
if MANUFACTURER_ID in matcher:
|
||||
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
|
||||
matcher
|
||||
)
|
||||
return
|
||||
return False
|
||||
|
||||
def remove(self, matcher: _T) -> None:
|
||||
def remove(self, matcher: _T) -> bool:
|
||||
"""Remove a matcher from the index.
|
||||
|
||||
Matchers only end up in one bucket, so once we have
|
||||
@@ -212,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
||||
self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove(
|
||||
matcher
|
||||
)
|
||||
return
|
||||
|
||||
if SERVICE_UUID in matcher:
|
||||
self.service_uuid[matcher[SERVICE_UUID]].remove(matcher)
|
||||
return
|
||||
|
||||
if SERVICE_DATA_UUID in matcher:
|
||||
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].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 True
|
||||
|
||||
if SERVICE_DATA_UUID in matcher:
|
||||
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def build(self) -> None:
|
||||
"""Rebuild the index sets."""
|
||||
@@ -235,33 +241,36 @@ class BluetoothMatcherIndexBase(Generic[_T]):
|
||||
def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]:
|
||||
"""Check for a match."""
|
||||
matches = []
|
||||
if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
|
||||
if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
|
||||
for matcher in self.local_name.get(
|
||||
service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], []
|
||||
):
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
|
||||
for service_data_uuid in self.service_data_uuid_set.intersection(
|
||||
service_info.service_data
|
||||
):
|
||||
for matcher in self.service_data_uuid[service_data_uuid]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
if self.service_data_uuid_set and service_info.service_data:
|
||||
for service_data_uuid in self.service_data_uuid_set.intersection(
|
||||
service_info.service_data
|
||||
):
|
||||
for matcher in self.service_data_uuid[service_data_uuid]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
|
||||
for manufacturer_id in self.manufacturer_id_set.intersection(
|
||||
service_info.manufacturer_data
|
||||
):
|
||||
for matcher in self.manufacturer_id[manufacturer_id]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
if self.manufacturer_id_set and service_info.manufacturer_data:
|
||||
for manufacturer_id in self.manufacturer_id_set.intersection(
|
||||
service_info.manufacturer_data
|
||||
):
|
||||
for matcher in self.manufacturer_id[manufacturer_id]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
|
||||
for service_uuid in self.service_uuid_set.intersection(
|
||||
service_info.service_uuids
|
||||
):
|
||||
for matcher in self.service_uuid[service_uuid]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
if self.service_uuid_set and service_info.service_uuids:
|
||||
for service_uuid in self.service_uuid_set.intersection(
|
||||
service_info.service_uuids
|
||||
):
|
||||
for matcher in self.service_uuid[service_uuid]:
|
||||
if ble_device_matches(matcher, service_info):
|
||||
matches.append(matcher)
|
||||
|
||||
return matches
|
||||
|
||||
@@ -279,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.
|
||||
@@ -291,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.
|
||||
@@ -306,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
|
||||
@@ -317,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
|
||||
|
||||
|
||||
@@ -347,12 +372,9 @@ def ble_device_matches(
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
) -> bool:
|
||||
"""Check if a ble device and advertisement_data matches the matcher."""
|
||||
device = service_info.device
|
||||
|
||||
# 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
|
||||
|
||||
@@ -379,7 +401,8 @@ def ble_device_matches(
|
||||
return False
|
||||
|
||||
if (local_name := matcher.get(LOCAL_NAME)) and (
|
||||
(device_name := advertisement_data.local_name or device.name) is None
|
||||
(device_name := advertisement_data.local_name or service_info.device.name)
|
||||
is None
|
||||
or not _memorized_fnmatch(
|
||||
device_name,
|
||||
local_name,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -46,7 +46,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]:
|
||||
adapters[adapter] = AdapterDetails(
|
||||
address=adapter1["Address"],
|
||||
sw_version=adapter1["Name"], # This is actually the BlueZ version
|
||||
hw_version=adapter1["Modalias"],
|
||||
hw_version=adapter1.get("Modalias"),
|
||||
passive_scan="org.bluez.AdvertisementMonitorManager1" in details,
|
||||
)
|
||||
return adapters
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.10.2"],
|
||||
"requirements": ["bimmer_connected==0.10.4"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow for Bond integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
@@ -83,7 +84,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
instead ask them to manually enter the token.
|
||||
"""
|
||||
host = self._discovered[CONF_HOST]
|
||||
if not (token := await async_get_token(self.hass, host)):
|
||||
try:
|
||||
if not (token := await async_get_token(self.hass, host)):
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
return
|
||||
|
||||
self._discovered[CONF_ACCESS_TOKEN] = token
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bt_smarthub",
|
||||
"name": "BT Smart Hub",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bt_smarthub",
|
||||
"requirements": ["btsmarthub_devicelist==0.2.0"],
|
||||
"requirements": ["btsmarthub_devicelist==0.2.2"],
|
||||
"codeowners": ["@jxwolstenholme"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["btsmarthub_devicelist"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
|
||||
class EcobeeSensorEntityDescriptionMixin:
|
||||
"""Represent the required ecobee entity description attributes."""
|
||||
|
||||
runtime_key: str
|
||||
runtime_key: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
runtime_key="actualTemperature",
|
||||
runtime_key=None,
|
||||
),
|
||||
EcobeeSensorEntityDescription(
|
||||
key="humidity",
|
||||
@@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
runtime_key="actualHumidity",
|
||||
runtime_key=None,
|
||||
),
|
||||
EcobeeSensorEntityDescription(
|
||||
key="co2PPM",
|
||||
@@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity):
|
||||
for item in sensor["capability"]:
|
||||
if item["type"] != self.entity_description.key:
|
||||
continue
|
||||
thermostat = self.data.ecobee.get_thermostat(self.index)
|
||||
self._state = thermostat["runtime"][self.entity_description.runtime_key]
|
||||
if self.entity_description.runtime_key is None:
|
||||
self._state = item["value"]
|
||||
else:
|
||||
thermostat = self.data.ecobee.get_thermostat(self.index)
|
||||
self._state = thermostat["runtime"][
|
||||
self.entity_description.runtime_key
|
||||
]
|
||||
break
|
||||
|
||||
@@ -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==20220906.0"],
|
||||
"requirements": ["home-assistant-frontend==20220907.2"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -260,8 +260,6 @@ class BrightnessTrait(_Trait):
|
||||
brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS)
|
||||
if brightness is not None:
|
||||
response["brightness"] = round(100 * (brightness / 255))
|
||||
else:
|
||||
response["brightness"] = 0
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -92,7 +92,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the valve off (closed)."""
|
||||
"""Turn the switch off."""
|
||||
if not self._attr_is_on:
|
||||
return
|
||||
|
||||
try:
|
||||
async with self._client:
|
||||
await self._client.valve.close()
|
||||
@@ -103,7 +106,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the valve on (open)."""
|
||||
"""Turn the switch on."""
|
||||
if self._attr_is_on:
|
||||
return
|
||||
|
||||
try:
|
||||
async with self._client:
|
||||
await self._client.valve.open()
|
||||
|
||||
@@ -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.1"],
|
||||
"requirements": ["aiohomekit==1.5.12"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "imap",
|
||||
"name": "IMAP",
|
||||
"documentation": "https://www.home-assistant.io/integrations/imap",
|
||||
"requirements": ["aioimaplib==1.0.0"],
|
||||
"requirements": ["aioimaplib==1.0.1"],
|
||||
"codeowners": [],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioimaplib"]
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN
|
||||
from .coordinator import LaMetricDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_service(
|
||||
@@ -31,8 +32,10 @@ async def async_get_service(
|
||||
"""Get the LaMetric notification service."""
|
||||
if discovery_info is None:
|
||||
return None
|
||||
lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]]
|
||||
return LaMetricNotificationService(lametric)
|
||||
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
discovery_info["entry_id"]
|
||||
]
|
||||
return LaMetricNotificationService(coordinator.lametric)
|
||||
|
||||
|
||||
class LaMetricNotificationService(BaseNotificationService):
|
||||
|
||||
@@ -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.0"],
|
||||
"requirements": ["led-ble==0.10.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Config flow to configure the LG Soundbar integration."""
|
||||
from queue import Queue
|
||||
from queue import Full, Queue
|
||||
import socket
|
||||
|
||||
import temescal
|
||||
@@ -20,18 +20,29 @@ def test_connect(host, port):
|
||||
uuid_q = Queue(maxsize=1)
|
||||
name_q = Queue(maxsize=1)
|
||||
|
||||
def queue_add(attr_q, data):
|
||||
try:
|
||||
attr_q.put_nowait(data)
|
||||
except Full:
|
||||
pass
|
||||
|
||||
def msg_callback(response):
|
||||
if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]:
|
||||
uuid_q.put_nowait(response["data"]["s_uuid"])
|
||||
if (
|
||||
response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"]
|
||||
and "s_uuid" in response["data"]
|
||||
):
|
||||
queue_add(uuid_q, response["data"]["s_uuid"])
|
||||
if (
|
||||
response["msg"] == "SPK_LIST_VIEW_INFO"
|
||||
and "s_user_name" in response["data"]
|
||||
):
|
||||
name_q.put_nowait(response["data"]["s_user_name"])
|
||||
queue_add(name_q, response["data"]["s_user_name"])
|
||||
|
||||
try:
|
||||
connection = temescal.temescal(host, port=port, callback=msg_callback)
|
||||
connection.get_mac_info()
|
||||
if uuid_q.empty():
|
||||
connection.get_product_info()
|
||||
connection.get_info()
|
||||
details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)}
|
||||
return details
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2022.8.2"],
|
||||
"requirements": ["pylitterbot==2022.9.1"],
|
||||
"codeowners": ["@natekspencer", "@tkdrob"],
|
||||
"dhcp": [{ "hostname": "litter-robot4" }],
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -30,6 +30,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
from homeassistant.helpers.reload import (
|
||||
async_integration_yaml_config,
|
||||
async_reload_integration_platforms,
|
||||
@@ -65,13 +66,7 @@ from .const import ( # noqa: F401
|
||||
CONF_TLS_VERSION,
|
||||
CONF_TOPIC,
|
||||
CONF_WILL_MESSAGE,
|
||||
CONFIG_ENTRY_IS_SETUP,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_RELOAD_ENTRY,
|
||||
DATA_MQTT_RELOAD_NEEDED,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_QOS,
|
||||
DEFAULT_RETAIN,
|
||||
@@ -81,7 +76,7 @@ from .const import ( # noqa: F401
|
||||
PLATFORMS,
|
||||
RELOADABLE_PLATFORMS,
|
||||
)
|
||||
from .mixins import async_discover_yaml_entities
|
||||
from .mixins import MqttData
|
||||
from .models import ( # noqa: F401
|
||||
MqttCommandTemplate,
|
||||
MqttValueTemplate,
|
||||
@@ -169,6 +164,8 @@ async def _async_setup_discovery(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Start the MQTT protocol service."""
|
||||
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
|
||||
conf: ConfigType | None = config.get(DOMAIN)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_subscribe)
|
||||
@@ -177,7 +174,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
if conf:
|
||||
conf = dict(conf)
|
||||
hass.data[DATA_MQTT_CONFIG] = conf
|
||||
mqtt_data.config = conf
|
||||
|
||||
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None:
|
||||
# Create an import flow if the user has yaml configured entities etc.
|
||||
@@ -189,12 +186,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={},
|
||||
)
|
||||
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
|
||||
mqtt_data.reload_needed = True
|
||||
elif mqtt_entry_status is False:
|
||||
_LOGGER.info(
|
||||
"MQTT will be not available until the config entry is enabled",
|
||||
)
|
||||
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
|
||||
mqtt_data.reload_needed = True
|
||||
|
||||
return True
|
||||
|
||||
@@ -252,33 +249,34 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
|
||||
|
||||
Causes for this is config entry options changing.
|
||||
"""
|
||||
mqtt_client = hass.data[DATA_MQTT]
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
assert (client := mqtt_data.client) is not None
|
||||
|
||||
if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None:
|
||||
if (conf := mqtt_data.config) is None:
|
||||
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
|
||||
|
||||
mqtt_client.conf = _merge_extended_config(entry, conf)
|
||||
await mqtt_client.async_disconnect()
|
||||
mqtt_client.init_client()
|
||||
await mqtt_client.async_connect()
|
||||
mqtt_data.config = _merge_extended_config(entry, conf)
|
||||
await client.async_disconnect()
|
||||
client.init_client()
|
||||
await client.async_connect()
|
||||
|
||||
await discovery.async_stop(hass)
|
||||
if mqtt_client.conf.get(CONF_DISCOVERY):
|
||||
await _async_setup_discovery(hass, mqtt_client.conf, entry)
|
||||
if client.conf.get(CONF_DISCOVERY):
|
||||
await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry)
|
||||
|
||||
|
||||
async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None:
|
||||
"""Fetch fresh MQTT yaml config from the hass config when (re)loading the entry."""
|
||||
if DATA_MQTT_RELOAD_ENTRY in hass.data:
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
if mqtt_data.reload_entry:
|
||||
hass_config = await conf_util.async_hass_config_yaml(hass)
|
||||
mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
|
||||
hass.data[DATA_MQTT_CONFIG] = mqtt_config
|
||||
mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
|
||||
|
||||
# Remove unknown keys from config entry data
|
||||
_filter_entry_config(hass, entry)
|
||||
|
||||
# Merge basic configuration, and add missing defaults for basic options
|
||||
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
|
||||
_merge_basic_config(hass, entry, mqtt_data.config or {})
|
||||
# Bail out if broker setting is missing
|
||||
if CONF_BROKER not in entry.data:
|
||||
_LOGGER.error("MQTT broker is not configured, please configure it")
|
||||
@@ -286,7 +284,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict |
|
||||
|
||||
# If user doesn't have configuration.yaml config, generate default values
|
||||
# for options not in config entry data
|
||||
if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None:
|
||||
if (conf := mqtt_data.config) is None:
|
||||
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
|
||||
|
||||
# User has configuration.yaml config, warn about config entry overrides
|
||||
@@ -309,15 +307,20 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict |
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
|
||||
# Merge basic configuration, and add missing defaults for basic options
|
||||
if (conf := await async_fetch_config(hass, entry)) is None:
|
||||
# Bail out
|
||||
return False
|
||||
|
||||
hass.data[DATA_MQTT] = MQTT(hass, entry, conf)
|
||||
mqtt_data.client = MQTT(hass, entry, conf)
|
||||
# Restore saved subscriptions
|
||||
if mqtt_data.subscriptions_to_restore:
|
||||
mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore
|
||||
mqtt_data.subscriptions_to_restore = []
|
||||
entry.add_update_listener(_async_config_entry_updated)
|
||||
|
||||
await hass.data[DATA_MQTT].async_connect()
|
||||
await mqtt_data.client.async_connect()
|
||||
|
||||
async def async_publish_service(call: ServiceCall) -> None:
|
||||
"""Handle MQTT publish service calls."""
|
||||
@@ -366,7 +369,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
return
|
||||
|
||||
await hass.data[DATA_MQTT].async_publish(msg_topic, payload, qos, retain)
|
||||
assert mqtt_data.client is not None and msg_topic is not None
|
||||
await mqtt_data.client.async_publish(msg_topic, payload, qos, retain)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA
|
||||
@@ -407,7 +411,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
# setup platforms and discovery
|
||||
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
|
||||
|
||||
async def async_setup_reload_service() -> None:
|
||||
"""Create the reload service for the MQTT domain."""
|
||||
@@ -420,13 +423,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
|
||||
# Reload the modern yaml platforms
|
||||
mqtt_platforms = async_get_platforms(hass, DOMAIN)
|
||||
tasks = [
|
||||
entity.async_remove()
|
||||
for mqtt_platform in mqtt_platforms
|
||||
for entity in mqtt_platform.entities.values()
|
||||
if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access
|
||||
if mqtt_platform.config_entry
|
||||
and mqtt_platform.domain in RELOADABLE_PLATFORMS
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
|
||||
hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {})
|
||||
mqtt_data.updated_config = config_yaml.get(DOMAIN, {})
|
||||
await asyncio.gather(
|
||||
*(
|
||||
[
|
||||
async_discover_yaml_entities(hass, component)
|
||||
mqtt_data.reload_handlers[component]()
|
||||
for component in RELOADABLE_PLATFORMS
|
||||
if component in mqtt_data.reload_handlers
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -438,6 +453,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 +476,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()
|
||||
if DATA_MQTT_RELOAD_NEEDED in hass.data:
|
||||
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
|
||||
# When the entry is reloaded, also reload manual set up items to enable MQTT
|
||||
if mqtt_data.reload_entry:
|
||||
mqtt_data.reload_entry = False
|
||||
reload_manual_setup = True
|
||||
|
||||
# When the entry was disabled before, reload manual set up items to enable MQTT again
|
||||
if mqtt_data.reload_needed:
|
||||
mqtt_data.reload_needed = False
|
||||
reload_manual_setup = True
|
||||
|
||||
if reload_manual_setup:
|
||||
await async_reload_manual_mqtt_items(hass)
|
||||
|
||||
await async_forward_entry_setup_and_setup_discovery(entry)
|
||||
@@ -568,7 +593,9 @@ def async_subscribe_connection_status(
|
||||
|
||||
def is_connected(hass: HomeAssistant) -> bool:
|
||||
"""Return if MQTT client is connected."""
|
||||
return hass.data[DATA_MQTT].connected
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
assert mqtt_data.client is not None
|
||||
return mqtt_data.client.connected
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
@@ -584,6 +611,10 @@ async def async_remove_config_entry_device(
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload MQTT dump and publish service when the config entry is unloaded."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
assert mqtt_data.client is not None
|
||||
mqtt_client = mqtt_data.client
|
||||
|
||||
# Unload publish and dump services.
|
||||
hass.services.async_remove(
|
||||
DOMAIN,
|
||||
@@ -596,7 +627,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# Stop the discovery
|
||||
await discovery.async_stop(hass)
|
||||
mqtt_client: MQTT = hass.data[DATA_MQTT]
|
||||
# Unload the platforms
|
||||
await asyncio.gather(
|
||||
*(
|
||||
@@ -606,23 +636,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
# Unsubscribe reload dispatchers
|
||||
while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []):
|
||||
while reload_dispatchers := mqtt_data.reload_dispatchers:
|
||||
reload_dispatchers.pop()()
|
||||
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
|
||||
# Cleanup listeners
|
||||
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
|
||||
mqtt_data.reload_needed = True
|
||||
elif mqtt_entry_status is True:
|
||||
# The entry is reloaded:
|
||||
# Trigger re-fetching the yaml config at entry setup
|
||||
hass.data[DATA_MQTT_RELOAD_ENTRY] = True
|
||||
# Stop the loop
|
||||
mqtt_data.reload_entry = True
|
||||
# Reload the legacy yaml platform to make entities unavailable
|
||||
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
|
||||
# Cleanup entity registry hooks
|
||||
registry_hooks = mqtt_data.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:
|
||||
mqtt_data.subscriptions_to_restore = mqtt_client.subscriptions
|
||||
|
||||
return True
|
||||
|
||||
@@ -44,7 +44,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -146,9 +145,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, alarm.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttAvailability,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -102,9 +101,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, binary_sensor.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -25,7 +25,6 @@ from .const import (
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -81,9 +80,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT button through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, button.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -23,7 +23,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -105,9 +104,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, camera.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
||||
from collections.abc import Callable, Coroutine, Iterable
|
||||
from functools import lru_cache, partial, wraps
|
||||
import inspect
|
||||
from itertools import groupby
|
||||
@@ -17,6 +17,7 @@ import attr
|
||||
import certifi
|
||||
from paho.mqtt.client import MQTTMessage
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_PASSWORD,
|
||||
@@ -52,7 +53,6 @@ from .const import (
|
||||
MQTT_DISCONNECTED,
|
||||
PROTOCOL_31,
|
||||
)
|
||||
from .discovery import LAST_DISCOVERY
|
||||
from .models import (
|
||||
AsyncMessageCallbackType,
|
||||
MessageCallbackType,
|
||||
@@ -68,6 +68,9 @@ if TYPE_CHECKING:
|
||||
# because integrations should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .mixins import MqttData
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_COOLDOWN = 2
|
||||
@@ -97,8 +100,12 @@ async def async_publish(
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
) -> None:
|
||||
"""Publish message to a MQTT topic."""
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .mixins import MqttData
|
||||
|
||||
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
|
||||
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
if mqtt_data.client is None or not mqtt_config_entry_enabled(hass):
|
||||
raise HomeAssistantError(
|
||||
f"Cannot publish to topic '{topic}', MQTT is not enabled"
|
||||
)
|
||||
@@ -126,11 +133,13 @@ async def async_publish(
|
||||
)
|
||||
return
|
||||
|
||||
await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain)
|
||||
await mqtt_data.client.async_publish(
|
||||
topic, outgoing_payload, qos or 0, retain or False
|
||||
)
|
||||
|
||||
|
||||
AsyncDeprecatedMessageCallbackType = Callable[
|
||||
[str, ReceivePayloadType, int], Awaitable[None]
|
||||
[str, ReceivePayloadType, int], Coroutine[Any, Any, None]
|
||||
]
|
||||
DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None]
|
||||
|
||||
@@ -175,13 +184,18 @@ async def async_subscribe(
|
||||
| DeprecatedMessageCallbackType
|
||||
| AsyncDeprecatedMessageCallbackType,
|
||||
qos: int = DEFAULT_QOS,
|
||||
encoding: str | None = "utf-8",
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
):
|
||||
"""Subscribe to an MQTT topic.
|
||||
|
||||
Call the return value to unsubscribe.
|
||||
"""
|
||||
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .mixins import MqttData
|
||||
|
||||
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
if mqtt_data.client is None or not mqtt_config_entry_enabled(hass):
|
||||
raise HomeAssistantError(
|
||||
f"Cannot subscribe to topic '{topic}', MQTT is not enabled"
|
||||
)
|
||||
@@ -206,7 +220,7 @@ async def async_subscribe(
|
||||
cast(DeprecatedMessageCallbackType, msg_callback)
|
||||
)
|
||||
|
||||
async_remove = await hass.data[DATA_MQTT].async_subscribe(
|
||||
async_remove = await mqtt_data.client.async_subscribe(
|
||||
topic,
|
||||
catch_log_exception(
|
||||
wrapped_msg_callback,
|
||||
@@ -310,14 +324,16 @@ class MQTT:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry,
|
||||
conf,
|
||||
config_entry: ConfigEntry,
|
||||
conf: ConfigType,
|
||||
) -> None:
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
self._mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.conf = conf
|
||||
@@ -435,12 +451,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 +518,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]]
|
||||
@@ -632,7 +650,6 @@ class MQTT:
|
||||
subscription.job,
|
||||
)
|
||||
continue
|
||||
|
||||
self.hass.async_run_hass_job(
|
||||
subscription.job,
|
||||
ReceiveMessage(
|
||||
@@ -692,10 +709,10 @@ class MQTT:
|
||||
async def _discovery_cooldown(self):
|
||||
now = time.time()
|
||||
# Reset discovery and subscribe cooldowns
|
||||
self.hass.data[LAST_DISCOVERY] = now
|
||||
self._mqtt_data.last_discovery = now
|
||||
self._last_subscribe = now
|
||||
|
||||
last_discovery = self.hass.data[LAST_DISCOVERY]
|
||||
last_discovery = self._mqtt_data.last_discovery
|
||||
last_subscribe = self._last_subscribe
|
||||
wait_until = max(
|
||||
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
|
||||
@@ -703,7 +720,7 @@ class MQTT:
|
||||
while now < wait_until:
|
||||
await asyncio.sleep(wait_until - now)
|
||||
now = time.time()
|
||||
last_discovery = self.hass.data[LAST_DISCOVERY]
|
||||
last_discovery = self._mqtt_data.last_discovery
|
||||
last_subscribe = self._last_subscribe
|
||||
wait_until = max(
|
||||
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
|
||||
|
||||
@@ -50,7 +50,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -350,9 +349,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, climate.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_PROTOCOL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .client import MqttClientSetup
|
||||
@@ -30,12 +30,13 @@ from .const import (
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_WILL_MESSAGE,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT,
|
||||
DEFAULT_BIRTH,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_WILL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .mixins import MqttData
|
||||
from .util import MQTT_WILL_BIRTH_SCHEMA
|
||||
|
||||
MQTT_TIMEOUT = 5
|
||||
@@ -164,9 +165,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the MQTT broker configuration."""
|
||||
mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
errors = {}
|
||||
current_config = self.config_entry.data
|
||||
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
yaml_config = mqtt_data.config or {}
|
||||
if user_input is not None:
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
@@ -214,9 +216,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the MQTT options."""
|
||||
mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
errors = {}
|
||||
current_config = self.config_entry.data
|
||||
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
yaml_config = mqtt_data.config or {}
|
||||
options_config: dict[str, Any] = {}
|
||||
if user_input is not None:
|
||||
bad_birth = False
|
||||
@@ -334,14 +337,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
|
||||
|
||||
def try_connection(hass, broker, port, username, password, protocol="3.1"):
|
||||
def try_connection(
|
||||
hass: HomeAssistant,
|
||||
broker: str,
|
||||
port: int,
|
||||
username: str | None,
|
||||
password: str | None,
|
||||
protocol: str = "3.1",
|
||||
) -> bool:
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
# Get the config from configuration.yaml
|
||||
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
|
||||
yaml_config = mqtt_data.config or {}
|
||||
entry_config = {
|
||||
CONF_BROKER: broker,
|
||||
CONF_PORT: port,
|
||||
@@ -351,7 +362,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"):
|
||||
}
|
||||
client = MqttClientSetup({**yaml_config, **entry_config}).client
|
||||
|
||||
result = queue.Queue(maxsize=1)
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_connect(client_, userdata, flags, result_code):
|
||||
"""Handle connection result."""
|
||||
|
||||
@@ -30,14 +30,8 @@ CONF_CLIENT_CERT = "client_cert"
|
||||
CONF_TLS_INSECURE = "tls_insecure"
|
||||
CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
||||
DATA_MQTT = "mqtt"
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy"
|
||||
DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers"
|
||||
DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry"
|
||||
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
|
||||
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"
|
||||
|
||||
DEFAULT_PREFIX = "homeassistant"
|
||||
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
|
||||
|
||||
@@ -46,7 +46,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -242,9 +241,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, cover.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -33,11 +33,13 @@ from .const import (
|
||||
CONF_PAYLOAD,
|
||||
CONF_QOS,
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .discovery import MQTT_DISCOVERY_DONE
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
MqttData,
|
||||
MqttDiscoveryDeviceUpdate,
|
||||
send_discovery_done,
|
||||
update_device,
|
||||
@@ -81,8 +83,6 @@ TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
DEVICE_TRIGGERS = "mqtt_device_triggers"
|
||||
|
||||
LOG_NAME = "Device trigger"
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
|
||||
self.device_id = device_id
|
||||
self.discovery_data = discovery_data
|
||||
self.hass = hass
|
||||
self._mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
|
||||
MqttDiscoveryDeviceUpdate.__init__(
|
||||
self,
|
||||
@@ -217,8 +218,8 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
|
||||
"""Initialize the device trigger."""
|
||||
discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
|
||||
discovery_id = discovery_hash[1]
|
||||
if discovery_id not in self.hass.data.setdefault(DEVICE_TRIGGERS, {}):
|
||||
self.hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
||||
if discovery_id not in self._mqtt_data.device_triggers:
|
||||
self._mqtt_data.device_triggers[discovery_id] = Trigger(
|
||||
hass=self.hass,
|
||||
device_id=self.device_id,
|
||||
discovery_data=self.discovery_data,
|
||||
@@ -230,7 +231,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
|
||||
value_template=self._config[CONF_VALUE_TEMPLATE],
|
||||
)
|
||||
else:
|
||||
await self.hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
|
||||
await self._mqtt_data.device_triggers[discovery_id].update_trigger(
|
||||
self._config
|
||||
)
|
||||
debug_info.add_trigger_discovery_data(
|
||||
@@ -246,16 +247,16 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
|
||||
)
|
||||
config = TRIGGER_DISCOVERY_SCHEMA(discovery_data)
|
||||
update_device(self.hass, self._config_entry, config)
|
||||
device_trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id]
|
||||
device_trigger: Trigger = self._mqtt_data.device_triggers[discovery_id]
|
||||
await device_trigger.update_trigger(config)
|
||||
|
||||
async def async_tear_down(self) -> None:
|
||||
"""Cleanup device trigger."""
|
||||
discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
|
||||
discovery_id = discovery_hash[1]
|
||||
if discovery_id in self.hass.data[DEVICE_TRIGGERS]:
|
||||
if discovery_id in self._mqtt_data.device_triggers:
|
||||
_LOGGER.info("Removing trigger: %s", discovery_hash)
|
||||
trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id]
|
||||
trigger: Trigger = self._mqtt_data.device_triggers[discovery_id]
|
||||
trigger.detach_trigger()
|
||||
debug_info.remove_trigger_discovery_data(self.hass, discovery_hash)
|
||||
|
||||
@@ -280,11 +281,10 @@ async def async_setup_trigger(
|
||||
|
||||
async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None:
|
||||
"""Handle Mqtt removed from a device."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
triggers = await async_get_triggers(hass, device_id)
|
||||
for trig in triggers:
|
||||
device_trigger: Trigger = hass.data[DEVICE_TRIGGERS].pop(
|
||||
trig[CONF_DISCOVERY_ID]
|
||||
)
|
||||
device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID])
|
||||
if device_trigger:
|
||||
device_trigger.detach_trigger()
|
||||
discovery_data = cast(dict, device_trigger.discovery_data)
|
||||
@@ -296,12 +296,13 @@ async def async_get_triggers(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, str]]:
|
||||
"""List device triggers for MQTT devices."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
triggers: list[dict[str, str]] = []
|
||||
|
||||
if DEVICE_TRIGGERS not in hass.data:
|
||||
if not mqtt_data.device_triggers:
|
||||
return triggers
|
||||
|
||||
for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items():
|
||||
for discovery_id, trig in mqtt_data.device_triggers.items():
|
||||
if trig.device_id != device_id or trig.topic is None:
|
||||
continue
|
||||
|
||||
@@ -324,12 +325,12 @@ async def async_attach_trigger(
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
hass.data.setdefault(DEVICE_TRIGGERS, {})
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
device_id = config[CONF_DEVICE_ID]
|
||||
discovery_id = config[CONF_DISCOVERY_ID]
|
||||
|
||||
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
|
||||
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
||||
if discovery_id not in mqtt_data.device_triggers:
|
||||
mqtt_data.device_triggers[discovery_id] = Trigger(
|
||||
hass=hass,
|
||||
device_id=device_id,
|
||||
discovery_data=None,
|
||||
@@ -340,6 +341,6 @@ async def async_attach_trigger(
|
||||
qos=None,
|
||||
value_template=None,
|
||||
)
|
||||
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
|
||||
return await mqtt_data.device_triggers[discovery_id].add_trigger(
|
||||
action, trigger_info
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ def _async_get_diagnostics(
|
||||
device: DeviceEntry | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
mqtt_instance: MQTT = hass.data[DATA_MQTT]
|
||||
mqtt_instance: MQTT = hass.data[DATA_MQTT].client
|
||||
|
||||
redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import functools
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,9 +29,13 @@ from .const import (
|
||||
ATTR_DISCOVERY_TOPIC,
|
||||
CONF_AVAILABILITY,
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .mixins import MqttData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TOPIC_MATCHER = re.compile(
|
||||
@@ -69,7 +74,6 @@ INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe"
|
||||
MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}"
|
||||
MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}"
|
||||
MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}"
|
||||
LAST_DISCOVERY = "mqtt_last_discovery"
|
||||
|
||||
TOPIC_BASE = "~"
|
||||
|
||||
@@ -80,12 +84,12 @@ class MQTTConfig(dict):
|
||||
discovery_data: dict
|
||||
|
||||
|
||||
def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple) -> None:
|
||||
def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None:
|
||||
"""Clear entry in ALREADY_DISCOVERED list."""
|
||||
del hass.data[ALREADY_DISCOVERED][discovery_hash]
|
||||
|
||||
|
||||
def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple):
|
||||
def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]):
|
||||
"""Clear entry in ALREADY_DISCOVERED list."""
|
||||
hass.data[ALREADY_DISCOVERED][discovery_hash] = {}
|
||||
|
||||
@@ -94,11 +98,12 @@ async def async_start( # noqa: C901
|
||||
hass: HomeAssistant, discovery_topic, config_entry=None
|
||||
) -> None:
|
||||
"""Start MQTT Discovery."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
mqtt_integrations = {}
|
||||
|
||||
async def async_discovery_message_received(msg):
|
||||
"""Process the received message."""
|
||||
hass.data[LAST_DISCOVERY] = time.time()
|
||||
mqtt_data.last_discovery = time.time()
|
||||
payload = msg.payload
|
||||
topic = msg.topic
|
||||
topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1)
|
||||
@@ -253,7 +258,7 @@ async def async_start( # noqa: C901
|
||||
)
|
||||
)
|
||||
|
||||
hass.data[LAST_DISCOVERY] = time.time()
|
||||
mqtt_data.last_discovery = time.time()
|
||||
mqtt_integrations = await async_get_mqtt(hass)
|
||||
|
||||
hass.data[INTEGRATION_UNSUBSCRIBE] = {}
|
||||
|
||||
@@ -50,7 +50,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -241,9 +240,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, fan.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -187,9 +186,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, humidifier.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from ..mixins import (
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -111,9 +110,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT lights configured under the light platform key (deprecated)."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, light.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -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,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -102,9 +101,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, lock.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, Protocol, cast, final
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -28,11 +29,16 @@ 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,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
@@ -48,12 +54,13 @@ 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
|
||||
|
||||
from . import debug_info, subscription
|
||||
from .client import async_publish
|
||||
from .client import MQTT, Subscription, async_publish
|
||||
from .const import (
|
||||
ATTR_DISCOVERY_HASH,
|
||||
ATTR_DISCOVERY_PAYLOAD,
|
||||
@@ -63,9 +70,6 @@ from .const import (
|
||||
CONF_QOS,
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_RELOAD_DISPATCHERS,
|
||||
DATA_MQTT_UPDATED_CONFIG,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_PAYLOAD_AVAILABLE,
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE,
|
||||
@@ -89,6 +93,9 @@ from .subscription import (
|
||||
)
|
||||
from .util import mqtt_config_entry_enabled, valid_subscribe_topic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device_trigger import Trigger
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AVAILABILITY_ALL = "all"
|
||||
@@ -265,6 +272,27 @@ def warn_for_legacy_schema(domain: str) -> Callable:
|
||||
return validator
|
||||
|
||||
|
||||
@dataclass
|
||||
class MqttData:
|
||||
"""Keep the MQTT entry data."""
|
||||
|
||||
client: MQTT | None = None
|
||||
config: ConfigType | None = None
|
||||
device_triggers: dict[str, Trigger] = field(default_factory=dict)
|
||||
discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
last_discovery: float = 0.0
|
||||
reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list)
|
||||
reload_entry: bool = False
|
||||
reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
reload_needed: bool = False
|
||||
subscriptions_to_restore: list[Subscription] = field(default_factory=list)
|
||||
updated_config: ConfigType = field(default_factory=dict)
|
||||
|
||||
|
||||
class SetupEntity(Protocol):
|
||||
"""Protocol type for async_setup_entities."""
|
||||
|
||||
@@ -279,29 +307,6 @@ class SetupEntity(Protocol):
|
||||
"""Define setup_entities type."""
|
||||
|
||||
|
||||
async def async_discover_yaml_entities(
|
||||
hass: HomeAssistant, platform_domain: str
|
||||
) -> None:
|
||||
"""Discover entities for a platform."""
|
||||
if DATA_MQTT_UPDATED_CONFIG in hass.data:
|
||||
# The platform has been reloaded
|
||||
config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG]
|
||||
else:
|
||||
config_yaml = hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
if not config_yaml:
|
||||
return
|
||||
if platform_domain not in config_yaml:
|
||||
return
|
||||
await asyncio.gather(
|
||||
*(
|
||||
discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {})
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, platform_domain, config_yaml
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_get_platform_config_from_yaml(
|
||||
hass: HomeAssistant,
|
||||
platform_domain: str,
|
||||
@@ -309,8 +314,9 @@ async def async_get_platform_config_from_yaml(
|
||||
) -> list[ConfigType]:
|
||||
"""Return a list of validated configurations for the domain."""
|
||||
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
if config_yaml is None:
|
||||
config_yaml = hass.data.get(DATA_MQTT_CONFIG)
|
||||
config_yaml = mqtt_data.config
|
||||
if not config_yaml:
|
||||
return []
|
||||
if not (platform_configs := config_yaml.get(platform_domain)):
|
||||
@@ -322,9 +328,10 @@ async def async_setup_entry_helper(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
async_setup: partial[Coroutine[HomeAssistant, str, None]],
|
||||
schema: vol.Schema,
|
||||
discovery_schema: vol.Schema,
|
||||
) -> None:
|
||||
"""Set up entity, automation or tag creation dynamically through MQTT discovery."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
|
||||
async def async_discover(discovery_payload):
|
||||
"""Discover and add an MQTT entity, automation or tag."""
|
||||
@@ -338,7 +345,7 @@ async def async_setup_entry_helper(
|
||||
return
|
||||
discovery_data = discovery_payload.discovery_data
|
||||
try:
|
||||
config = schema(discovery_payload)
|
||||
config = discovery_schema(discovery_payload)
|
||||
await async_setup(config, discovery_data=discovery_data)
|
||||
except Exception:
|
||||
discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
|
||||
@@ -348,12 +355,37 @@ async def async_setup_entry_helper(
|
||||
)
|
||||
raise
|
||||
|
||||
hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append(
|
||||
mqtt_data.reload_dispatchers.append(
|
||||
async_dispatcher_connect(
|
||||
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
|
||||
)
|
||||
)
|
||||
|
||||
async def _async_setup_entities() -> None:
|
||||
"""Set up MQTT items from configuration.yaml."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
if mqtt_data.updated_config:
|
||||
# The platform has been reloaded
|
||||
config_yaml = mqtt_data.updated_config
|
||||
else:
|
||||
config_yaml = mqtt_data.config or {}
|
||||
if not config_yaml:
|
||||
return
|
||||
if domain not in config_yaml:
|
||||
return
|
||||
await asyncio.gather(
|
||||
*[
|
||||
async_setup(config)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, domain, config_yaml
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# discover manual configured MQTT items
|
||||
mqtt_data.reload_handlers[domain] = _async_setup_entities
|
||||
await _async_setup_entities()
|
||||
|
||||
|
||||
async def async_setup_platform_helper(
|
||||
hass: HomeAssistant,
|
||||
@@ -363,6 +395,13 @@ async def async_setup_platform_helper(
|
||||
async_setup_entities: SetupEntity,
|
||||
) -> None:
|
||||
"""Help to set up the platform for manual configured MQTT entities."""
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
if mqtt_data.reload_entry:
|
||||
_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",
|
||||
@@ -582,7 +621,10 @@ class MqttAvailability(Entity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the device is available."""
|
||||
if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping:
|
||||
mqtt_data: MqttData = self.hass.data[DATA_MQTT]
|
||||
assert mqtt_data.client is not None
|
||||
client = mqtt_data.client
|
||||
if not client.connected and not self.hass.is_stopping:
|
||||
return False
|
||||
if not self._avail_topics:
|
||||
return True
|
||||
@@ -617,7 +659,7 @@ async def cleanup_device_registry(
|
||||
)
|
||||
|
||||
|
||||
def get_discovery_hash(discovery_data: dict) -> tuple:
|
||||
def get_discovery_hash(discovery_data: dict) -> tuple[str, str]:
|
||||
"""Get the discovery hash from the discovery data."""
|
||||
return discovery_data[ATTR_DISCOVERY_HASH]
|
||||
|
||||
@@ -647,6 +689,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 +833,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 +842,13 @@ 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
|
||||
mqtt_data: MqttData = hass.data[DATA_MQTT]
|
||||
self._registry_hooks = mqtt_data.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 +911,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 +922,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[str, str] = 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 +1043,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):
|
||||
|
||||
@@ -44,7 +44,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -138,9 +137,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT number through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, number.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ from .mixins import (
|
||||
CONF_OBJECT_ID,
|
||||
MQTT_AVAILABILITY_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -78,9 +77,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, scene.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -93,9 +92,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT select through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, select.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for MQTT sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import subscription
|
||||
@@ -41,7 +41,6 @@ from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttAvailability,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -146,9 +145,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, sensor.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
@@ -346,7 +342,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@@ -356,7 +352,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
return self._config[CONF_FORCE_UPDATE]
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -142,9 +141,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, siren.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
@@ -101,9 +100,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, switch.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from ..mixins import (
|
||||
async_discover_yaml_entities,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_helper,
|
||||
)
|
||||
from ..mixins import async_setup_entry_helper, async_setup_platform_helper
|
||||
from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE
|
||||
from .schema_legacy import (
|
||||
DISCOVERY_SCHEMA_LEGACY,
|
||||
@@ -90,9 +86,6 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await async_discover_yaml_entities(hass, vacuum.DOMAIN)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -138,8 +138,8 @@ class NetatmoDataHandler:
|
||||
@callback
|
||||
def async_force_update(self, signal_name: str) -> None:
|
||||
"""Prioritize data retrieval for given data class entry."""
|
||||
self.publisher[signal_name].next_scan = time()
|
||||
self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
|
||||
# self.publisher[signal_name].next_scan = time()
|
||||
# self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
|
||||
|
||||
async def handle_event(self, event: dict) -> None:
|
||||
"""Handle webhook events."""
|
||||
|
||||
@@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
|
||||
|
||||
async def _is_owm_api_online(hass, api_key, lat, lon):
|
||||
owm = OWM(api_key).weather_manager()
|
||||
return await hass.async_add_executor_job(owm.one_call, lat, lon)
|
||||
return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon)
|
||||
|
||||
@@ -373,7 +373,7 @@ class Luminary(LightEntity):
|
||||
self._max_mireds = color_util.color_temperature_kelvin_to_mired(
|
||||
self._luminary.min_temp() or DEFAULT_KELVIN
|
||||
)
|
||||
if len(self._attr_supported_color_modes == 1):
|
||||
if len(self._attr_supported_color_modes) == 1:
|
||||
# The light supports only a single color mode
|
||||
self._attr_color_mode = list(self._attr_supported_color_modes)[0]
|
||||
|
||||
@@ -392,7 +392,7 @@ class Luminary(LightEntity):
|
||||
if ColorMode.HS in self._attr_supported_color_modes:
|
||||
self._rgb_color = self._luminary.rgb()
|
||||
|
||||
if len(self._attr_supported_color_modes > 1):
|
||||
if len(self._attr_supported_color_modes) > 1:
|
||||
# The light supports hs + color temp, determine which one it is
|
||||
if self._rgb_color == (0, 0, 0):
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
from functools import partial, wraps
|
||||
from typing import Any
|
||||
|
||||
from regenmaschine import Client
|
||||
from regenmaschine.controller import Controller
|
||||
from regenmaschine.errors import RainMachineError
|
||||
from regenmaschine.errors import RainMachineError, UnknownAPICallError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@@ -22,7 +23,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
@@ -152,9 +153,9 @@ class RainMachineData:
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_controller_for_service_call(
|
||||
def async_get_entry_for_service_call(
|
||||
hass: HomeAssistant, call: ServiceCall
|
||||
) -> Controller:
|
||||
) -> ConfigEntry:
|
||||
"""Get the controller related to a service call (by device ID)."""
|
||||
device_id = call.data[CONF_DEVICE_ID]
|
||||
device_registry = dr.async_get(hass)
|
||||
@@ -166,8 +167,7 @@ def async_get_controller_for_service_call(
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is None:
|
||||
continue
|
||||
if entry.domain == DOMAIN:
|
||||
data: RainMachineData = hass.data[DOMAIN][entry_id]
|
||||
return data.controller
|
||||
return entry
|
||||
|
||||
raise ValueError(f"No controller for device ID: {device_id}")
|
||||
|
||||
@@ -190,7 +190,9 @@ async def async_update_programs_and_zones(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry( # noqa: C901
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up RainMachine as config entry."""
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
client = Client(session=websession)
|
||||
@@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
data = await controller.restrictions.universal()
|
||||
else:
|
||||
data = await controller.zones.all(details=True, include_inactive=True)
|
||||
except UnknownAPICallError:
|
||||
LOGGER.info(
|
||||
"Skipping unsupported API call for controller %s: %s",
|
||||
controller.name,
|
||||
api_category,
|
||||
)
|
||||
except RainMachineError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
@@ -280,15 +288,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
async def async_pause_watering(call: ServiceCall) -> None:
|
||||
"""Pause watering for a set number of seconds."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
await controller.watering.pause_all(call.data[CONF_SECONDS])
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
def call_with_controller(update_programs_and_zones: bool = True) -> Callable:
|
||||
"""Hydrate a service call with the appropriate controller."""
|
||||
|
||||
async def async_push_weather_data(call: ServiceCall) -> None:
|
||||
def decorator(func: Callable) -> Callable[..., Awaitable]:
|
||||
"""Define the decorator."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(call: ServiceCall) -> None:
|
||||
"""Wrap the service function."""
|
||||
entry = async_get_entry_for_service_call(hass, call)
|
||||
data: RainMachineData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
try:
|
||||
await func(call, data.controller)
|
||||
except RainMachineError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Error while executing {func.__name__}: {err}"
|
||||
) from err
|
||||
|
||||
if update_programs_and_zones:
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@call_with_controller()
|
||||
async def async_pause_watering(call: ServiceCall, controller: Controller) -> None:
|
||||
"""Pause watering for a set number of seconds."""
|
||||
await controller.watering.pause_all(call.data[CONF_SECONDS])
|
||||
|
||||
@call_with_controller(update_programs_and_zones=False)
|
||||
async def async_push_weather_data(
|
||||
call: ServiceCall, controller: Controller
|
||||
) -> None:
|
||||
"""Push weather data to the device."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
await controller.parsers.post_data(
|
||||
{
|
||||
CONF_WEATHER: [
|
||||
@@ -301,9 +336,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
}
|
||||
)
|
||||
|
||||
async def async_restrict_watering(call: ServiceCall) -> None:
|
||||
@call_with_controller()
|
||||
async def async_restrict_watering(
|
||||
call: ServiceCall, controller: Controller
|
||||
) -> None:
|
||||
"""Restrict watering for a time period."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
duration = call.data[CONF_DURATION]
|
||||
await controller.restrictions.set_universal(
|
||||
{
|
||||
@@ -311,30 +348,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"rainDelayDuration": duration.total_seconds(),
|
||||
},
|
||||
)
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
async def async_stop_all(call: ServiceCall) -> None:
|
||||
@call_with_controller()
|
||||
async def async_stop_all(call: ServiceCall, controller: Controller) -> None:
|
||||
"""Stop all watering."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
await controller.watering.stop_all()
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
async def async_unpause_watering(call: ServiceCall) -> None:
|
||||
@call_with_controller()
|
||||
async def async_unpause_watering(call: ServiceCall, controller: Controller) -> None:
|
||||
"""Unpause watering."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
await controller.watering.unpause_all()
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
async def async_unrestrict_watering(call: ServiceCall) -> None:
|
||||
@call_with_controller()
|
||||
async def async_unrestrict_watering(
|
||||
call: ServiceCall, controller: Controller
|
||||
) -> None:
|
||||
"""Unrestrict watering."""
|
||||
controller = async_get_controller_for_service_call(hass, call)
|
||||
await controller.restrictions.set_universal(
|
||||
{
|
||||
"rainDelayStartTime": round(as_timestamp(utcnow())),
|
||||
"rainDelayDuration": 0,
|
||||
},
|
||||
)
|
||||
await async_update_programs_and_zones(hass, entry)
|
||||
|
||||
for service_name, schema, method in (
|
||||
(
|
||||
|
||||
@@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self.entity_description.key == TYPE_FLOW_SENSOR:
|
||||
self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor")
|
||||
self._attr_is_on = self.coordinator.data.get("system", {}).get(
|
||||
"useFlowSensor"
|
||||
)
|
||||
|
||||
|
||||
class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==2022.08.0"],
|
||||
"requirements": ["regenmaschine==2022.09.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "local_polling",
|
||||
"homekit": {
|
||||
|
||||
@@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
|
||||
self._attr_native_value = self.coordinator.data["system"].get(
|
||||
self._attr_native_value = self.coordinator.data.get("system", {}).get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
|
||||
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
|
||||
clicks_per_m3 = self.coordinator.data["system"].get(
|
||||
clicks = self.coordinator.data.get("system", {}).get(
|
||||
"flowSensorWateringClicks"
|
||||
)
|
||||
clicks_per_m3 = self.coordinator.data.get("system", {}).get(
|
||||
"flowSensorClicksPerCubicMeter"
|
||||
)
|
||||
|
||||
@@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
|
||||
self._attr_native_value = self.coordinator.data["system"].get(
|
||||
self._attr_native_value = self.coordinator.data.get("system", {}).get(
|
||||
"flowSensorStartIndex"
|
||||
)
|
||||
elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
|
||||
self._attr_native_value = self.coordinator.data["system"].get(
|
||||
self._attr_native_value = self.coordinator.data.get("system", {}).get(
|
||||
"flowSensorWateringClicks"
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -458,7 +458,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
if hostname := urlparse(discovery_info.ssdp_location or "").hostname:
|
||||
self._host = hostname
|
||||
self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER]
|
||||
self._manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER)
|
||||
self._abort_if_manufacturer_is_not_samsung()
|
||||
|
||||
# Set defaults, in case they cannot be extracted from device_info
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user