mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 04:14:32 +02:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6dcaaadd8 | |||
| 73cd4a3366 | |||
| 9ec9dc047b | |||
| a3ad44f7b6 | |||
| da3f9fdf7d | |||
| eeeff761e9 | |||
| 3cd9c7d19d | |||
| 62b44e6903 | |||
| 9c6a69775a | |||
| 8ce1b104eb | |||
| 2916d35626 | |||
| 814f92a75c | |||
| 1ae45fa7ad | |||
| e18d61224d | |||
| 7cfaf46287 | |||
| cebf53c340 | |||
| 8fa8716387 | |||
| 5a96b0ffbc | |||
| 2e13fb3c24 | |||
| 90cb9ccde2 | |||
| 9a91a7edf5 | |||
| c769b20256 | |||
| 47c1e48166 | |||
| 0e6550ab70 | |||
| 5fa2dc540b | |||
| b503be9301 | |||
| d99e04e2bc | |||
| 39a38d9866 | |||
| bc106cc430 | |||
| 2ae390a342 | |||
| 5cc58579bb | |||
| 61129734f5 | |||
| a26e221ebc | |||
| 026c843545 | |||
| f2d41a0011 | |||
| bee853d415 | |||
| 3ba8ddb192 | |||
| 53107e4f2c | |||
| b539700e45 | |||
| dd2c2eb974 | |||
| c1c8299511 | |||
| c72db40082 | |||
| 9327938dcc | |||
| c7f807cf33 | |||
| 691eac7a7f | |||
| e341e55e88 | |||
| 51e8ddbd27 | |||
| c2c397527e | |||
| 328f479bc4 | |||
| 5df7882ce4 | |||
| 7fe6174bd9 | |||
| d4a31b037f | |||
| 5db1c67812 | |||
| f40513c95b | |||
| 2d7cbeb1fc | |||
| 718bc734f2 | |||
| 66c131515a | |||
| 200a24d869 | |||
| c030af0919 | |||
| ad2067381b | |||
| cc9e55594f | |||
| fadb63c43a | |||
| 0cf817698d | |||
| 432768f503 | |||
| 65ccb7446f | |||
| 90bec9cfcd | |||
| ab21ac370c | |||
| 43b9e4febf | |||
| 3659bceedb | |||
| e4395dee6c | |||
| 0b8bba0db9 | |||
| 2cb77c4702 | |||
| dcbca89d06 | |||
| 9614e8496b | |||
| deed4c5980 | |||
| 9c9bacad31 | |||
| daf2f746ed | |||
| d0bb58c698 | |||
| 652ce897c7 | |||
| 5120a03470 | |||
| b349322055 | |||
| 00b363d896 | |||
| a1ebea271c | |||
| c73f423102 | |||
| a630e7dc49 | |||
| b7e3e7a8e6 | |||
| 7eecf48645 | |||
| bb26ded115 | |||
| 8c65e930d5 | |||
| ff332049e1 | |||
| fa12fd3776 | |||
| 0adeebfe33 | |||
| 2328813e69 | |||
| 013f376ef3 | |||
| a8e6ad9f3d | |||
| a8dcecf1ec | |||
| 76db6acfb2 | |||
| 844d0680f0 | |||
| c684603037 | |||
| e6b88a5214 | |||
| a16d86585b | |||
| df308f703f | |||
| 40e7055934 | |||
| 45bd4038f4 | |||
| 3aa0294b5d | |||
| 143e9f4fc3 | |||
| 0de84d882a | |||
| ff786c3be8 | |||
| d617271ca0 | |||
| d38382fbe3 | |||
| 475b9e212d | |||
| a8ad3292c8 | |||
| 78d8bc66d1 |
@@ -1197,6 +1197,7 @@ omit =
|
||||
homeassistant/components/tado/water_heater.py
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/__init__.py
|
||||
homeassistant/components/tankerkoenig/binary_sensor.py
|
||||
homeassistant/components/tankerkoenig/const.py
|
||||
homeassistant/components/tankerkoenig/sensor.py
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
|
||||
@@ -1187,6 +1187,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/yi/ @bachya
|
||||
/homeassistant/components/youless/ @gjong
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/zengge/ @emontnemery
|
||||
/homeassistant/components/zeroconf/ @bdraco
|
||||
/tests/components/zeroconf/ @bdraco
|
||||
/homeassistant/components/zerproc/ @emlove
|
||||
|
||||
@@ -111,9 +111,3 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input)
|
||||
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import config from configuration.yaml."""
|
||||
await self.async_set_unique_id(import_config[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
@@ -16,12 +15,11 @@ from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
@@ -39,28 +37,9 @@ from .const import (
|
||||
REGULAR_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Component setup, run import config flow for each entry in config."""
|
||||
_LOGGER.warning(
|
||||
"Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Google Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"requirements": ["pychromecast==10.3.0"],
|
||||
"requirements": ["pychromecast==11.0.0"],
|
||||
"after_dependencies": [
|
||||
"cloud",
|
||||
"http",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import (
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
ATTR_PRESET_MODES,
|
||||
}
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView,
|
||||
FlowManagerResourceView,
|
||||
)
|
||||
from homeassistant.loader import async_get_config_flows
|
||||
from homeassistant.loader import Integration, async_get_config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
@@ -63,19 +63,33 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
|
||||
integrations = {}
|
||||
type_filter = request.query["type"]
|
||||
|
||||
async def load_integration(
|
||||
hass: HomeAssistant, domain: str
|
||||
) -> Integration | None:
|
||||
"""Load integration."""
|
||||
try:
|
||||
return await loader.async_get_integration(hass, domain)
|
||||
except loader.IntegrationNotFound:
|
||||
return None
|
||||
|
||||
# Fetch all the integrations so we can check their type
|
||||
for integration in await asyncio.gather(
|
||||
*(
|
||||
loader.async_get_integration(hass, domain)
|
||||
load_integration(hass, domain)
|
||||
for domain in {entry.domain for entry in entries}
|
||||
)
|
||||
):
|
||||
integrations[integration.domain] = integration
|
||||
if integration:
|
||||
integrations[integration.domain] = integration
|
||||
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if integrations[entry.domain].integration_type == type_filter
|
||||
if (type_filter != "helper" and entry.domain not in integrations)
|
||||
or (
|
||||
entry.domain in integrations
|
||||
and integrations[entry.domain].integration_type == type_filter
|
||||
)
|
||||
]
|
||||
|
||||
return self.json([entry_json(entry) for entry in entries])
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_platform(
|
||||
unique_id="update_no_install",
|
||||
name="Demo Update No Install",
|
||||
title="Awesomesoft Inc.",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.1",
|
||||
release_summary="Awesome update, fixing everything!",
|
||||
release_url="https://www.example.com/release/1.0.1",
|
||||
@@ -41,14 +41,14 @@ async def async_setup_platform(
|
||||
unique_id="update_2_date",
|
||||
name="Demo No Update",
|
||||
title="AdGuard Home",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.0",
|
||||
),
|
||||
DemoUpdate(
|
||||
unique_id="update_addon",
|
||||
name="Demo add-on",
|
||||
title="AdGuard Home",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.1",
|
||||
release_summary="Awesome update, fixing everything!",
|
||||
release_url="https://www.example.com/release/1.0.1",
|
||||
@@ -57,7 +57,7 @@ async def async_setup_platform(
|
||||
unique_id="update_light_bulb",
|
||||
name="Demo Living Room Bulb Update",
|
||||
title="Philips Lamps Firmware",
|
||||
current_version="1.93.3",
|
||||
installed_version="1.93.3",
|
||||
latest_version="1.94.2",
|
||||
release_summary="Added support for effects",
|
||||
release_url="https://www.example.com/release/1.93.3",
|
||||
@@ -67,7 +67,7 @@ async def async_setup_platform(
|
||||
unique_id="update_support_progress",
|
||||
name="Demo Update with Progress",
|
||||
title="Philips Lamps Firmware",
|
||||
current_version="1.93.3",
|
||||
installed_version="1.93.3",
|
||||
latest_version="1.94.2",
|
||||
support_progress=True,
|
||||
release_summary="Added support for effects",
|
||||
@@ -104,7 +104,7 @@ class DemoUpdate(UpdateEntity):
|
||||
unique_id: str,
|
||||
name: str,
|
||||
title: str | None,
|
||||
current_version: str | None,
|
||||
installed_version: str | None,
|
||||
latest_version: str | None,
|
||||
release_summary: str | None = None,
|
||||
release_url: str | None = None,
|
||||
@@ -114,7 +114,7 @@ class DemoUpdate(UpdateEntity):
|
||||
device_class: UpdateDeviceClass | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Demo select entity."""
|
||||
self._attr_current_version = current_version
|
||||
self._attr_installed_version = installed_version
|
||||
self._attr_device_class = device_class
|
||||
self._attr_latest_version = latest_version
|
||||
self._attr_name = name or DEVICE_DEFAULT_NAME
|
||||
@@ -149,7 +149,7 @@ class DemoUpdate(UpdateEntity):
|
||||
await _fake_install()
|
||||
|
||||
self._attr_in_progress = False
|
||||
self._attr_current_version = (
|
||||
self._attr_installed_version = (
|
||||
version if version is not None else self.latest_version
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -16,10 +16,10 @@ from homeassistant.const import (
|
||||
TIME_SECONDS,
|
||||
)
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
@@ -37,8 +37,9 @@ UNIT_PREFIXES = [
|
||||
{"value": "m", "label": "m (milli)"},
|
||||
{"value": "k", "label": "k (kilo)"},
|
||||
{"value": "M", "label": "M (mega)"},
|
||||
{"value": "G", "label": "T (tera)"},
|
||||
{"value": "T", "label": "P (peta)"},
|
||||
{"value": "G", "label": "G (giga)"},
|
||||
{"value": "T", "label": "T (tera)"},
|
||||
{"value": "P", "label": "P (peta)"},
|
||||
]
|
||||
TIME_UNITS = [
|
||||
{"value": TIME_SECONDS, "label": "Seconds"},
|
||||
@@ -78,16 +79,16 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Derivative."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -250,8 +250,10 @@ class DerivativeSensor(RestoreEntity, SensorEntity):
|
||||
self._state = derivative
|
||||
self.async_write_ha_state()
|
||||
|
||||
async_track_state_change_event(
|
||||
self.hass, self._sensor_source_id, calc_derivative
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, self._sensor_source_id, calc_derivative
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.frame import report
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import IntegrationNotFound, bind_hass
|
||||
from homeassistant.requirements import async_get_integration_with_requirements
|
||||
@@ -88,24 +87,6 @@ TYPES = {
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_device_automations(
|
||||
hass: HomeAssistant,
|
||||
automation_type: DeviceAutomationType | str,
|
||||
device_ids: Iterable[str] | None = None,
|
||||
) -> Mapping[str, Any]:
|
||||
"""Return all the device automations for a type optionally limited to specific device ids."""
|
||||
if isinstance(automation_type, str):
|
||||
report(
|
||||
"uses str for async_get_device_automations automation_type. This is "
|
||||
"deprecated and will stop working in Home Assistant 2022.4, it should be "
|
||||
"updated to use DeviceAutomationType instead",
|
||||
error_if_core=False,
|
||||
)
|
||||
automation_type = DeviceAutomationType[automation_type.upper()]
|
||||
return await _async_get_device_automations(hass, automation_type, device_ids)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up device automation."""
|
||||
websocket_api.async_register_command(hass, websocket_device_automation_list_actions)
|
||||
@@ -156,26 +137,18 @@ async def async_get_device_automation_platform( # noqa: D103
|
||||
|
||||
@overload
|
||||
async def async_get_device_automation_platform( # noqa: D103
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
|
||||
) -> "DeviceAutomationPlatformType":
|
||||
...
|
||||
|
||||
|
||||
async def async_get_device_automation_platform(
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
|
||||
) -> "DeviceAutomationPlatformType":
|
||||
"""Load device automation platform for integration.
|
||||
|
||||
Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
|
||||
"""
|
||||
if isinstance(automation_type, str):
|
||||
report(
|
||||
"uses str for async_get_device_automation_platform automation_type. This "
|
||||
"is deprecated and will stop working in Home Assistant 2022.4, it should "
|
||||
"be updated to use DeviceAutomationType instead",
|
||||
error_if_core=False,
|
||||
)
|
||||
automation_type = DeviceAutomationType[automation_type.upper()]
|
||||
platform_name = automation_type.value.section
|
||||
try:
|
||||
integration = await async_get_integration_with_requirements(hass, domain)
|
||||
@@ -215,10 +188,11 @@ async def _async_get_device_automations_from_domain(
|
||||
)
|
||||
|
||||
|
||||
async def _async_get_device_automations(
|
||||
@bind_hass
|
||||
async def async_get_device_automations(
|
||||
hass: HomeAssistant,
|
||||
automation_type: DeviceAutomationType,
|
||||
device_ids: Iterable[str] | None,
|
||||
device_ids: Iterable[str] | None = None,
|
||||
) -> Mapping[str, list[dict[str, Any]]]:
|
||||
"""List device automations."""
|
||||
device_registry = dr.async_get(hass)
|
||||
@@ -336,7 +310,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg):
|
||||
"""Handle request for device actions."""
|
||||
device_id = msg["device_id"]
|
||||
actions = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.ACTION, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
@@ -355,7 +329,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg):
|
||||
"""Handle request for device conditions."""
|
||||
device_id = msg["device_id"]
|
||||
conditions = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.CONDITION, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
@@ -374,7 +348,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
|
||||
"""Handle request for device triggers."""
|
||||
device_id = msg["device_id"]
|
||||
triggers = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.TRIGGER, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"""The dnsip component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up DNS IP from a config entry."""
|
||||
|
||||
@@ -82,14 +82,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Return Option handler."""
|
||||
return DnsIPOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
|
||||
hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME)
|
||||
self._async_abort_entries_match({CONF_HOSTNAME: hostname})
|
||||
config[CONF_HOSTNAME] = hostname
|
||||
return await self.async_step_user(user_input=config)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -6,20 +6,14 @@ import logging
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_HOSTNAME,
|
||||
@@ -27,10 +21,6 @@ from .const import (
|
||||
CONF_IPV6,
|
||||
CONF_RESOLVER,
|
||||
CONF_RESOLVER_IPV6,
|
||||
DEFAULT_HOSTNAME,
|
||||
DEFAULT_IPV6,
|
||||
DEFAULT_RESOLVER,
|
||||
DEFAULT_RESOLVER_IPV6,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@@ -38,38 +28,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
|
||||
vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string,
|
||||
vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string,
|
||||
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the DNS IP sensor."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the DNS IP platform in YAML is deprecated and will be "
|
||||
"removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "environment_canada",
|
||||
"name": "Environment Canada",
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"requirements": ["env_canada==0.5.20"],
|
||||
"requirements": ["env_canada==0.5.21"],
|
||||
"codeowners": ["@gwww", "@michaeldavie"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -209,13 +209,23 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_aqhi_value(data):
|
||||
if (aqhi := data.current) is not None:
|
||||
return aqhi
|
||||
if data.forecasts and (hourly := data.forecasts.get("hourly")) is not None:
|
||||
if values := list(hourly.values()):
|
||||
return values[0]
|
||||
return None
|
||||
|
||||
|
||||
AQHI_SENSOR = ECSensorEntityDescription(
|
||||
key="aqhi",
|
||||
name="AQHI",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.current,
|
||||
value_fn=_get_aqhi_value,
|
||||
)
|
||||
|
||||
ALERT_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
|
||||
@@ -80,12 +80,16 @@ class ECWeather(CoordinatorEntity, WeatherEntity):
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
if self.ec_data.conditions.get("temperature", {}).get("value"):
|
||||
return float(self.ec_data.conditions["temperature"]["value"])
|
||||
if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get(
|
||||
"temperature"
|
||||
if (
|
||||
temperature := self.ec_data.conditions.get("temperature", {}).get("value")
|
||||
) is not None:
|
||||
return float(temperature)
|
||||
if (
|
||||
self.ec_data.hourly_forecasts
|
||||
and (temperature := self.ec_data.hourly_forecasts[0].get("temperature"))
|
||||
is not None
|
||||
):
|
||||
return float(self.ec_data.hourly_forecasts[0]["temperature"])
|
||||
return float(temperature)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -520,6 +520,7 @@ async def _cleanup_instance(
|
||||
data = domain_data.pop_entry_data(entry)
|
||||
for disconnect_cb in data.disconnect_callbacks:
|
||||
disconnect_cb()
|
||||
data.disconnect_callbacks = []
|
||||
for cleanup_callback in data.cleanup_callbacks:
|
||||
cleanup_callback()
|
||||
await data.client.disconnect()
|
||||
|
||||
@@ -7,18 +7,16 @@ from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ffmpeg
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.ffmpeg import get_ffmpeg_manager
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_INTEGRATION_DISCOVERY,
|
||||
ConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
ATTR_DIRECTION,
|
||||
@@ -27,7 +25,6 @@ from .const import (
|
||||
ATTR_SERIAL,
|
||||
ATTR_SPEED,
|
||||
ATTR_TYPE,
|
||||
CONF_CAMERAS,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
DATA_COORDINATOR,
|
||||
DEFAULT_CAMERA_USERNAME,
|
||||
@@ -47,62 +44,9 @@ from .const import (
|
||||
from .coordinator import EzvizDataUpdateCoordinator
|
||||
from .entity import EzvizEntity
|
||||
|
||||
CAMERA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA},
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: entity_platform.AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up a Ezviz IP Camera from platform config."""
|
||||
_LOGGER.warning(
|
||||
"Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards"
|
||||
)
|
||||
|
||||
# Check if entry config exists and skips import if it does.
|
||||
if hass.config_entries.async_entries(DOMAIN):
|
||||
return
|
||||
|
||||
# Check if importing camera account.
|
||||
if CONF_CAMERAS in config:
|
||||
cameras_conf = config[CONF_CAMERAS]
|
||||
for serial, camera in cameras_conf.items():
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
ATTR_SERIAL: serial,
|
||||
CONF_USERNAME: camera[CONF_USERNAME],
|
||||
CONF_PASSWORD: camera[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Check if importing main ezviz cloud account.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -307,50 +307,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Handle config import from yaml."""
|
||||
_LOGGER.debug("import config: %s", import_config)
|
||||
|
||||
# Check importing camera.
|
||||
if ATTR_SERIAL in import_config:
|
||||
return await self.async_step_import_camera(import_config)
|
||||
|
||||
# Validate and setup of main ezviz cloud account.
|
||||
try:
|
||||
return await self._validate_and_create_auth(import_config)
|
||||
|
||||
except InvalidURL:
|
||||
_LOGGER.error("Error importing Ezviz platform config: invalid host")
|
||||
return self.async_abort(reason="invalid_host")
|
||||
|
||||
except InvalidHost:
|
||||
_LOGGER.error("Error importing Ezviz platform config: cannot connect")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
except (AuthTestResultFailed, PyEzvizError):
|
||||
_LOGGER.error("Error importing Ezviz platform config: invalid auth")
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Error importing ezviz platform config: unexpected exception"
|
||||
)
|
||||
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
async def async_step_import_camera(self, data):
|
||||
"""Create RTSP auth entry per camera in config."""
|
||||
|
||||
await self.async_set_unique_id(data[ATTR_SERIAL])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Create camera with: %s", data)
|
||||
|
||||
cam_serial = data.pop(ATTR_SERIAL)
|
||||
data[CONF_TYPE] = ATTR_TYPE_CAMERA
|
||||
|
||||
return self.async_create_entry(title=cam_serial, data=data)
|
||||
|
||||
|
||||
class EzvizOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Ezviz client options."""
|
||||
|
||||
@@ -5,7 +5,6 @@ MANUFACTURER = "Ezviz"
|
||||
|
||||
# Configuration
|
||||
ATTR_SERIAL = "serial"
|
||||
CONF_CAMERAS = "cameras"
|
||||
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
|
||||
ATTR_HOME = "HOME_MODE"
|
||||
ATTR_AWAY = "AWAY_MODE"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_PRESET_MODES
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {ATTR_PRESET_MODES}
|
||||
@@ -91,6 +91,11 @@ def _cleanup_entity_filter(device: er.RegistryEntry) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _ha_is_stopping(activity: str) -> None:
|
||||
"""Inform that HA is stopping."""
|
||||
_LOGGER.info("Cannot execute %s: HomeAssistant is shutting down", activity)
|
||||
|
||||
|
||||
class ClassSetupMissing(Exception):
|
||||
"""Raised when a Class func is called before setup."""
|
||||
|
||||
@@ -351,6 +356,10 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
def scan_devices(self, now: datetime | None = None) -> None:
|
||||
"""Scan for new devices and return a list of found device ids."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping("scan devices")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
||||
self._update_available, self._latest_firmware = self._update_device_info()
|
||||
|
||||
@@ -603,6 +612,10 @@ class AvmWrapper(FritzBoxTools):
|
||||
) -> dict:
|
||||
"""Return service details."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping(f"{service_name}/{action_name}")
|
||||
return {}
|
||||
|
||||
if f"{service_name}{service_suffix}" not in self.connection.services:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow to configure the FRITZ!Box Tools integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
@@ -129,6 +130,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self.context[CONF_HOST] = self._host
|
||||
|
||||
if ipaddress.ip_address(self._host).is_link_local:
|
||||
return self.async_abort(reason="ignore_ip6_link_local")
|
||||
|
||||
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
|
||||
if uuid.startswith("uuid:"):
|
||||
uuid = uuid[5:]
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"connection_error": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"upnp_not_configured": "Missing UPnP settings on device."
|
||||
},
|
||||
@@ -31,16 +31,6 @@
|
||||
"description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.",
|
||||
"title": "Updating FRITZ!Box Tools - credentials"
|
||||
},
|
||||
"start_config": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.",
|
||||
"title": "Setup FRITZ!Box Tools - mandatory"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.dhcp import DhcpServiceInfo
|
||||
from homeassistant.const import CONF_HOST, CONF_RESOURCE
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -110,10 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, conf: dict) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]})
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle a flow initiated by the DHCP client."""
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
"""Support for Fronius devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_RESOURCE,
|
||||
ELECTRIC_CURRENT_AMPERE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_WATT_HOUR,
|
||||
@@ -29,10 +23,8 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -49,38 +41,8 @@ if TYPE_CHECKING:
|
||||
FroniusStorageUpdateCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh"
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS): object,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Import Fronius configuration from yaml."""
|
||||
_LOGGER.warning(
|
||||
"Loading Fronius via platform setup is deprecated. Please remove it from your yaml configuration"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220330.0"],
|
||||
"requirements": ["home-assistant-frontend==20220401.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -4,12 +4,13 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
from errno import EHOSTUNREACH, EIO
|
||||
from functools import partial
|
||||
import imghdr
|
||||
import io
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import PIL
|
||||
from async_timeout import timeout
|
||||
import av
|
||||
from httpx import HTTPStatusError, RequestError, TimeoutException
|
||||
@@ -57,7 +58,7 @@ DEFAULT_DATA = {
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
SUPPORTED_IMAGE_TYPES = ["png", "jpeg", "svg+xml"]
|
||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"}
|
||||
|
||||
|
||||
def build_schema(
|
||||
@@ -110,11 +111,16 @@ def build_schema(
|
||||
|
||||
def get_image_type(image):
|
||||
"""Get the format of downloaded bytes that could be an image."""
|
||||
fmt = imghdr.what(None, h=image)
|
||||
fmt = None
|
||||
imagefile = io.BytesIO(image)
|
||||
with contextlib.suppress(PIL.UnidentifiedImageError):
|
||||
img = PIL.Image.open(imagefile)
|
||||
fmt = img.format.lower()
|
||||
|
||||
if fmt is None:
|
||||
# if imghdr can't figure it out, could be svg.
|
||||
# if PIL can't figure it out, could be svg.
|
||||
with contextlib.suppress(UnicodeDecodeError):
|
||||
if image.decode("utf-8").startswith("<svg"):
|
||||
if image.decode("utf-8").lstrip().startswith("<svg"):
|
||||
return "svg+xml"
|
||||
return fmt
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "generic",
|
||||
"name": "Generic Camera",
|
||||
"config_flow": true,
|
||||
"requirements": ["av==9.0.0"],
|
||||
"requirements": ["av==9.0.0", "pillow==9.0.1"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"codeowners": ["@davet2001"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -183,9 +183,13 @@ class GoogleCalendarService:
|
||||
"""Get the calendar service with valid credetnails."""
|
||||
await self._session.async_ensure_token_valid()
|
||||
creds = _async_google_creds(self._hass, self._session.token)
|
||||
return google_discovery.build(
|
||||
"calendar", "v3", credentials=creds, cache_discovery=False
|
||||
)
|
||||
|
||||
def _build() -> google_discovery.Resource:
|
||||
return google_discovery.build(
|
||||
"calendar", "v3", credentials=creds, cache_discovery=False
|
||||
)
|
||||
|
||||
return await self._hass.async_add_executor_job(_build)
|
||||
|
||||
async def async_list_calendars(
|
||||
self,
|
||||
|
||||
@@ -454,7 +454,7 @@ class GroupEntity(Entity):
|
||||
self.async_update_group_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
start.async_at_start(self.hass, _update_at_start)
|
||||
self.async_on_remove(start.async_at_start(self.hass, _update_at_start))
|
||||
|
||||
@callback
|
||||
def async_defer_or_update_ha_state(self) -> None:
|
||||
@@ -689,7 +689,7 @@ class Group(Entity):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle addition to Home Assistant."""
|
||||
start.async_at_start(self.hass, self._async_start)
|
||||
self.async_on_remove(start.async_at_start(self.hass, self._async_start))
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Handle removal from Home Assistant."""
|
||||
|
||||
@@ -10,11 +10,11 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
HelperOptionsFlowHandler,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
entity_selector_without_own_entities,
|
||||
)
|
||||
|
||||
@@ -25,11 +25,11 @@ from .const import CONF_HIDE_MEMBERS
|
||||
|
||||
def basic_group_options_schema(
|
||||
domain: str,
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
handler = cast(HelperOptionsFlowHandler, handler)
|
||||
handler = cast(SchemaOptionsFlowHandler, handler)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
|
||||
@@ -58,7 +58,7 @@ def basic_group_config_schema(domain: str) -> vol.Schema:
|
||||
|
||||
|
||||
def binary_sensor_options_schema(
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
@@ -78,7 +78,7 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend(
|
||||
|
||||
def light_switch_options_schema(
|
||||
domain: str,
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
@@ -119,45 +119,45 @@ def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]
|
||||
return _set_group_type
|
||||
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowMenuStep(GROUP_TYPES),
|
||||
"binary_sensor": HelperFlowFormStep(
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowMenuStep(GROUP_TYPES),
|
||||
"binary_sensor": SchemaFlowFormStep(
|
||||
BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor")
|
||||
),
|
||||
"cover": HelperFlowFormStep(
|
||||
"cover": SchemaFlowFormStep(
|
||||
basic_group_config_schema("cover"), set_group_type("cover")
|
||||
),
|
||||
"fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
|
||||
"light": HelperFlowFormStep(
|
||||
"fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
|
||||
"light": SchemaFlowFormStep(
|
||||
basic_group_config_schema("light"), set_group_type("light")
|
||||
),
|
||||
"lock": HelperFlowFormStep(
|
||||
"lock": SchemaFlowFormStep(
|
||||
basic_group_config_schema("lock"), set_group_type("lock")
|
||||
),
|
||||
"media_player": HelperFlowFormStep(
|
||||
"media_player": SchemaFlowFormStep(
|
||||
basic_group_config_schema("media_player"), set_group_type("media_player")
|
||||
),
|
||||
"switch": HelperFlowFormStep(
|
||||
"switch": SchemaFlowFormStep(
|
||||
basic_group_config_schema("switch"), set_group_type("switch")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(None, next_step=choose_options_step),
|
||||
"binary_sensor": HelperFlowFormStep(binary_sensor_options_schema),
|
||||
"cover": HelperFlowFormStep(partial(basic_group_options_schema, "cover")),
|
||||
"fan": HelperFlowFormStep(partial(basic_group_options_schema, "fan")),
|
||||
"light": HelperFlowFormStep(partial(light_switch_options_schema, "light")),
|
||||
"lock": HelperFlowFormStep(partial(basic_group_options_schema, "lock")),
|
||||
"media_player": HelperFlowFormStep(
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(None, next_step=choose_options_step),
|
||||
"binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema),
|
||||
"cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")),
|
||||
"fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")),
|
||||
"light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")),
|
||||
"lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")),
|
||||
"media_player": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "media_player")
|
||||
),
|
||||
"switch": HelperFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
"switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
}
|
||||
|
||||
|
||||
class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for groups."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "hangouts",
|
||||
"name": "Google Hangouts",
|
||||
"name": "Google Chat",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hangouts",
|
||||
"requirements": ["hangups==0.4.17"],
|
||||
"requirements": ["hangups==0.4.18"],
|
||||
"codeowners": [],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hangups", "urwid"]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"authorization_code": "Authorization Code (required for manual authentication)"
|
||||
},
|
||||
"title": "Google Hangouts Login"
|
||||
"title": "Google Chat Login"
|
||||
},
|
||||
"2fa": {
|
||||
"data": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"email": "Email",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Google Hangouts Login"
|
||||
"title": "Google Chat Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ from .auth import async_setup_auth_view
|
||||
from .const import (
|
||||
ATTR_ADDON,
|
||||
ATTR_ADDONS,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_DISCOVERY,
|
||||
ATTR_FOLDERS,
|
||||
@@ -98,6 +99,7 @@ DATA_INFO = "hassio_info"
|
||||
DATA_OS_INFO = "hassio_os_info"
|
||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
@@ -422,6 +424,16 @@ def get_supervisor_info(hass):
|
||||
return hass.data.get(DATA_SUPERVISOR_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_info(hass):
|
||||
"""Return Addons info.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data.get(DATA_ADDONS_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_stats(hass):
|
||||
@@ -607,6 +619,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
changelog = await hassio.get_addon_changelog(slug)
|
||||
return (slug, changelog)
|
||||
|
||||
async def update_addon_info(slug):
|
||||
"""Return the info for an add-on."""
|
||||
info = await hassio.get_addon_info(slug)
|
||||
return (slug, info)
|
||||
|
||||
async def update_info_data(now):
|
||||
"""Update last available supervisor information."""
|
||||
|
||||
@@ -641,6 +658,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
*[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
)
|
||||
hass.data[DATA_ADDONS_INFO] = dict(
|
||||
await asyncio.gather(
|
||||
*[update_addon_info(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
)
|
||||
|
||||
if ADDONS_COORDINATOR in hass.data:
|
||||
await hass.data[ADDONS_COORDINATOR].async_refresh()
|
||||
@@ -845,6 +867,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Update data via library."""
|
||||
new_data = {}
|
||||
supervisor_info = get_supervisor_info(self.hass)
|
||||
addons_info = get_addons_info(self.hass)
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
addons_changelogs = get_addons_changelogs(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
@@ -858,6 +881,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
addon[ATTR_SLUG]: {
|
||||
**addon,
|
||||
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
|
||||
ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get(
|
||||
ATTR_AUTO_UPDATE, False
|
||||
),
|
||||
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
|
||||
ATTR_REPOSITORY: repositories.get(
|
||||
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
||||
|
||||
@@ -39,6 +39,7 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
ATTR_VERSION_LATEST = "version_latest"
|
||||
ATTR_UPDATE_AVAILABLE = "update_available"
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import (
|
||||
async_update_supervisor,
|
||||
)
|
||||
from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
@@ -99,6 +100,11 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
"""Return the add-on data."""
|
||||
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug]
|
||||
|
||||
@property
|
||||
def auto_update(self):
|
||||
"""Return true if auto-update is enabled for the add-on."""
|
||||
return self._addon_data[ATTR_AUTO_UPDATE]
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
"""Return the title of the update."""
|
||||
@@ -110,8 +116,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
return self._addon_data[ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str | None:
|
||||
"""Version currently in use."""
|
||||
def installed_version(self) -> str | None:
|
||||
"""Version installed and in use."""
|
||||
return self._addon_data[ATTR_VERSION]
|
||||
|
||||
@property
|
||||
@@ -133,9 +139,12 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
|
||||
return None
|
||||
|
||||
if f"# {self.latest_version}" in notes and f"# {self.current_version}" in notes:
|
||||
if (
|
||||
f"# {self.latest_version}" in notes
|
||||
and f"# {self.installed_version}" in notes
|
||||
):
|
||||
# Split the release notes to only what is between the versions if we can
|
||||
new_notes = notes.split(f"# {self.current_version}")[0]
|
||||
new_notes = notes.split(f"# {self.installed_version}")[0]
|
||||
if f"# {self.latest_version}" in new_notes:
|
||||
# Make sure the latest version is still there.
|
||||
# This can be False if the order of the release notes are not correct
|
||||
@@ -176,7 +185,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION]
|
||||
|
||||
@@ -210,6 +219,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
"""Update entity to handle updates for the Home Assistant Supervisor."""
|
||||
|
||||
_attr_auto_update = True
|
||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
_attr_title = "Home Assistant Supervisor"
|
||||
|
||||
@@ -219,7 +229,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION]
|
||||
|
||||
@@ -264,7 +274,7 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION]
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity):
|
||||
async def _update_at_start(_):
|
||||
await self.async_update()
|
||||
|
||||
async_at_start(self.hass, _update_at_start)
|
||||
self.async_on_remove(async_at_start(self.hass, _update_at_start))
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
|
||||
@@ -466,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass, domains, include_entity_category=True
|
||||
)
|
||||
# In accessory mode we can only have one
|
||||
default_value = next(
|
||||
iter(
|
||||
@@ -505,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass, domains, include_entity_category=True
|
||||
)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
@@ -559,21 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
ent_reg = entity_registry.async_get(self.hass)
|
||||
excluded_entities = set()
|
||||
for entity_id in all_supported_entities:
|
||||
if ent_reg_ent := ent_reg.async_get(entity_id):
|
||||
if (
|
||||
ent_reg_ent.entity_category is not None
|
||||
or ent_reg_ent.hidden_by is not None
|
||||
):
|
||||
excluded_entities.add(entity_id)
|
||||
# Remove entity category entities since we will exclude them anyways
|
||||
all_supported_entities = {
|
||||
k: v
|
||||
for k, v in all_supported_entities.items()
|
||||
if k not in excluded_entities
|
||||
}
|
||||
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
default_value = [
|
||||
entity_id for entity_id in entities if entity_id in all_supported_entities
|
||||
@@ -652,16 +642,37 @@ async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
|
||||
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
|
||||
|
||||
|
||||
def _exclude_by_entity_registry(
|
||||
ent_reg: entity_registry.EntityRegistry,
|
||||
entity_id: str,
|
||||
include_entity_category: bool,
|
||||
) -> bool:
|
||||
"""Filter out hidden entities and ones with entity category (unless specified)."""
|
||||
return bool(
|
||||
(entry := ent_reg.async_get(entity_id))
|
||||
and (
|
||||
entry.hidden_by is not None
|
||||
or (not include_entity_category or entry.entity_category is not None)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _async_get_matching_entities(
|
||||
hass: HomeAssistant, domains: list[str] | None = None
|
||||
hass: HomeAssistant,
|
||||
domains: list[str] | None = None,
|
||||
include_entity_category: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""Fetch all entities or entities in the given domains."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
return {
|
||||
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
|
||||
for state in sorted(
|
||||
hass.states.async_all(domains and set(domains)),
|
||||
key=lambda item: item.entity_id,
|
||||
)
|
||||
if not _exclude_by_entity_registry(
|
||||
ent_reg, state.entity_id, include_entity_category
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ async def async_setup_devices(bridge: "HueBridge"):
|
||||
dev_reg, entry.entry_id
|
||||
):
|
||||
if device not in known_devices:
|
||||
# handle case where a virtual device was created for a Hue group
|
||||
hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN)
|
||||
if hue_dev_id in api.groups:
|
||||
continue
|
||||
dev_reg.async_remove_device(device.id)
|
||||
|
||||
# add listener for updates on Hue devices controller
|
||||
|
||||
@@ -194,7 +194,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str == EFFECT_NONE:
|
||||
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
|
||||
effect = EffectStatus.NO_EFFECT
|
||||
elif effect_str is not None:
|
||||
# work out if we got a regular effect or timed effect
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_AVAILABLE_MODES, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_AVAILABLE_MODES,
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
"""The iCloud component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .account import IcloudAccount
|
||||
@@ -15,9 +12,6 @@ from .const import (
|
||||
CONF_GPS_ACCURACY_THRESHOLD,
|
||||
CONF_MAX_INTERVAL,
|
||||
CONF_WITH_FAMILY,
|
||||
DEFAULT_GPS_ACCURACY_THRESHOLD,
|
||||
DEFAULT_MAX_INTERVAL,
|
||||
DEFAULT_WITH_FAMILY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
STORAGE_KEY,
|
||||
@@ -69,47 +63,7 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_WITH_FAMILY, default=DEFAULT_WITH_FAMILY): cv.boolean,
|
||||
vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up iCloud from legacy config file."""
|
||||
if (conf := config.get(DOMAIN)) is None:
|
||||
return True
|
||||
|
||||
# Note: need to remember to cleanup device_tracker (remove async_setup_scanner)
|
||||
_LOGGER.warning(
|
||||
"Configuration of the iCloud integration in YAML is deprecated and "
|
||||
"will be removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
|
||||
for account_conf in conf:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -172,10 +172,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self._validate_and_create_entry(user_input, "user")
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Import a config entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_reauth(self, user_input=None):
|
||||
"""Update password for a config entry that can't authenticate."""
|
||||
# Store existing entry data so it can be used later and set unique ID
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_boolean",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Boolean",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_boolean",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_button",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Button",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_button",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_datetime",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Datetime",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_datetime",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_number",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Number",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_number",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_select",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Select",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_select",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_text",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Text",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_text",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -16,10 +16,10 @@ from homeassistant.const import (
|
||||
TIME_SECONDS,
|
||||
)
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
@@ -37,8 +37,8 @@ UNIT_PREFIXES = [
|
||||
{"value": "none", "label": "none"},
|
||||
{"value": "k", "label": "k (kilo)"},
|
||||
{"value": "M", "label": "M (mega)"},
|
||||
{"value": "G", "label": "T (tera)"},
|
||||
{"value": "T", "label": "P (peta)"},
|
||||
{"value": "G", "label": "G (giga)"},
|
||||
{"value": "T", "label": "T (tera)"},
|
||||
]
|
||||
TIME_UNITS = [
|
||||
{"value": TIME_SECONDS, "label": "s (seconds)"},
|
||||
@@ -88,16 +88,16 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Integration."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -253,8 +253,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
self._state = integral
|
||||
self.async_write_ha_state()
|
||||
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._sensor_source_id], calc_integration
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._sensor_source_id], calc_integration
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry, OptionsFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import (
|
||||
@@ -63,6 +63,13 @@ CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure"
|
||||
CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP"
|
||||
CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode"
|
||||
|
||||
_IA_SELECTOR = selector.selector({"text": {}})
|
||||
_IP_SELECTOR = selector.selector({"text": {}})
|
||||
_PORT_SELECTOR = vol.All(
|
||||
selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
|
||||
|
||||
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a KNX config flow."""
|
||||
@@ -164,7 +171,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
**DEFAULT_ENTRY_DATA, # type: ignore[misc]
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[CONF_KNX_INDIVIDUAL_ADDRESS],
|
||||
CONF_KNX_ROUTE_BACK: (
|
||||
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
|
||||
),
|
||||
@@ -202,18 +208,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
port = self._selected_tunnel.port
|
||||
if not self._selected_tunnel.supports_tunnelling_tcp:
|
||||
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP)
|
||||
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods),
|
||||
vol.Required(CONF_HOST, default=ip_address): str,
|
||||
vol.Required(CONF_PORT, default=port): cv.port,
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
||||
): str,
|
||||
vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR,
|
||||
vol.Required(CONF_PORT, default=port): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
|
||||
@@ -245,9 +249,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_SECURE_USER_ID): int,
|
||||
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): str,
|
||||
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): str,
|
||||
vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All(
|
||||
selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector(
|
||||
{"text": {"type": "password"}}
|
||||
),
|
||||
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector(
|
||||
{"text": {"type": "password"}}
|
||||
),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -290,8 +301,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "file_not_found"
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_KNXKEY_FILENAME): str,
|
||||
vol.Required(CONF_KNX_KNXKEY_PASSWORD): str,
|
||||
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
|
||||
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -319,13 +330,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
fields = {
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
||||
): str,
|
||||
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): str,
|
||||
vol.Required(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port,
|
||||
): _IA_SELECTOR,
|
||||
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="routing", data_schema=vol.Schema(fields), errors=errors
|
||||
@@ -370,17 +383,17 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS,
|
||||
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
|
||||
): str,
|
||||
): selector.selector({"text": {}}),
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_GRP,
|
||||
default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP),
|
||||
): str,
|
||||
): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_PORT,
|
||||
default=self.current_config.get(
|
||||
CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT
|
||||
),
|
||||
): cv.port,
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
@@ -394,7 +407,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_LOCAL_IP,
|
||||
default=local_ip,
|
||||
)
|
||||
] = str
|
||||
] = _IP_SELECTOR
|
||||
data_schema[
|
||||
vol.Required(
|
||||
CONF_KNX_STATE_UPDATER,
|
||||
@@ -403,7 +416,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||
),
|
||||
)
|
||||
] = bool
|
||||
] = selector.selector({"boolean": {}})
|
||||
data_schema[
|
||||
vol.Required(
|
||||
CONF_KNX_RATE_LIMIT,
|
||||
@@ -412,7 +425,18 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_DEFAULT_RATE_LIMIT,
|
||||
),
|
||||
)
|
||||
] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT))
|
||||
] = vol.All(
|
||||
selector.selector(
|
||||
{
|
||||
"number": {
|
||||
"min": 1,
|
||||
"max": CONF_MAX_RATE_LIMIT,
|
||||
"mode": "box",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
@@ -444,10 +468,10 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
): vol.In(connection_methods),
|
||||
vol.Required(
|
||||
CONF_HOST, default=self.current_config.get(CONF_HOST)
|
||||
): str,
|
||||
): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
|
||||
): cv.port,
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
),
|
||||
last_step=True,
|
||||
|
||||
@@ -19,39 +19,56 @@
|
||||
"tunneling_type": "KNX Tunneling Type",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"individual_address": "Individual address for the connection",
|
||||
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
|
||||
"local_ip": "Local IP of Home Assistant"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "Port of the KNX/IP tunneling device.",
|
||||
"host": "IP address of the KNX/IP tunneling device.",
|
||||
"local_ip": "Leave blank to use auto-discovery."
|
||||
}
|
||||
},
|
||||
"secure_tunneling": {
|
||||
"description": "Select how you want to configure IP Secure.",
|
||||
"description": "Select how you want to configure KNX/IP Secure.",
|
||||
"menu_options": {
|
||||
"secure_knxkeys": "Configure a knxkeys file containing IP secure information",
|
||||
"secure_manual": "Configure IP secure manually"
|
||||
"secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys",
|
||||
"secure_manual": "Configure IP secure keys manually"
|
||||
}
|
||||
},
|
||||
"secure_knxkeys": {
|
||||
"description": "Please enter the information for your knxkeys file.",
|
||||
"description": "Please enter the information for your `.knxkeys` file.",
|
||||
"data": {
|
||||
"knxkeys_filename": "The full name of your knxkeys file",
|
||||
"knxkeys_password": "The password to decrypt the knxkeys file"
|
||||
"knxkeys_filename": "The filename of your `.knxkeys` file (including extension)",
|
||||
"knxkeys_password": "The password to decrypt the `.knxkeys` file"
|
||||
},
|
||||
"data_description": {
|
||||
"knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`",
|
||||
"knxkeys_password": "This was set when exporting the file from ETS."
|
||||
}
|
||||
},
|
||||
"secure_manual": {
|
||||
"description": "Please enter the IP secure information.",
|
||||
"description": "Please enter your IP secure information.",
|
||||
"data": {
|
||||
"user_id": "User ID",
|
||||
"user_password": "User password",
|
||||
"device_authentication": "Device authentication password"
|
||||
},
|
||||
"data_description": {
|
||||
"user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.",
|
||||
"user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS.",
|
||||
"device_authentication": "This is set in the 'IP' panel of the interface in ETS."
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"description": "Please configure the routing options.",
|
||||
"data": {
|
||||
"individual_address": "Individual address for the routing connection",
|
||||
"multicast_group": "The multicast group used for routing",
|
||||
"multicast_port": "The multicast port used for routing",
|
||||
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
|
||||
"individual_address": "Individual address",
|
||||
"multicast_group": "Multicast group used for routing",
|
||||
"multicast_port": "Multicast port used for routing",
|
||||
"local_ip": "Local IP of Home Assistant"
|
||||
},
|
||||
"data_description": {
|
||||
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
||||
"local_ip": "Leave blank to use auto-discovery."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -71,11 +88,19 @@
|
||||
"data": {
|
||||
"connection_type": "KNX Connection Type",
|
||||
"individual_address": "Default individual address",
|
||||
"multicast_group": "Multicast group used for routing and discovery",
|
||||
"multicast_port": "Multicast port used for routing and discovery",
|
||||
"local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)",
|
||||
"state_updater": "Globally enable reading states from the KNX Bus",
|
||||
"rate_limit": "Maximum outgoing telegrams per second"
|
||||
"multicast_group": "Multicast group",
|
||||
"multicast_port": "Multicast port",
|
||||
"local_ip": "Local IP of Home Assistant",
|
||||
"state_updater": "State updater",
|
||||
"rate_limit": "Rate limit"
|
||||
},
|
||||
"data_description": {
|
||||
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
||||
"multicast_group": "Used for routing and discovery. Default: `224.0.23.12`",
|
||||
"multicast_port": "Used for routing and discovery. Default: `3671`",
|
||||
"local_ip": "Use `0.0.0.0` for auto-discovery.",
|
||||
"state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect.",
|
||||
"rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
@@ -83,6 +108,10 @@
|
||||
"tunneling_type": "KNX Tunneling Type",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"port": "Port of the KNX/IP tunneling device.",
|
||||
"host": "IP address of the KNX/IP tunneling device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,44 +13,61 @@
|
||||
"manual_tunnel": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"individual_address": "Individual address for the connection",
|
||||
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)",
|
||||
"local_ip": "Local IP of Home Assistant",
|
||||
"port": "Port",
|
||||
"tunneling_type": "KNX Tunneling Type"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "IP address of the KNX/IP tunneling device.",
|
||||
"local_ip": "Leave blank to use auto-discovery.",
|
||||
"port": "Port of the KNX/IP tunneling device."
|
||||
},
|
||||
"description": "Please enter the connection information of your tunneling device."
|
||||
},
|
||||
"routing": {
|
||||
"data": {
|
||||
"individual_address": "Individual address for the routing connection",
|
||||
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)",
|
||||
"multicast_group": "The multicast group used for routing",
|
||||
"multicast_port": "The multicast port used for routing"
|
||||
"individual_address": "Individual address",
|
||||
"local_ip": "Local IP of Home Assistant",
|
||||
"multicast_group": "Multicast group used for routing",
|
||||
"multicast_port": "Multicast port used for routing"
|
||||
},
|
||||
"data_description": {
|
||||
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
||||
"local_ip": "Leave blank to use auto-discovery."
|
||||
},
|
||||
"description": "Please configure the routing options."
|
||||
},
|
||||
"secure_knxkeys": {
|
||||
"data": {
|
||||
"knxkeys_filename": "The full name of your knxkeys file",
|
||||
"knxkeys_password": "The password to decrypt the knxkeys file."
|
||||
"knxkeys_filename": "The filename of your `.knxkeys` file (including extension)",
|
||||
"knxkeys_password": "The password to decrypt the `.knxkeys` file"
|
||||
},
|
||||
"description": "Please enter the information for your knxkeys file."
|
||||
},
|
||||
"secure_tunneling": {
|
||||
"description": "Select how you want to configure IP Secure.",
|
||||
"menu_options": {
|
||||
"secure_knxkeys": "Configure a knxkeys file containing IP secure information",
|
||||
"secure_manual": "Configure IP secure manually"
|
||||
}
|
||||
"data_description": {
|
||||
"knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`",
|
||||
"knxkeys_password": "This was set when exporting the file from ETS."
|
||||
},
|
||||
"description": "Please enter the information for your `.knxkeys` file."
|
||||
},
|
||||
"secure_manual": {
|
||||
"description": "Please enter the IP secure information.",
|
||||
"data": {
|
||||
"user_id": "User ID",
|
||||
"user_password": "User password",
|
||||
"device_authentication": "Device authentication password"
|
||||
"device_authentication": "Device authentication password",
|
||||
"user_id": "User ID",
|
||||
"user_password": "User password"
|
||||
},
|
||||
"data_description": {
|
||||
"device_authentication": "This is set in the 'IP' panel of the interface in ETS.",
|
||||
"user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.",
|
||||
"user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS."
|
||||
},
|
||||
"description": "Please enter your IP secure information."
|
||||
},
|
||||
"secure_tunneling": {
|
||||
"description": "Select how you want to configure KNX/IP Secure.",
|
||||
"menu_options": {
|
||||
"secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys",
|
||||
"secure_manual": "Configure IP secure keys manually"
|
||||
}
|
||||
},
|
||||
},
|
||||
"tunnel": {
|
||||
"data": {
|
||||
"gateway": "KNX Tunnel Connection"
|
||||
@@ -71,11 +88,19 @@
|
||||
"data": {
|
||||
"connection_type": "KNX Connection Type",
|
||||
"individual_address": "Default individual address",
|
||||
"local_ip": "Local IP of Home Assistant (use 0.0.0.0 for automatic detection)",
|
||||
"multicast_group": "Multicast group used for routing and discovery",
|
||||
"multicast_port": "Multicast port used for routing and discovery",
|
||||
"rate_limit": "Maximum outgoing telegrams per second",
|
||||
"state_updater": "Globally enable reading states from the KNX Bus"
|
||||
"local_ip": "Local IP of Home Assistant",
|
||||
"multicast_group": "Multicast group",
|
||||
"multicast_port": "Multicast port",
|
||||
"rate_limit": "Rate limit",
|
||||
"state_updater": "State updater"
|
||||
},
|
||||
"data_description": {
|
||||
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
||||
"local_ip": "Use `0.0.0.0` for auto-discovery.",
|
||||
"multicast_group": "Used for routing and discovery. Default: `224.0.23.12`",
|
||||
"multicast_port": "Used for routing and discovery. Default: `3671`",
|
||||
"rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40",
|
||||
"state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect."
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
@@ -83,6 +108,10 @@
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"tunneling_type": "KNX Tunneling Type"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "IP address of the KNX/IP tunneling device.",
|
||||
"port": "Port of the KNX/IP tunneling device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,6 @@ class KodiEntity(MediaPlayerEntity):
|
||||
self._app_properties = {}
|
||||
self._media_position_updated_at = None
|
||||
self._media_position = None
|
||||
self._connect_error = False
|
||||
|
||||
@property
|
||||
def _kodi_is_off(self):
|
||||
@@ -447,8 +446,10 @@ class KodiEntity(MediaPlayerEntity):
|
||||
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
|
||||
if not self._connect_error:
|
||||
self._connect_error = True
|
||||
_LOGGER.error("Unable to connect to Kodi via websocket", exc_info=True)
|
||||
_LOGGER.warning("Unable to connect to Kodi via websocket")
|
||||
await self._clear_connection(False)
|
||||
else:
|
||||
self._connect_error = False
|
||||
|
||||
async def _ping(self):
|
||||
try:
|
||||
@@ -456,8 +457,10 @@ class KodiEntity(MediaPlayerEntity):
|
||||
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
|
||||
if not self._connect_error:
|
||||
self._connect_error = True
|
||||
_LOGGER.error("Unable to ping Kodi via websocket", exc_info=True)
|
||||
_LOGGER.warning("Unable to ping Kodi via websocket")
|
||||
await self._clear_connection()
|
||||
else:
|
||||
self._connect_error = False
|
||||
|
||||
async def _async_connect_websocket_if_disconnected(self, *_):
|
||||
"""Reconnect the websocket if it fails."""
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -27,7 +26,3 @@ class LaunchLibraryFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(title="Launch Library", data=user_input)
|
||||
|
||||
return self.async_show_form(step_id="user")
|
||||
|
||||
async def async_step_import(self, conf: dict[str, Any]) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
return await self.async_step_user(user_input={CONF_NAME: conf[CONF_NAME]})
|
||||
|
||||
@@ -4,25 +4,20 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pylaunches.objects.event import Event
|
||||
from pylaunches.objects.launch import Launch
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
@@ -34,13 +29,6 @@ from .const import DOMAIN
|
||||
|
||||
DEFAULT_NEXT_LAUNCH_NAME = "Next launch"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME, default=DEFAULT_NEXT_LAUNCH_NAME): cv.string}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LaunchLibrarySensorEntityDescriptionMixin:
|
||||
@@ -137,28 +125,6 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Import Launch Library configuration from yaml."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the launch_library platform in YAML is deprecated and will be "
|
||||
"removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import (
|
||||
ATTR_EFFECT_LIST,
|
||||
ATTR_MAX_MIREDS,
|
||||
ATTR_MIN_MIREDS,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_EFFECT_LIST,
|
||||
ATTR_MIN_MIREDS,
|
||||
ATTR_MAX_MIREDS,
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2021.12.0"],
|
||||
"requirements": ["pylitterbot==2022.3.0"],
|
||||
"codeowners": ["@natekspencer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pylitterbot"]
|
||||
|
||||
@@ -70,6 +70,7 @@ from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F40
|
||||
from .const import ( # noqa: F401
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_ENTITY_PICTURE_LOCAL,
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
@@ -936,7 +937,7 @@ class MediaPlayerEntity(Entity):
|
||||
state_attr[attr] = value
|
||||
|
||||
if self.media_image_remotely_accessible:
|
||||
state_attr["entity_picture_local"] = self.media_image_local
|
||||
state_attr[ATTR_ENTITY_PICTURE_LOCAL] = self.media_image_local
|
||||
|
||||
return state_attr
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ CONTENT_AUTH_EXPIRY_TIME = 3600 * 24
|
||||
|
||||
ATTR_APP_ID = "app_id"
|
||||
ATTR_APP_NAME = "app_name"
|
||||
ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local"
|
||||
ATTR_GROUP_MEMBERS = "group_members"
|
||||
ATTR_INPUT_SOURCE = "source"
|
||||
ATTR_INPUT_SOURCE_LIST = "source_list"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import (
|
||||
ATTR_ENTITY_PICTURE_LOCAL,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static and token attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_ENTITY_PICTURE_LOCAL,
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
@@ -79,10 +78,6 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle configuration by yaml file."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_onboarding(self, data=None):
|
||||
"""Handle a flow initialized by onboarding."""
|
||||
# Don't create entry if latitude or longitude isn't set.
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"""Support for Met.no weather service."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_TEMP,
|
||||
@@ -16,13 +13,11 @@ from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_WIND_BEARING,
|
||||
ATTR_WEATHER_WIND_SPEED,
|
||||
PLATFORM_SCHEMA,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ELEVATION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
@@ -35,11 +30,9 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
from homeassistant.util.pressure import convert as convert_pressure
|
||||
@@ -55,8 +48,6 @@ from .const import (
|
||||
FORECAST_MAP,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = (
|
||||
"Weather forecast from met.no, delivered by the Norwegian "
|
||||
"Meteorological Institute."
|
||||
@@ -64,42 +55,6 @@ ATTRIBUTION = (
|
||||
DEFAULT_NAME = "Met.no"
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Inclusive(
|
||||
CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together"
|
||||
): cv.latitude,
|
||||
vol.Inclusive(
|
||||
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
|
||||
): cv.longitude,
|
||||
vol.Optional(CONF_ELEVATION): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Met.no weather platform."""
|
||||
_LOGGER.warning("Loading Met.no via platform config is deprecated")
|
||||
|
||||
# Add defaults.
|
||||
config = {CONF_ELEVATION: hass.config.elevation, **config}
|
||||
|
||||
if config.get(CONF_LATITUDE) is None:
|
||||
config[CONF_TRACK_HOME] = True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
||||
@@ -8,10 +8,10 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
|
||||
@@ -38,16 +38,16 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Min/Max."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -131,12 +131,15 @@ DEFAULT_PROTOCOL = PROTOCOL_311
|
||||
DEFAULT_TLS_PROTOCOL = "auto"
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_WILL_MESSAGE: DEFAULT_WILL,
|
||||
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL,
|
||||
CONF_WILL_MESSAGE: DEFAULT_WILL,
|
||||
}
|
||||
|
||||
MANDATORY_DEFAULT_VALUES = (CONF_PORT,)
|
||||
|
||||
ATTR_TOPIC_TEMPLATE = "topic_template"
|
||||
ATTR_PAYLOAD_TEMPLATE = "payload_template"
|
||||
|
||||
@@ -203,9 +206,7 @@ CONFIG_SCHEMA_BASE = vol.Schema(
|
||||
CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
|
||||
): cv.isfile,
|
||||
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
|
||||
vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any(
|
||||
"auto", "1.0", "1.1", "1.2"
|
||||
),
|
||||
vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
|
||||
cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
|
||||
),
|
||||
@@ -220,6 +221,17 @@ CONFIG_SCHEMA_BASE = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
DEPRECATED_CONFIG_KEYS = [
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TLS_VERSION,
|
||||
CONF_USERNAME,
|
||||
CONF_WILL_MESSAGE,
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
@@ -602,6 +614,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_MQTT_CONFIG] = conf
|
||||
|
||||
if not bool(hass.config_entries.async_entries(DOMAIN)):
|
||||
# Create an import flow if the user has yaml configured entities etc.
|
||||
# but no broker configuration. Note: The intention is not for this to
|
||||
# import broker configuration from YAML because that has been deprecated.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@@ -612,18 +627,53 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _merge_config(entry, conf):
|
||||
"""Merge configuration.yaml config with config entry."""
|
||||
# Base config on default values
|
||||
def _merge_basic_config(
|
||||
hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Merge basic options in configuration.yaml config with config entry.
|
||||
|
||||
This mends incomplete migration from old version of HA Core.
|
||||
"""
|
||||
|
||||
entry_updated = False
|
||||
entry_config = {**entry.data}
|
||||
for key in DEPRECATED_CONFIG_KEYS:
|
||||
if key in yaml_config and key not in entry_config:
|
||||
entry_config[key] = yaml_config[key]
|
||||
entry_updated = True
|
||||
|
||||
for key in MANDATORY_DEFAULT_VALUES:
|
||||
if key not in entry_config:
|
||||
entry_config[key] = DEFAULT_VALUES[key]
|
||||
entry_updated = True
|
||||
|
||||
if entry_updated:
|
||||
hass.config_entries.async_update_entry(entry, data=entry_config)
|
||||
|
||||
|
||||
def _merge_extended_config(entry, conf):
|
||||
"""Merge advanced options in configuration.yaml config with config entry."""
|
||||
# Add default values
|
||||
conf = {**DEFAULT_VALUES, **conf}
|
||||
return {**conf, **entry.data}
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
# If user didn't have configuration.yaml config, generate defaults
|
||||
# Merge basic configuration, and add missing defaults for basic options
|
||||
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
|
||||
|
||||
# Bail out if broker setting is missing
|
||||
if CONF_BROKER not in entry.data:
|
||||
_LOGGER.error("MQTT broker is not configured, please configure it")
|
||||
return False
|
||||
|
||||
# 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:
|
||||
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
|
||||
|
||||
# User has configuration.yaml config, warn about config entry overrides
|
||||
elif any(key in conf for key in entry.data):
|
||||
shared_keys = conf.keys() & entry.data.keys()
|
||||
override = {k: entry.data[k] for k in shared_keys}
|
||||
@@ -635,8 +685,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
override,
|
||||
)
|
||||
|
||||
# Merge the configuration values from configuration.yaml
|
||||
conf = _merge_config(entry, conf)
|
||||
# Merge advanced configuration values from configuration.yaml
|
||||
conf = _merge_extended_config(entry, conf)
|
||||
|
||||
hass.data[DATA_MQTT] = MQTT(
|
||||
hass,
|
||||
@@ -870,7 +920,7 @@ class MQTT:
|
||||
if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None:
|
||||
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
|
||||
|
||||
self.conf = _merge_config(entry, conf)
|
||||
self.conf = _merge_extended_config(entry, conf)
|
||||
await self.async_disconnect()
|
||||
self.init_client()
|
||||
await self.async_connect()
|
||||
|
||||
@@ -814,7 +814,7 @@ class MqttEntity(
|
||||
return self._config[CONF_ENABLED_BY_DEFAULT]
|
||||
|
||||
@property
|
||||
def entity_category(self) -> EntityCategory | str | None:
|
||||
def entity_category(self) -> EntityCategory | None:
|
||||
"""Return the entity category if any."""
|
||||
return self._config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
|
||||
@@ -184,17 +184,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_setup_finish()
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Handle Nanoleaf configuration import."""
|
||||
self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]})
|
||||
_LOGGER.debug(
|
||||
"Importing Nanoleaf on %s from your configuration.yaml", config[CONF_HOST]
|
||||
)
|
||||
self.nanoleaf = Nanoleaf(
|
||||
async_get_clientsession(self.hass), config[CONF_HOST], config[CONF_TOKEN]
|
||||
)
|
||||
return await self.async_setup_finish()
|
||||
|
||||
async def async_setup_finish(
|
||||
self, discovery_integration_import: bool = False
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"""Support for Nanoleaf Lights."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from aionanoleaf import Nanoleaf
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@@ -14,7 +12,6 @@ from homeassistant.components.light import (
|
||||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
PLATFORM_SCHEMA,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
@@ -22,12 +19,9 @@ from homeassistant.components.light import (
|
||||
SUPPORT_TRANSITION,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_kelvin_to_mired as kelvin_to_mired,
|
||||
@@ -41,38 +35,6 @@ from .entity import NanoleafEntity
|
||||
RESERVED_EFFECTS = ("*Solid*", "*Static*", "*Dynamic*")
|
||||
DEFAULT_NAME = "Nanoleaf"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Import Nanoleaf light platform."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the Nanoleaf integration in YAML is deprecated and "
|
||||
"will be removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_HOST: config[CONF_HOST], CONF_TOKEN: config[CONF_TOKEN]},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
|
||||
@@ -108,7 +108,7 @@ class NeatoConnectedSwitch(SwitchEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def entity_category(self) -> str:
|
||||
def entity_category(self) -> EntityCategory:
|
||||
"""Device entity category."""
|
||||
return EntityCategory.CONFIG
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"requirements": ["pynetgear==0.9.1"],
|
||||
"requirements": ["pynetgear==0.9.2"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_MIN,
|
||||
ATTR_MAX,
|
||||
ATTR_STEP,
|
||||
ATTR_MODE,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
from pyoverkiz.enums.ui import UIWidget
|
||||
@@ -22,6 +22,7 @@ from homeassistant.components.alarm_control_panel.const import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
@@ -54,15 +55,15 @@ class OverkizAlarmDescription(
|
||||
"""Class to describe an Overkiz alarm control panel."""
|
||||
|
||||
alarm_disarm: str | None = None
|
||||
alarm_disarm_args: OverkizStateType | list[OverkizStateType] | None = None
|
||||
alarm_disarm_args: OverkizStateType | list[OverkizStateType] = None
|
||||
alarm_arm_home: str | None = None
|
||||
alarm_arm_home_args: OverkizStateType | list[OverkizStateType] | None = None
|
||||
alarm_arm_home_args: OverkizStateType | list[OverkizStateType] = None
|
||||
alarm_arm_night: str | None = None
|
||||
alarm_arm_night_args: OverkizStateType | list[OverkizStateType] | None = None
|
||||
alarm_arm_night_args: OverkizStateType | list[OverkizStateType] = None
|
||||
alarm_arm_away: str | None = None
|
||||
alarm_arm_away_args: OverkizStateType | list[OverkizStateType] | None = None
|
||||
alarm_arm_away_args: OverkizStateType | list[OverkizStateType] = None
|
||||
alarm_trigger: str | None = None
|
||||
alarm_trigger_args: OverkizStateType | list[OverkizStateType] | None = None
|
||||
alarm_trigger_args: OverkizStateType | list[OverkizStateType] = None
|
||||
|
||||
|
||||
MAP_INTERNAL_STATUS_STATE: dict[str, str] = {
|
||||
@@ -91,23 +92,25 @@ def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType])
|
||||
]
|
||||
|
||||
|
||||
MAP_CORE_ACTIVE_ZONES: dict[str, str] = {
|
||||
OverkizCommandParam.A: STATE_ALARM_ARMED_HOME,
|
||||
f"{OverkizCommandParam.A},{OverkizCommandParam.B}": STATE_ALARM_ARMED_NIGHT,
|
||||
f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}": STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
|
||||
def _state_stateful_alarm_controller(
|
||||
select_state: Callable[[str], OverkizStateType]
|
||||
) -> str:
|
||||
"""Return the state of the device."""
|
||||
if state := cast(list, select_state(OverkizState.CORE_ACTIVE_ZONES)):
|
||||
if [
|
||||
OverkizCommandParam.A,
|
||||
OverkizCommandParam.B,
|
||||
OverkizCommandParam.C,
|
||||
] in state:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
if state := cast(str, select_state(OverkizState.CORE_ACTIVE_ZONES)):
|
||||
# The Stateful Alarm Controller has 3 zones with the following options:
|
||||
# (A, B, C, A,B, B,C, A,C, A,B,C). Since it is not possible to map this to AlarmControlPanel entity,
|
||||
# only the most important zones are mapped, other zones can only be disarmed.
|
||||
if state in MAP_CORE_ACTIVE_ZONES:
|
||||
return MAP_CORE_ACTIVE_ZONES[state]
|
||||
|
||||
if [OverkizCommandParam.A, OverkizCommandParam.B] in state:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
|
||||
if OverkizCommandParam.A in state:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
return STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
@@ -181,17 +184,13 @@ ALARM_DESCRIPTIONS: list[OverkizAlarmDescription] = [
|
||||
SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT
|
||||
),
|
||||
fn_state=_state_stateful_alarm_controller,
|
||||
alarm_disarm=OverkizCommand.DISARM,
|
||||
alarm_disarm=OverkizCommand.ALARM_OFF,
|
||||
alarm_arm_home=OverkizCommand.ALARM_ZONE_ON,
|
||||
alarm_arm_home_args=[OverkizCommandParam.A],
|
||||
alarm_arm_home_args=OverkizCommandParam.A,
|
||||
alarm_arm_night=OverkizCommand.ALARM_ZONE_ON,
|
||||
alarm_arm_night_args=[OverkizCommandParam.A, OverkizCommandParam.B],
|
||||
alarm_arm_night_args=f"{OverkizCommandParam.A}, {OverkizCommandParam.B}",
|
||||
alarm_arm_away=OverkizCommand.ALARM_ZONE_ON,
|
||||
alarm_arm_away_args=[
|
||||
OverkizCommandParam.A,
|
||||
OverkizCommandParam.B,
|
||||
OverkizCommandParam.C,
|
||||
],
|
||||
alarm_arm_away_args=f"{OverkizCommandParam.A},{OverkizCommandParam.B},{OverkizCommandParam.C}",
|
||||
),
|
||||
# MyFoxAlarmController
|
||||
OverkizAlarmDescription(
|
||||
@@ -267,7 +266,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
assert self.entity_description.alarm_disarm
|
||||
await self.executor.async_execute_command(
|
||||
await self.async_execute_command(
|
||||
self.entity_description.alarm_disarm,
|
||||
self.entity_description.alarm_disarm_args,
|
||||
)
|
||||
@@ -275,7 +274,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
assert self.entity_description.alarm_arm_home
|
||||
await self.executor.async_execute_command(
|
||||
await self.async_execute_command(
|
||||
self.entity_description.alarm_arm_home,
|
||||
self.entity_description.alarm_arm_home_args,
|
||||
)
|
||||
@@ -283,7 +282,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
assert self.entity_description.alarm_arm_night
|
||||
await self.executor.async_execute_command(
|
||||
await self.async_execute_command(
|
||||
self.entity_description.alarm_arm_night,
|
||||
self.entity_description.alarm_arm_night_args,
|
||||
)
|
||||
@@ -291,7 +290,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
assert self.entity_description.alarm_arm_away
|
||||
await self.executor.async_execute_command(
|
||||
await self.async_execute_command(
|
||||
self.entity_description.alarm_arm_away,
|
||||
self.entity_description.alarm_arm_away_args,
|
||||
)
|
||||
@@ -299,7 +298,14 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
"""Send alarm trigger command."""
|
||||
assert self.entity_description.alarm_trigger
|
||||
await self.executor.async_execute_command(
|
||||
await self.async_execute_command(
|
||||
self.entity_description.alarm_trigger,
|
||||
self.entity_description.alarm_trigger_args,
|
||||
)
|
||||
|
||||
async def async_execute_command(self, command_name: str, args: Any) -> None:
|
||||
"""Execute device command in async context."""
|
||||
if args:
|
||||
await self.executor.async_execute_command(command_name, args)
|
||||
else:
|
||||
await self.executor.async_execute_command(command_name)
|
||||
|
||||
@@ -28,6 +28,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.LOCK,
|
||||
Platform.NUMBER,
|
||||
Platform.SCENE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Overkiz (by Somfy)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/overkiz",
|
||||
"requirements": ["pyoverkiz==1.3.13"],
|
||||
"requirements": ["pyoverkiz==1.3.14"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -3,17 +3,15 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.core import Context, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.template import Template, is_template_string
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import slugify
|
||||
@@ -82,36 +80,11 @@ def async_create(
|
||||
)
|
||||
notification_id = entity_id.split(".")[1]
|
||||
|
||||
warn = False
|
||||
|
||||
attr: dict[str, str] = {}
|
||||
attr: dict[str, str] = {ATTR_MESSAGE: message}
|
||||
if title is not None:
|
||||
if is_template_string(title):
|
||||
warn = True
|
||||
try:
|
||||
title = cast(
|
||||
str, Template(title, hass).async_render(parse_result=False) # type: ignore[no-untyped-call]
|
||||
)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error("Error rendering title %s: %s", title, ex)
|
||||
|
||||
attr[ATTR_TITLE] = title
|
||||
attr[ATTR_FRIENDLY_NAME] = title
|
||||
|
||||
if is_template_string(message):
|
||||
warn = True
|
||||
try:
|
||||
message = Template(message, hass).async_render(parse_result=False) # type: ignore[no-untyped-call]
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error("Error rendering message %s: %s", message, ex)
|
||||
|
||||
attr[ATTR_MESSAGE] = message
|
||||
|
||||
if warn:
|
||||
_LOGGER.warning(
|
||||
"Passing a template string to persistent_notification.async_create function is deprecated"
|
||||
)
|
||||
|
||||
hass.states.async_set(entity_id, STATE, attr, context=context)
|
||||
|
||||
# Store notification and fire event
|
||||
|
||||
@@ -22,7 +22,7 @@ from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN
|
||||
class PiHoleUpdateEntityDescription(UpdateEntityDescription):
|
||||
"""Describes PiHole update entity."""
|
||||
|
||||
current_version: Callable[[dict], str | None] = lambda api: None
|
||||
installed_version: Callable[[dict], str | None] = lambda api: None
|
||||
latest_version: Callable[[dict], str | None] = lambda api: None
|
||||
release_base_url: str | None = None
|
||||
title: str | None = None
|
||||
@@ -34,7 +34,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
name="Core Update Available",
|
||||
title="Pi-hole Core",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
current_version=lambda versions: versions.get("core_current"),
|
||||
installed_version=lambda versions: versions.get("core_current"),
|
||||
latest_version=lambda versions: versions.get("core_latest"),
|
||||
release_base_url="https://github.com/pi-hole/pi-hole/releases/tag",
|
||||
),
|
||||
@@ -43,7 +43,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
name="Web Update Available",
|
||||
title="Pi-hole Web interface",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
current_version=lambda versions: versions.get("web_current"),
|
||||
installed_version=lambda versions: versions.get("web_current"),
|
||||
latest_version=lambda versions: versions.get("web_latest"),
|
||||
release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag",
|
||||
),
|
||||
@@ -52,7 +52,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
name="FTL Update Available",
|
||||
title="Pi-hole FTL DNS",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
current_version=lambda versions: versions.get("FTL_current"),
|
||||
installed_version=lambda versions: versions.get("FTL_current"),
|
||||
latest_version=lambda versions: versions.get("FTL_latest"),
|
||||
release_base_url="https://github.com/pi-hole/FTL/releases/tag",
|
||||
),
|
||||
@@ -100,10 +100,10 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity):
|
||||
self._attr_title = description.title
|
||||
|
||||
@property
|
||||
def current_version(self) -> str | None:
|
||||
"""Version currently in use."""
|
||||
def installed_version(self) -> str | None:
|
||||
"""Version installed and in use."""
|
||||
if isinstance(self.api.versions, dict):
|
||||
return self.entity_description.current_version(self.api.versions)
|
||||
return self.entity_description.installed_version(self.api.versions)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
@@ -64,7 +63,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
self._attr_preset_modes = list(presets)
|
||||
|
||||
# Determine hvac modes and current hvac mode
|
||||
self._attr_hvac_modes = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
self._attr_hvac_modes = [HVAC_MODE_HEAT]
|
||||
if self.coordinator.data.gateway.get("cooling_present"):
|
||||
self._attr_hvac_modes.append(HVAC_MODE_COOL)
|
||||
if self.device.get("available_schedules") != ["None"]:
|
||||
@@ -90,7 +89,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return HVAC operation ie. heat, cool mode."""
|
||||
if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes:
|
||||
return HVAC_MODE_OFF
|
||||
return HVAC_MODE_HEAT
|
||||
return mode
|
||||
|
||||
@property
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "plugwise",
|
||||
"name": "Plugwise",
|
||||
"documentation": "https://www.home-assistant.io/integrations/plugwise",
|
||||
"requirements": ["plugwise==0.17.2"],
|
||||
"requirements": ["plugwise==0.17.3"],
|
||||
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
|
||||
"zeroconf": ["_plugwise._tcp.local."],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -4,6 +4,7 @@ from functools import partial
|
||||
import json
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
|
||||
DATA_INSTANCE = "recorder_instance"
|
||||
@@ -25,3 +26,5 @@ MAX_ROWS_TO_PURGE = 998
|
||||
DB_WORKER_PREFIX = "DbWorker"
|
||||
|
||||
JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":"))
|
||||
|
||||
ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES}
|
||||
|
||||
@@ -38,7 +38,7 @@ from homeassistant.core import Context, Event, EventOrigin, State, split_entity_
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import JSON_DUMP
|
||||
from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP
|
||||
|
||||
# SQLAlchemy Schema
|
||||
# pylint: disable=invalid-name
|
||||
@@ -269,11 +269,12 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
|
||||
if state is None:
|
||||
return "{}"
|
||||
domain = split_entity_id(state.entity_id)[0]
|
||||
if exclude_attrs := exclude_attrs_by_domain.get(domain):
|
||||
return JSON_DUMP(
|
||||
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
|
||||
)
|
||||
return JSON_DUMP(state.attributes)
|
||||
exclude_attrs = (
|
||||
exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS
|
||||
)
|
||||
return JSON_DUMP(
|
||||
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def hash_shared_attrs(shared_attrs: str) -> int:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "roku",
|
||||
"name": "Roku",
|
||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||
"requirements": ["rokuecp==0.15.0"],
|
||||
"requirements": ["rokuecp==0.16.0"],
|
||||
"homekit": {
|
||||
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_OPTIONS
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {ATTR_OPTIONS}
|
||||
@@ -3,10 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||
ClimateEntity,
|
||||
)
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
@@ -18,36 +15,26 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_STATE,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_API_KEY,
|
||||
CONF_ID,
|
||||
PRECISION_TENTHS,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import ALL, DOMAIN, LOGGER
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SensiboDataUpdateCoordinator
|
||||
from .entity import SensiboDeviceBaseEntity
|
||||
|
||||
SERVICE_ASSUME_STATE = "assume_state"
|
||||
|
||||
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
)
|
||||
|
||||
FIELD_TO_FLAG = {
|
||||
"fanLevel": SUPPORT_FAN_MODE,
|
||||
"swing": SUPPORT_SWING_MODE,
|
||||
@@ -74,25 +61,6 @@ AC_STATE_TO_DATA = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up Sensibo devices."""
|
||||
LOGGER.warning(
|
||||
"Loading Sensibo via platform setup is deprecated; Please remove it from your configuration"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
|
||||
@@ -75,11 +75,6 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, config: dict) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
|
||||
return await self.async_step_user(user_input=config)
|
||||
|
||||
async def async_step_user(self, user_input=None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ PLATFORMS = [
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
ALL = ["all"]
|
||||
DEFAULT_NAME = "Sensibo"
|
||||
TIMEOUT = 8
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_AVAILABLE_TONES
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {ATTR_AVAILABLE_TONES}
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "SleepIQ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
|
||||
"requirements": ["asyncsleepiq==1.2.1"],
|
||||
"requirements": ["asyncsleepiq==1.2.3"],
|
||||
"codeowners": ["@mfugate1", "@kbickar"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for SleepIQ foundation preset selection."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncsleepiq import BED_PRESETS, SleepIQBed, SleepIQPreset
|
||||
from asyncsleepiq import BED_PRESETS, Side, SleepIQBed, SleepIQPreset
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -39,14 +39,11 @@ class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity):
|
||||
"""Initialize the select entity."""
|
||||
self.preset = preset
|
||||
|
||||
if preset.side:
|
||||
self._attr_name = (
|
||||
f"SleepNumber {bed.name} Foundation Preset {preset.side_full}"
|
||||
)
|
||||
self._attr_unique_id = f"{bed.id}_preset_{preset.side}"
|
||||
else:
|
||||
self._attr_name = f"SleepNumber {bed.name} Foundation Preset"
|
||||
self._attr_unique_id = f"{bed.id}_preset"
|
||||
self._attr_name = f"SleepNumber {bed.name} Foundation Preset"
|
||||
self._attr_unique_id = f"{bed.id}_preset"
|
||||
if preset.side != Side.NONE:
|
||||
self._attr_name += f" {preset.side_full}"
|
||||
self._attr_unique_id += f"_{preset.side}"
|
||||
|
||||
super().__init__(coordinator, bed)
|
||||
self._async_update_attrs()
|
||||
|
||||
@@ -63,14 +63,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Handle import of solax config from YAML."""
|
||||
|
||||
import_data = {
|
||||
CONF_IP_ADDRESS: config[CONF_IP_ADDRESS],
|
||||
CONF_PORT: config[CONF_PORT],
|
||||
CONF_PASSWORD: DEFAULT_PASSWORD,
|
||||
}
|
||||
|
||||
return await self.async_step_user(user_input=import_data)
|
||||
|
||||
@@ -3,38 +3,25 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from solax.inverter import InverterError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
},
|
||||
)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
@@ -81,30 +68,6 @@ async def async_setup_entry(
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Platform setup."""
|
||||
|
||||
_LOGGER.warning(
|
||||
"Configuration of the SolaX Power platform in YAML is deprecated and "
|
||||
"will be removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RealTimeDataEndpoint:
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ from typing import Any, Literal, cast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import StateAttributes, States
|
||||
from homeassistant.components.recorder.util import execute, session_scope
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
@@ -304,7 +302,7 @@ class StatisticsSensor(SensorEntity):
|
||||
if "recorder" in self.hass.config.components:
|
||||
self.hass.async_create_task(self._initialize_from_database())
|
||||
|
||||
async_at_start(self.hass, async_stats_sensor_startup)
|
||||
self.async_on_remove(async_at_start(self.hass, async_stats_sensor_startup))
|
||||
|
||||
def _add_state_to_queue(self, new_state: State) -> None:
|
||||
"""Add the state to the queue."""
|
||||
@@ -474,37 +472,29 @@ class StatisticsSensor(SensorEntity):
|
||||
def _fetch_states_from_database(self) -> list[State]:
|
||||
"""Fetch the states from the database."""
|
||||
_LOGGER.debug("%s: initializing values from the database", self.entity_id)
|
||||
states = []
|
||||
|
||||
with session_scope(hass=self.hass) as session:
|
||||
query = session.query(States, StateAttributes).filter(
|
||||
States.entity_id == self._source_entity_id.lower()
|
||||
lower_entity_id = self._source_entity_id.lower()
|
||||
if self._samples_max_age is not None:
|
||||
start_date = (
|
||||
dt_util.utcnow() - self._samples_max_age - timedelta(microseconds=1)
|
||||
)
|
||||
|
||||
if self._samples_max_age is not None:
|
||||
records_older_then = dt_util.utcnow() - self._samples_max_age
|
||||
_LOGGER.debug(
|
||||
"%s: retrieve records not older then %s",
|
||||
self.entity_id,
|
||||
records_older_then,
|
||||
)
|
||||
query = query.filter(States.last_updated >= records_older_then)
|
||||
else:
|
||||
_LOGGER.debug("%s: retrieving all records", self.entity_id)
|
||||
|
||||
query = query.outerjoin(
|
||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||
_LOGGER.debug(
|
||||
"%s: retrieve records not older then %s",
|
||||
self.entity_id,
|
||||
start_date,
|
||||
)
|
||||
query = query.order_by(States.last_updated.desc()).limit(
|
||||
self._samples_max_buffer_size
|
||||
)
|
||||
if results := execute(query, to_native=False, validate_entity_ids=False):
|
||||
for state, attributes in results:
|
||||
native = state.to_native()
|
||||
if not native.attributes:
|
||||
native.attributes = attributes.to_native()
|
||||
states.append(native)
|
||||
return states
|
||||
else:
|
||||
start_date = datetime.fromtimestamp(0, tz=dt_util.UTC)
|
||||
_LOGGER.debug("%s: retrieving all records", self.entity_id)
|
||||
entity_states = history.state_changes_during_period(
|
||||
self.hass,
|
||||
start_date,
|
||||
entity_id=lower_entity_id,
|
||||
descending=True,
|
||||
limit=self._samples_max_buffer_size,
|
||||
include_start_time_state=False,
|
||||
)
|
||||
# Need to cast since minimal responses is not passed in
|
||||
return cast(list[State], entity_states.get(lower_entity_id, []))
|
||||
|
||||
async def _initialize_from_database(self) -> None:
|
||||
"""Initialize the list of states from the database.
|
||||
|
||||
@@ -8,17 +8,17 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_ID, Platform
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
wrapped_entity_config_entry_title,
|
||||
)
|
||||
|
||||
from .const import CONF_TARGET_DOMAIN, DOMAIN
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.selector(
|
||||
@@ -43,7 +43,7 @@ CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
}
|
||||
|
||||
|
||||
class SwitchAsXConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for Switch as X."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -131,19 +131,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Handle config import from yaml."""
|
||||
_LOGGER.debug("import config: %s", import_config)
|
||||
|
||||
import_config[CONF_MAC] = import_config[CONF_MAC].replace("-", ":").lower()
|
||||
|
||||
await self.async_set_unique_id(import_config[CONF_MAC].replace(":", ""))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=import_config[CONF_NAME], data=import_config
|
||||
)
|
||||
|
||||
|
||||
class SwitchbotOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Switchbot options."""
|
||||
|
||||
@@ -5,27 +5,15 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from switchbot import Switchbot # pylint: disable=import-error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
PLATFORM_SCHEMA,
|
||||
SwitchDeviceClass,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SENSOR_TYPE,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import ATTR_BOT, CONF_RETRY_COUNT, DATA_COORDINATOR, DEFAULT_NAME, DOMAIN
|
||||
from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN
|
||||
from .coordinator import SwitchbotDataUpdateCoordinator
|
||||
from .entity import SwitchbotEntity
|
||||
|
||||
@@ -33,46 +21,6 @@ from .entity import SwitchbotEntity
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: entity_platform.AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Import yaml config and initiates config flow for Switchbot devices."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the Switchbot switch platform in YAML is deprecated and "
|
||||
"will be removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
|
||||
# Check if entry config exists and skips import if it does.
|
||||
if hass.config_entries.async_entries(DOMAIN):
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_NAME: config[CONF_NAME],
|
||||
CONF_PASSWORD: config.get(CONF_PASSWORD, None),
|
||||
CONF_MAC: config[CONF_MAC].replace("-", ":").lower(),
|
||||
CONF_SENSOR_TYPE: ATTR_BOT,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -55,15 +55,15 @@ class SynoDSMUpdateEntity(SynologyDSMBaseEntity, UpdateEntity):
|
||||
_attr_title = "Synology DSM"
|
||||
|
||||
@property
|
||||
def current_version(self) -> str | None:
|
||||
"""Version currently in use."""
|
||||
def installed_version(self) -> str | None:
|
||||
"""Version installed and in use."""
|
||||
return self._api.information.version_string # type: ignore[no-any-return]
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str | None:
|
||||
"""Latest version available for install."""
|
||||
if not self._api.upgrade.update_available:
|
||||
return self.current_version
|
||||
return self.installed_version
|
||||
return self._api.upgrade.available_version # type: ignore[no-any-return]
|
||||
|
||||
@property
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user