mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
Merge pull request #63412 from home-assistant/rc
This commit is contained in:
commit
cf84ba1da1
@ -248,8 +248,10 @@ class ConfiguredDoorBird:
|
|||||||
if self.custom_url is not None:
|
if self.custom_url is not None:
|
||||||
hass_url = self.custom_url
|
hass_url = self.custom_url
|
||||||
|
|
||||||
|
favorites = self.device.favorites()
|
||||||
|
|
||||||
for event in self.doorstation_events:
|
for event in self.doorstation_events:
|
||||||
self._register_event(hass_url, event)
|
self._register_event(hass_url, event, favs=favorites)
|
||||||
|
|
||||||
_LOGGER.info("Successfully registered URL for %s on %s", event, self.name)
|
_LOGGER.info("Successfully registered URL for %s on %s", event, self.name)
|
||||||
|
|
||||||
@ -261,15 +263,15 @@ class ConfiguredDoorBird:
|
|||||||
def _get_event_name(self, event):
|
def _get_event_name(self, event):
|
||||||
return f"{self.slug}_{event}"
|
return f"{self.slug}_{event}"
|
||||||
|
|
||||||
def _register_event(self, hass_url, event):
|
def _register_event(self, hass_url, event, favs=None):
|
||||||
"""Add a schedule entry in the device for a sensor."""
|
"""Add a schedule entry in the device for a sensor."""
|
||||||
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
|
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
|
||||||
|
|
||||||
# Register HA URL as webhook if not already, then get the ID
|
# Register HA URL as webhook if not already, then get the ID
|
||||||
if not self.webhook_is_registered(url):
|
if not self.webhook_is_registered(url, favs=favs):
|
||||||
self.device.change_favorite("http", f"Home Assistant ({event})", url)
|
self.device.change_favorite("http", f"Home Assistant ({event})", url)
|
||||||
|
|
||||||
if not self.get_webhook_id(url):
|
if not self.get_webhook_id(url, favs=favs):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"',
|
'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"',
|
||||||
url,
|
url,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Flux LED/MagicHome",
|
"name": "Flux LED/MagicHome",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||||
"requirements": ["flux_led==0.27.21"],
|
"requirements": ["flux_led==0.27.32"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"codeowners": ["@icemanch"],
|
"codeowners": ["@icemanch"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -160,7 +160,10 @@ class FroniusSolarNet:
|
|||||||
)
|
)
|
||||||
if self.logger_coordinator:
|
if self.logger_coordinator:
|
||||||
_logger_info = self.logger_coordinator.data[SOLAR_NET_ID_SYSTEM]
|
_logger_info = self.logger_coordinator.data[SOLAR_NET_ID_SYSTEM]
|
||||||
solar_net_device[ATTR_MODEL] = _logger_info["product_type"]["value"]
|
# API v0 doesn't provide product_type
|
||||||
|
solar_net_device[ATTR_MODEL] = _logger_info.get("product_type", {}).get(
|
||||||
|
"value", "Datalogger Web"
|
||||||
|
)
|
||||||
solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][
|
solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][
|
||||||
"value"
|
"value"
|
||||||
]
|
]
|
||||||
|
@ -2296,8 +2296,8 @@ class SensorStateTrait(_Trait):
|
|||||||
|
|
||||||
sensor_types = {
|
sensor_types = {
|
||||||
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
||||||
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
||||||
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
||||||
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
|
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
|
||||||
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
||||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Gree Climate",
|
"name": "Gree Climate",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||||
"requirements": ["greeclimate==0.12.5"],
|
"requirements": ["greeclimate==1.0.1"],
|
||||||
"codeowners": ["@cmroche"],
|
"codeowners": ["@cmroche"],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
}
|
}
|
||||||
|
@ -619,7 +619,9 @@ class GTFSDepartureSensor(SensorEntity):
|
|||||||
if not self._departure:
|
if not self._departure:
|
||||||
self._state = None
|
self._state = None
|
||||||
else:
|
else:
|
||||||
self._state = self._departure["departure_time"]
|
self._state = self._departure["departure_time"].replace(
|
||||||
|
tzinfo=dt_util.UTC
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch trip and route details once, unless updated
|
# Fetch trip and route details once, unless updated
|
||||||
if not self._departure:
|
if not self._departure:
|
||||||
|
@ -134,7 +134,7 @@ class HassIOIngress(HomeAssistantView):
|
|||||||
if (
|
if (
|
||||||
hdrs.CONTENT_LENGTH in result.headers
|
hdrs.CONTENT_LENGTH in result.headers
|
||||||
and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000
|
and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000
|
||||||
):
|
) or result.status in (204, 304):
|
||||||
# Return Response
|
# Return Response
|
||||||
body = await result.read()
|
body = await result.read()
|
||||||
return web.Response(
|
return web.Response(
|
||||||
|
@ -17,13 +17,15 @@ from homeassistant.components import ssdp, zeroconf
|
|||||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client, device_registry
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_HUE_GROUPS,
|
CONF_ALLOW_HUE_GROUPS,
|
||||||
CONF_ALLOW_UNREACHABLE,
|
CONF_ALLOW_UNREACHABLE,
|
||||||
CONF_API_VERSION,
|
CONF_API_VERSION,
|
||||||
|
CONF_IGNORE_AVAILABILITY,
|
||||||
DEFAULT_ALLOW_HUE_GROUPS,
|
DEFAULT_ALLOW_HUE_GROUPS,
|
||||||
DEFAULT_ALLOW_UNREACHABLE,
|
DEFAULT_ALLOW_UNREACHABLE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: config_entries.ConfigEntry,
|
config_entry: config_entries.ConfigEntry,
|
||||||
) -> HueOptionsFlowHandler:
|
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return HueOptionsFlowHandler(config_entry)
|
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
|
||||||
|
return HueV1OptionsFlowHandler(config_entry)
|
||||||
@classmethod
|
return HueV2OptionsFlowHandler(config_entry)
|
||||||
@callback
|
|
||||||
def async_supports_options_flow(
|
|
||||||
cls, config_entry: config_entries.ConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Return options flow support for this handler."""
|
|
||||||
return config_entry.data.get(CONF_API_VERSION, 1) == 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the Hue flow."""
|
"""Initialize the Hue flow."""
|
||||||
@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_link()
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
|
||||||
class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
class HueV1OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle Hue options."""
|
"""Handle Hue options for V1 implementation."""
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialize Hue options flow."""
|
"""Initialize Hue options flow."""
|
||||||
@ -319,3 +315,47 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HueV2OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle Hue options for V2 implementation."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
"""Initialize Hue options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult:
|
||||||
|
"""Manage Hue options."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
# create a list of Hue device ID's that the user can select
|
||||||
|
# to ignore availability status
|
||||||
|
dev_reg = device_registry.async_get(self.hass)
|
||||||
|
entries = device_registry.async_entries_for_config_entry(
|
||||||
|
dev_reg, self.config_entry.entry_id
|
||||||
|
)
|
||||||
|
dev_ids = {
|
||||||
|
identifier[1]: entry.name
|
||||||
|
for entry in entries
|
||||||
|
for identifier in entry.identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
}
|
||||||
|
# filter any non existing device id's from the list
|
||||||
|
cur_ids = [
|
||||||
|
item
|
||||||
|
for item in self.config_entry.options.get(CONF_IGNORE_AVAILABILITY, [])
|
||||||
|
if item in dev_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_IGNORE_AVAILABILITY,
|
||||||
|
default=cur_ids,
|
||||||
|
): cv.multi_select(dev_ids),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
DOMAIN = "hue"
|
DOMAIN = "hue"
|
||||||
|
|
||||||
CONF_API_VERSION = "api_version"
|
CONF_API_VERSION = "api_version"
|
||||||
|
CONF_IGNORE_AVAILABILITY = "ignore_availability"
|
||||||
|
|
||||||
CONF_SUBTYPE = "subtype"
|
CONF_SUBTYPE = "subtype"
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"allow_hue_groups": "Allow Hue groups",
|
"allow_hue_groups": "Allow Hue groups",
|
||||||
"allow_hue_scenes": "Allow Hue scenes",
|
"allow_hue_scenes": "Allow Hue scenes",
|
||||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
|
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
|
||||||
|
"ignore_availability": "Ignore connectivity status for the given devices"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"allow_hue_groups": "Allow Hue groups",
|
"allow_hue_groups": "Allow Hue groups",
|
||||||
"allow_hue_scenes": "Allow Hue scenes",
|
"allow_hue_scenes": "Allow Hue scenes",
|
||||||
"allow_unreachable": "Allow unreachable bulbs to report their state correctly"
|
"allow_unreachable": "Allow unreachable bulbs to report their state correctly",
|
||||||
|
"ignore_availability": "Ignore connectivity status for the given devices"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"allow_hue_groups": "Sta Hue-groepen toe",
|
"allow_hue_groups": "Sta Hue-groepen toe",
|
||||||
"allow_hue_scenes": "Sta Hue sc\u00e8nes toe",
|
"allow_hue_scenes": "Sta Hue sc\u00e8nes toe",
|
||||||
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden"
|
"allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden",
|
||||||
|
"ignore_availability": "Negeer beschikbaarheid status voor deze apparaten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
|
|||||||
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||||
|
|
||||||
from ..bridge import HueBridge
|
from ..bridge import HueBridge
|
||||||
from ..const import DOMAIN
|
from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN
|
||||||
|
|
||||||
RESOURCE_TYPE_NAMES = {
|
RESOURCE_TYPE_NAMES = {
|
||||||
# a simple mapping of hue resource type to Hass name
|
# a simple mapping of hue resource type to Hass name
|
||||||
@ -71,7 +71,7 @@ class HueBaseEntity(Entity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added."""
|
"""Call when entity is added."""
|
||||||
self._check_availability_workaround()
|
self._check_availability()
|
||||||
# Add value_changed callbacks.
|
# Add value_changed callbacks.
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self.controller.subscribe(
|
self.controller.subscribe(
|
||||||
@ -80,7 +80,7 @@ class HueBaseEntity(Entity):
|
|||||||
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
|
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# also subscribe to device update event to catch devicer changes (e.g. name)
|
# also subscribe to device update event to catch device changes (e.g. name)
|
||||||
if self.device is None:
|
if self.device is None:
|
||||||
return
|
return
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
@ -92,25 +92,27 @@ class HueBaseEntity(Entity):
|
|||||||
)
|
)
|
||||||
# subscribe to zigbee_connectivity to catch availability changes
|
# subscribe to zigbee_connectivity to catch availability changes
|
||||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
self.async_on_remove(
|
||||||
self._handle_event,
|
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||||
zigbee.id,
|
self._handle_event,
|
||||||
EventType.RESOURCE_UPDATED,
|
zigbee.id,
|
||||||
|
EventType.RESOURCE_UPDATED,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return entity availability."""
|
"""Return entity availability."""
|
||||||
|
# entities without a device attached should be always available
|
||||||
if self.device is None:
|
if self.device is None:
|
||||||
# entities without a device attached should be always available
|
|
||||||
return True
|
return True
|
||||||
|
# the zigbee connectivity sensor itself should be always available
|
||||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||||
# the zigbee connectivity sensor itself should be always available
|
|
||||||
return True
|
return True
|
||||||
if self._ignore_availability:
|
if self._ignore_availability:
|
||||||
return True
|
return True
|
||||||
|
# all device-attached entities get availability from the zigbee connectivity
|
||||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||||
# all device-attached entities get availability from the zigbee connectivity
|
|
||||||
return zigbee.status == ConnectivityServiceStatus.CONNECTED
|
return zigbee.status == ConnectivityServiceStatus.CONNECTED
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -130,30 +132,41 @@ class HueBaseEntity(Entity):
|
|||||||
ent_reg.async_remove(self.entity_id)
|
ent_reg.async_remove(self.entity_id)
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Received status update for %s", self.entity_id)
|
self.logger.debug("Received status update for %s", self.entity_id)
|
||||||
self._check_availability_workaround()
|
self._check_availability()
|
||||||
self.on_update()
|
self.on_update()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _check_availability_workaround(self):
|
def _check_availability(self):
|
||||||
"""Check availability of the device."""
|
"""Check availability of the device."""
|
||||||
if self.resource.type != ResourceTypes.LIGHT:
|
# return if we already processed this entity
|
||||||
return
|
|
||||||
if self._ignore_availability is not None:
|
if self._ignore_availability is not None:
|
||||||
# already processed
|
|
||||||
return
|
return
|
||||||
|
# only do the availability check for entities connected to a device
|
||||||
|
if self.device is None:
|
||||||
|
return
|
||||||
|
# ignore availability if user added device to ignore list
|
||||||
|
if self.device.id in self.bridge.config_entry.options.get(
|
||||||
|
CONF_IGNORE_AVAILABILITY, []
|
||||||
|
):
|
||||||
|
self._ignore_availability = True
|
||||||
|
self.logger.info(
|
||||||
|
"Device %s is configured to ignore availability status. ",
|
||||||
|
self.name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# certified products (normally) report their state correctly
|
||||||
|
# no need for workaround/reporting
|
||||||
if self.device.product_data.certified:
|
if self.device.product_data.certified:
|
||||||
# certified products report their state correctly
|
|
||||||
self._ignore_availability = False
|
self._ignore_availability = False
|
||||||
return
|
return
|
||||||
# some (3th party) Hue lights report their connection status incorrectly
|
# some (3th party) Hue lights report their connection status incorrectly
|
||||||
# causing the zigbee availability to report as disconnected while in fact
|
# causing the zigbee availability to report as disconnected while in fact
|
||||||
# it can be controlled. Although this is in fact something the device manufacturer
|
# it can be controlled. If the light is reported unavailable
|
||||||
# should fix, we work around it here. If the light is reported unavailable
|
|
||||||
# by the zigbee connectivity but the state changes its considered as a
|
# by the zigbee connectivity but the state changes its considered as a
|
||||||
# malfunctioning device and we report it.
|
# malfunctioning device and we report it.
|
||||||
# while the user should actually fix this issue instead of ignoring it, we
|
# While the user should actually fix this issue, we allow to
|
||||||
# ignore the availability for this light from this point.
|
# ignore the availability for this light/device from the config options.
|
||||||
cur_state = self.resource.on.on
|
cur_state = self.resource.on.on
|
||||||
if self._last_state is None:
|
if self._last_state is None:
|
||||||
self._last_state = cur_state
|
self._last_state = cur_state
|
||||||
@ -166,9 +179,10 @@ class HueBaseEntity(Entity):
|
|||||||
# the device state changed from on->off or off->on
|
# the device state changed from on->off or off->on
|
||||||
# while it was reported as not connected!
|
# while it was reported as not connected!
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Light %s changed state while reported as disconnected. "
|
"Device %s changed state while reported as disconnected. "
|
||||||
"This might be an indicator that routing is not working for this device. "
|
"This might be an indicator that routing is not working for this device "
|
||||||
"Home Assistant will ignore availability for this light from now on. "
|
"or the device is having connectivity issues. "
|
||||||
|
"You can disable availability reporting for this device in the Hue options. "
|
||||||
"Device details: %s - %s (%s) fw: %s",
|
"Device details: %s - %s (%s) fw: %s",
|
||||||
self.name,
|
self.name,
|
||||||
self.device.product_data.manufacturer_name,
|
self.device.product_data.manufacturer_name,
|
||||||
@ -178,6 +192,4 @@ class HueBaseEntity(Entity):
|
|||||||
)
|
)
|
||||||
# do we want to store this in some persistent storage?
|
# do we want to store this in some persistent storage?
|
||||||
self._ignore_availability = True
|
self._ignore_availability = True
|
||||||
else:
|
|
||||||
self._ignore_availability = False
|
|
||||||
self._last_state = cur_state
|
self._last_state = cur_state
|
||||||
|
@ -7,7 +7,6 @@ from typing import Any
|
|||||||
from aiohue.v2 import HueBridgeV2
|
from aiohue.v2 import HueBridgeV2
|
||||||
from aiohue.v2.controllers.events import EventType
|
from aiohue.v2.controllers.events import EventType
|
||||||
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
||||||
from aiohue.v2.models.feature import AlertEffectType
|
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
@ -103,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
|
|
||||||
# Entities for Hue groups are disabled by default
|
# Entities for Hue groups are disabled by default
|
||||||
# unless they were enabled in old version (legacy option)
|
# unless they were enabled in old version (legacy option)
|
||||||
self._attr_entity_registry_enabled_default = bridge.config_entry.data.get(
|
self._attr_entity_registry_enabled_default = bridge.config_entry.options.get(
|
||||||
CONF_ALLOW_HUE_GROUPS, False
|
CONF_ALLOW_HUE_GROUPS, False
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -193,7 +192,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
color_xy=xy_color if light.supports_color else None,
|
color_xy=xy_color if light.supports_color else None,
|
||||||
color_temp=color_temp if light.supports_color_temperature else None,
|
color_temp=color_temp if light.supports_color_temperature else None,
|
||||||
transition_time=transition,
|
transition_time=transition,
|
||||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
|
||||||
allowed_errors=ALLOWED_ERRORS,
|
allowed_errors=ALLOWED_ERRORS,
|
||||||
)
|
)
|
||||||
for light in self.controller.get_lights(self.resource.id)
|
for light in self.controller.get_lights(self.resource.id)
|
||||||
@ -300,7 +298,10 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||||||
supported_color_modes.add(COLOR_MODE_ONOFF)
|
supported_color_modes.add(COLOR_MODE_ONOFF)
|
||||||
self._attr_supported_color_modes = supported_color_modes
|
self._attr_supported_color_modes = supported_color_modes
|
||||||
# pick a winner for the current colormode
|
# pick a winner for the current colormode
|
||||||
if lights_in_colortemp_mode == lights_with_color_temp_support:
|
if (
|
||||||
|
lights_with_color_temp_support > 0
|
||||||
|
and lights_in_colortemp_mode == lights_with_color_temp_support
|
||||||
|
):
|
||||||
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
|
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
|
||||||
elif lights_with_color_support > 0:
|
elif lights_with_color_support > 0:
|
||||||
self._attr_color_mode = COLOR_MODE_XY
|
self._attr_color_mode = COLOR_MODE_XY
|
||||||
|
@ -91,6 +91,9 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||||
# support transition if brightness control
|
# support transition if brightness control
|
||||||
self._attr_supported_features |= SUPPORT_TRANSITION
|
self._attr_supported_features |= SUPPORT_TRANSITION
|
||||||
|
self._last_xy: tuple[float, float] | None = self.xy_color
|
||||||
|
self._last_color_temp: int = self.color_temp
|
||||||
|
self._set_color_mode()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self) -> int | None:
|
def brightness(self) -> int | None:
|
||||||
@ -100,18 +103,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
return round((dimming.brightness / 100) * 255)
|
return round((dimming.brightness / 100) * 255)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def color_mode(self) -> str:
|
|
||||||
"""Return the current color mode of the light."""
|
|
||||||
if color_temp := self.resource.color_temperature:
|
|
||||||
if color_temp.mirek_valid and color_temp.mirek is not None:
|
|
||||||
return COLOR_MODE_COLOR_TEMP
|
|
||||||
if self.resource.supports_color:
|
|
||||||
return COLOR_MODE_XY
|
|
||||||
if self.resource.supports_dimming:
|
|
||||||
return COLOR_MODE_BRIGHTNESS
|
|
||||||
return COLOR_MODE_ONOFF
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if device is on (brightness above 0)."""
|
"""Return true if device is on (brightness above 0)."""
|
||||||
@ -158,6 +149,11 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
"dynamics": self.resource.dynamics.status.value,
|
"dynamics": self.resource.dynamics.status.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def on_update(self) -> None:
|
||||||
|
"""Call on update event."""
|
||||||
|
self._set_color_mode()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||||
@ -212,3 +208,43 @@ class HueLight(HueBaseEntity, LightEntity):
|
|||||||
id=self.resource.id,
|
id=self.resource.id,
|
||||||
short=flash == FLASH_SHORT,
|
short=flash == FLASH_SHORT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _set_color_mode(self) -> None:
|
||||||
|
"""Set current colormode of light."""
|
||||||
|
last_xy = self._last_xy
|
||||||
|
last_color_temp = self._last_color_temp
|
||||||
|
self._last_xy = self.xy_color
|
||||||
|
self._last_color_temp = self.color_temp
|
||||||
|
|
||||||
|
# Certified Hue lights return `mired_valid` to indicate CT is active
|
||||||
|
if color_temp := self.resource.color_temperature:
|
||||||
|
if color_temp.mirek_valid and color_temp.mirek is not None:
|
||||||
|
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
|
||||||
|
return
|
||||||
|
|
||||||
|
# Non-certified lights do not report their current color mode correctly
|
||||||
|
# so we keep track of the color values to determine which is active
|
||||||
|
if last_color_temp != self.color_temp:
|
||||||
|
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
|
||||||
|
return
|
||||||
|
if last_xy != self.xy_color:
|
||||||
|
self._attr_color_mode = COLOR_MODE_XY
|
||||||
|
return
|
||||||
|
|
||||||
|
# if we didn't detect any changes, abort and use previous values
|
||||||
|
if self._attr_color_mode is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# color mode not yet determined, work it out here
|
||||||
|
# Note that for lights that do not correctly report `mirek_valid`
|
||||||
|
# we might have an invalid startup state which will be auto corrected
|
||||||
|
if self.resource.supports_color:
|
||||||
|
self._attr_color_mode = COLOR_MODE_XY
|
||||||
|
elif self.resource.supports_color_temperature:
|
||||||
|
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
|
||||||
|
elif self.resource.supports_dimming:
|
||||||
|
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
|
||||||
|
else:
|
||||||
|
# fallback to on_off
|
||||||
|
self._attr_color_mode = COLOR_MODE_ONOFF
|
||||||
|
@ -511,11 +511,6 @@ class ZoneDevice(ClimateEntity):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._controller.available
|
return self._controller.available
|
||||||
|
|
||||||
@property
|
|
||||||
def assumed_state(self) -> bool:
|
|
||||||
"""Return True if unable to access real state of the entity."""
|
|
||||||
return self._controller.assumed_state
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return the ID of the controller device."""
|
"""Return the ID of the controller device."""
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
"domain": "izone",
|
"domain": "izone",
|
||||||
"name": "iZone",
|
"name": "iZone",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/izone",
|
"documentation": "https://www.home-assistant.io/integrations/izone",
|
||||||
"requirements": ["python-izone==1.1.8"],
|
"requirements": ["python-izone==1.2.3"],
|
||||||
"codeowners": ["@Swamp-Ig"],
|
"codeowners": ["@Swamp-Ig"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["iZone"]
|
"models": ["iZone"]
|
||||||
},
|
},
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_polling"
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ from .schema import ConnectionSchema
|
|||||||
|
|
||||||
CONF_KNX_GATEWAY: Final = "gateway"
|
CONF_KNX_GATEWAY: Final = "gateway"
|
||||||
CONF_MAX_RATE_LIMIT: Final = 60
|
CONF_MAX_RATE_LIMIT: Final = 60
|
||||||
|
CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0"
|
||||||
|
|
||||||
DEFAULT_ENTRY_DATA: Final = {
|
DEFAULT_ENTRY_DATA: Final = {
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||||
@ -328,6 +329,12 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
entry_data = {
|
entry_data = {
|
||||||
**DEFAULT_ENTRY_DATA,
|
**DEFAULT_ENTRY_DATA,
|
||||||
**self.general_settings,
|
**self.general_settings,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: self.general_settings.get(
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP
|
||||||
|
)
|
||||||
|
if self.general_settings.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
|
||||||
|
!= CONF_DEFAULT_LOCAL_IP
|
||||||
|
else None,
|
||||||
CONF_HOST: self.current_config.get(CONF_HOST, ""),
|
CONF_HOST: self.current_config.get(CONF_HOST, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,7 +344,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
**user_input,
|
**user_input,
|
||||||
}
|
}
|
||||||
|
|
||||||
entry_title = entry_data[CONF_KNX_CONNECTION_TYPE].capitalize()
|
entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize()
|
||||||
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
|
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
|
||||||
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
|
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
|
||||||
|
|
||||||
@ -388,12 +395,16 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.show_advanced_options:
|
if self.show_advanced_options:
|
||||||
|
local_ip = (
|
||||||
|
self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
|
||||||
|
if self.current_config.get(ConnectionSchema.CONF_KNX_LOCAL_IP)
|
||||||
|
is not None
|
||||||
|
else CONF_DEFAULT_LOCAL_IP
|
||||||
|
)
|
||||||
data_schema[
|
data_schema[
|
||||||
vol.Optional(
|
vol.Required(
|
||||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
||||||
default=self.current_config.get(
|
default=local_ip,
|
||||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
] = str
|
] = str
|
||||||
data_schema[
|
data_schema[
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"individual_address": "Individual address for the connection",
|
"individual_address": "Individual address for the connection",
|
||||||
"route_back": "Route Back / NAT Mode",
|
"route_back": "Route Back / NAT Mode",
|
||||||
"local_ip": "Local IP (leave empty if unsure)"
|
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
@ -29,7 +29,7 @@
|
|||||||
"individual_address": "Individual address for the routing connection",
|
"individual_address": "Individual address for the routing connection",
|
||||||
"multicast_group": "The multicast group used for routing",
|
"multicast_group": "The multicast group used for routing",
|
||||||
"multicast_port": "The multicast port used for routing",
|
"multicast_port": "The multicast port used for routing",
|
||||||
"local_ip": "Local IP (leave empty if unsure)"
|
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@
|
|||||||
"individual_address": "Default individual address",
|
"individual_address": "Default individual address",
|
||||||
"multicast_group": "Multicast group used for routing and discovery",
|
"multicast_group": "Multicast group used for routing and discovery",
|
||||||
"multicast_port": "Multicast port used for routing and discovery",
|
"multicast_port": "Multicast port used for routing and discovery",
|
||||||
"local_ip": "Local IP (leave empty if unsure)",
|
"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",
|
"state_updater": "Globally enable reading states from the KNX Bus",
|
||||||
"rate_limit": "Maximum outgoing telegrams per second"
|
"rate_limit": "Maximum outgoing telegrams per second"
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"individual_address": "Individual address for the connection",
|
"individual_address": "Individual address for the connection",
|
||||||
"local_ip": "Local IP (leave empty if unsure)",
|
"local_ip": "Local IP of Home Assistant (leave empty for automatic detection)",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"route_back": "Route Back / NAT Mode"
|
"route_back": "Route Back / NAT Mode"
|
||||||
},
|
},
|
||||||
@ -21,9 +21,9 @@
|
|||||||
"routing": {
|
"routing": {
|
||||||
"data": {
|
"data": {
|
||||||
"individual_address": "Individual address for the routing connection",
|
"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_group": "The multicast group used for routing",
|
||||||
"multicast_port": "The multicast port used for routing",
|
"multicast_port": "The multicast port used for routing"
|
||||||
"local_ip": "Local IP (leave empty if unsure)"
|
|
||||||
},
|
},
|
||||||
"description": "Please configure the routing options."
|
"description": "Please configure the routing options."
|
||||||
},
|
},
|
||||||
@ -47,9 +47,9 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"connection_type": "KNX Connection Type",
|
"connection_type": "KNX Connection Type",
|
||||||
"individual_address": "Default individual address",
|
"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_group": "Multicast group used for routing and discovery",
|
||||||
"multicast_port": "Multicast port used for routing and discovery",
|
"multicast_port": "Multicast port used for routing and discovery",
|
||||||
"local_ip": "Local IP (leave empty if unsure)",
|
|
||||||
"rate_limit": "Maximum outgoing telegrams per second",
|
"rate_limit": "Maximum outgoing telegrams per second",
|
||||||
"state_updater": "Globally enable reading states from the KNX Bus"
|
"state_updater": "Globally enable reading states from the KNX Bus"
|
||||||
}
|
}
|
||||||
@ -63,4 +63,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,14 +136,13 @@ async def async_setup_entry(
|
|||||||
for home_id in climate_topology.home_ids:
|
for home_id in climate_topology.home_ids:
|
||||||
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
||||||
|
|
||||||
try:
|
await data_handler.register_data_class(
|
||||||
await data_handler.register_data_class(
|
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
||||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
)
|
||||||
)
|
|
||||||
except KeyError:
|
if (climate_state := data_handler.data[signal_name]) is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
climate_state = data_handler.data[signal_name]
|
|
||||||
climate_topology.register_handler(home_id, climate_state.process_topology)
|
climate_topology.register_handler(home_id, climate_state.process_topology)
|
||||||
|
|
||||||
for room in climate_state.homes[home_id].rooms.values():
|
for room in climate_state.homes[home_id].rooms.values():
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Netatmo",
|
"name": "Netatmo",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pyatmo==6.2.0"
|
"pyatmo==6.2.2"
|
||||||
],
|
],
|
||||||
"after_dependencies": [
|
"after_dependencies": [
|
||||||
"cloud",
|
"cloud",
|
||||||
|
@ -49,17 +49,13 @@ async def async_setup_entry(
|
|||||||
for home_id in climate_topology.home_ids:
|
for home_id in climate_topology.home_ids:
|
||||||
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
||||||
|
|
||||||
try:
|
|
||||||
await data_handler.register_data_class(
|
|
||||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
await data_handler.register_data_class(
|
await data_handler.register_data_class(
|
||||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
||||||
)
|
)
|
||||||
climate_state = data_handler.data.get(signal_name)
|
|
||||||
|
if (climate_state := data_handler.data[signal_name]) is None:
|
||||||
|
continue
|
||||||
|
|
||||||
climate_topology.register_handler(home_id, climate_state.process_topology)
|
climate_topology.register_handler(home_id, climate_state.process_topology)
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[
|
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ipaddress import IPv4Address, IPv6Address, ip_interface
|
from ipaddress import IPv4Address, IPv6Address, ip_interface
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
@ -12,6 +14,8 @@ from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
|
|||||||
from .models import Adapter
|
from .models import Adapter
|
||||||
from .network import Network, async_get_network
|
from .network import Network, async_get_network
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||||
@ -32,6 +36,16 @@ async def async_get_source_ip(
|
|||||||
all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
|
all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
|
||||||
|
|
||||||
source_ip = util.async_get_source_ip(target_ip)
|
source_ip = util.async_get_source_ip(target_ip)
|
||||||
|
if not all_ipv4s:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
|
||||||
|
)
|
||||||
|
if source_ip is None:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"Could not determine source ip because the system does not have any enabled IPv4 addresses and creating a socket failed"
|
||||||
|
)
|
||||||
|
return source_ip
|
||||||
|
|
||||||
return source_ip if source_ip in all_ipv4s else all_ipv4s[0]
|
return source_ip if source_ip in all_ipv4s else all_ipv4s[0]
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ NO_IP_ERRORS = {
|
|||||||
"911": "A fatal error on NO-IP's side such as a database outage",
|
"911": "A fatal error on NO-IP's side such as a database outage",
|
||||||
}
|
}
|
||||||
|
|
||||||
UPDATE_URL = "https://dynupdate.noip.com/nic/update"
|
UPDATE_URL = "https://dynupdate.no-ip.com/nic/update"
|
||||||
HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}"
|
HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
@ -32,6 +32,8 @@ from .const import (
|
|||||||
UNDO_UPDATE_LISTENER,
|
UNDO_UPDATE_LISTENER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NUT_FAKE_SERIAL = ["unknown", "blank"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -140,7 +142,9 @@ def _firmware_from_status(status):
|
|||||||
def _serial_from_status(status):
|
def _serial_from_status(status):
|
||||||
"""Find the best serialvalue from the status."""
|
"""Find the best serialvalue from the status."""
|
||||||
serial = status.get("device.serial") or status.get("ups.serial")
|
serial = status.get("device.serial") or status.get("ups.serial")
|
||||||
if serial and (serial.lower() == "unknown" or serial.count("0") == len(serial)):
|
if serial and (
|
||||||
|
serial.lower() in NUT_FAKE_SERIAL or serial.count("0") == len(serial)
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
return serial
|
return serial
|
||||||
|
|
||||||
|
@ -380,6 +380,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
for user in plex_server.option_monitored_users
|
for user in plex_server.option_monitored_users
|
||||||
if plex_server.option_monitored_users[user]["enabled"]
|
if plex_server.option_monitored_users[user]["enabled"]
|
||||||
}
|
}
|
||||||
|
default_accounts.intersection_update(plex_server.accounts)
|
||||||
for user in plex_server.accounts:
|
for user in plex_server.accounts:
|
||||||
if user not in known_accounts:
|
if user not in known_accounts:
|
||||||
available_accounts[user] += " [New]"
|
available_accounts[user] += " [New]"
|
||||||
|
@ -219,7 +219,7 @@ class BlockSleepingClimate(
|
|||||||
return CURRENT_HVAC_OFF
|
return CURRENT_HVAC_OFF
|
||||||
|
|
||||||
return (
|
return (
|
||||||
CURRENT_HVAC_IDLE if self.device_block.status == "0" else CURRENT_HVAC_HEAT
|
CURRENT_HVAC_HEAT if bool(self.device_block.status) else CURRENT_HVAC_IDLE
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -123,6 +123,9 @@ async def async_get_triggers(
|
|||||||
append_input_triggers(triggers, input_triggers, device_id)
|
append_input_triggers(triggers, input_triggers, device_id)
|
||||||
return triggers
|
return triggers
|
||||||
|
|
||||||
|
if not block_wrapper.device.initialized:
|
||||||
|
return triggers
|
||||||
|
|
||||||
assert block_wrapper.device.blocks
|
assert block_wrapper.device.blocks
|
||||||
|
|
||||||
for block in block_wrapper.device.blocks:
|
for block in block_wrapper.device.blocks:
|
||||||
|
@ -163,7 +163,7 @@ class SisyphusPlayer(MediaPlayerEntity):
|
|||||||
if self._table.active_track:
|
if self._table.active_track:
|
||||||
return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE)
|
return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE)
|
||||||
|
|
||||||
return super.media_image_url()
|
return super().media_image_url
|
||||||
|
|
||||||
async def async_turn_on(self):
|
async def async_turn_on(self):
|
||||||
"""Wake up a sleeping table."""
|
"""Wake up a sleeping table."""
|
||||||
|
@ -307,6 +307,7 @@ CPU_SENSOR_PREFIXES = [
|
|||||||
"soc_thermal 1",
|
"soc_thermal 1",
|
||||||
"Tctl",
|
"Tctl",
|
||||||
"cpu0-thermal",
|
"cpu0-thermal",
|
||||||
|
"cpu0_thermal",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,9 +112,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
|||||||
self._supported_features |= SUPPORT_FAN_SPEED
|
self._supported_features |= SUPPORT_FAN_SPEED
|
||||||
self._fan_speed_type = EnumTypeData.from_json(function.values)
|
self._fan_speed_type = EnumTypeData.from_json(function.values)
|
||||||
|
|
||||||
if function := device.function.get(DPCode.ELECTRICITY_LEFT):
|
if status_range := device.status_range.get(DPCode.ELECTRICITY_LEFT):
|
||||||
self._supported_features |= SUPPORT_BATTERY
|
self._supported_features |= SUPPORT_BATTERY
|
||||||
self._battery_level_type = IntegerTypeData.from_json(function.values)
|
self._battery_level_type = IntegerTypeData.from_json(status_range.values)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def battery_level(self) -> int | None:
|
def battery_level(self) -> int | None:
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Xiaomi Miio",
|
"name": "Xiaomi Miio",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
||||||
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.9.2"],
|
"requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.9.2"],
|
||||||
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
|
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
|
||||||
"zeroconf": ["_miio._udp.local."],
|
"zeroconf": ["_miio._udp.local."],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
|
from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN
|
||||||
from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES
|
from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
|
||||||
@ -248,6 +249,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for feature, description in NUMBER_TYPES.items():
|
for feature, description in NUMBER_TYPES.items():
|
||||||
|
if feature == FEATURE_SET_LED_BRIGHTNESS and model != MODEL_FAN_ZA5:
|
||||||
|
# Delete LED bightness entity created by mistake if it exists
|
||||||
|
entity_reg = hass.helpers.entity_registry.async_get()
|
||||||
|
entity_id = entity_reg.async_get_entity_id(
|
||||||
|
PLATFORM_DOMAIN, DOMAIN, f"{description.key}_{config_entry.unique_id}"
|
||||||
|
)
|
||||||
|
if entity_id:
|
||||||
|
entity_reg.async_remove(entity_id)
|
||||||
|
continue
|
||||||
if feature & features:
|
if feature & features:
|
||||||
if (
|
if (
|
||||||
description.key == ATTR_OSCILLATION_ANGLE
|
description.key == ATTR_OSCILLATION_ANGLE
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 12
|
MINOR_VERSION: Final = 12
|
||||||
PATCH_VERSION: Final = "7"
|
PATCH_VERSION: Final = "8"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||||
|
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
|
|||||||
flipr-api==1.4.1
|
flipr-api==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux_led==0.27.21
|
flux_led==0.27.32
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
@ -754,7 +754,7 @@ gpiozero==1.5.1
|
|||||||
gps3==0.33.3
|
gps3==0.33.3
|
||||||
|
|
||||||
# homeassistant.components.gree
|
# homeassistant.components.gree
|
||||||
greeclimate==0.12.5
|
greeclimate==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.greeneye_monitor
|
# homeassistant.components.greeneye_monitor
|
||||||
greeneye_monitor==2.1
|
greeneye_monitor==2.1
|
||||||
@ -1006,7 +1006,7 @@ meteofrance-api==1.0.2
|
|||||||
mficlient==0.3.0
|
mficlient==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
micloud==0.4
|
micloud==0.5
|
||||||
|
|
||||||
# homeassistant.components.miflora
|
# homeassistant.components.miflora
|
||||||
miflora==0.7.0
|
miflora==0.7.0
|
||||||
@ -1364,7 +1364,7 @@ pyarlo==0.2.4
|
|||||||
pyatag==0.3.5.3
|
pyatag==0.3.5.3
|
||||||
|
|
||||||
# homeassistant.components.netatmo
|
# homeassistant.components.netatmo
|
||||||
pyatmo==6.2.0
|
pyatmo==6.2.2
|
||||||
|
|
||||||
# homeassistant.components.atome
|
# homeassistant.components.atome
|
||||||
pyatome==0.1.1
|
pyatome==0.1.1
|
||||||
@ -1890,7 +1890,7 @@ python-gitlab==1.6.0
|
|||||||
python-hpilo==4.3
|
python-hpilo==4.3
|
||||||
|
|
||||||
# homeassistant.components.izone
|
# homeassistant.components.izone
|
||||||
python-izone==1.1.8
|
python-izone==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.joaoapps_join
|
# homeassistant.components.joaoapps_join
|
||||||
python-join-api==0.0.6
|
python-join-api==0.0.6
|
||||||
|
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
|||||||
flipr-api==1.4.1
|
flipr-api==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.flux_led
|
# homeassistant.components.flux_led
|
||||||
flux_led==0.27.21
|
flux_led==0.27.32
|
||||||
|
|
||||||
# homeassistant.components.homekit
|
# homeassistant.components.homekit
|
||||||
fnvhash==0.1.0
|
fnvhash==0.1.0
|
||||||
@ -467,7 +467,7 @@ google-nest-sdm==0.4.9
|
|||||||
googlemaps==2.5.1
|
googlemaps==2.5.1
|
||||||
|
|
||||||
# homeassistant.components.gree
|
# homeassistant.components.gree
|
||||||
greeclimate==0.12.5
|
greeclimate==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.greeneye_monitor
|
# homeassistant.components.greeneye_monitor
|
||||||
greeneye_monitor==2.1
|
greeneye_monitor==2.1
|
||||||
@ -612,7 +612,7 @@ meteofrance-api==1.0.2
|
|||||||
mficlient==0.3.0
|
mficlient==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
micloud==0.4
|
micloud==0.5
|
||||||
|
|
||||||
# homeassistant.components.mill
|
# homeassistant.components.mill
|
||||||
mill-local==0.1.0
|
mill-local==0.1.0
|
||||||
@ -832,7 +832,7 @@ pyarlo==0.2.4
|
|||||||
pyatag==0.3.5.3
|
pyatag==0.3.5.3
|
||||||
|
|
||||||
# homeassistant.components.netatmo
|
# homeassistant.components.netatmo
|
||||||
pyatmo==6.2.0
|
pyatmo==6.2.2
|
||||||
|
|
||||||
# homeassistant.components.apple_tv
|
# homeassistant.components.apple_tv
|
||||||
pyatv==0.8.2
|
pyatv==0.8.2
|
||||||
@ -1136,7 +1136,7 @@ python-ecobee-api==0.2.14
|
|||||||
python-forecastio==1.4.0
|
python-forecastio==1.4.0
|
||||||
|
|
||||||
# homeassistant.components.izone
|
# homeassistant.components.izone
|
||||||
python-izone==1.1.8
|
python-izone==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.juicenet
|
# homeassistant.components.juicenet
|
||||||
python-juicenet==1.0.2
|
python-juicenet==1.0.2
|
||||||
|
@ -3030,8 +3030,8 @@ async def test_sensorstate(hass):
|
|||||||
"""Test SensorState trait support for sensor domain."""
|
"""Test SensorState trait support for sensor domain."""
|
||||||
sensor_types = {
|
sensor_types = {
|
||||||
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
||||||
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
sensor.DEVICE_CLASS_CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
||||||
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
sensor.DEVICE_CLASS_CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
||||||
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
|
sensor.DEVICE_CLASS_PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
|
||||||
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
sensor.DEVICE_CLASS_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
||||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
||||||
|
@ -11,6 +11,7 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components import ssdp, zeroconf
|
from homeassistant.components import ssdp, zeroconf
|
||||||
from homeassistant.components.hue import config_flow, const
|
from homeassistant.components.hue import config_flow, const
|
||||||
from homeassistant.components.hue.errors import CannotConnect
|
from homeassistant.components.hue.errors import CannotConnect
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -701,12 +702,33 @@ async def test_options_flow_v2(hass):
|
|||||||
"""Test options config flow for a V2 bridge."""
|
"""Test options config flow for a V2 bridge."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain="hue",
|
domain="hue",
|
||||||
unique_id="v2bridge",
|
unique_id="aabbccddeeff",
|
||||||
data={"host": "0.0.0.0", "api_version": 2},
|
data={"host": "0.0.0.0", "api_version": 2},
|
||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
assert config_flow.HueFlowHandler.async_supports_options_flow(entry) is False
|
dev_reg = dr.async_get(hass)
|
||||||
|
mock_dev_id = "aabbccddee"
|
||||||
|
dev_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id, identifiers={(const.DOMAIN, mock_dev_id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
schema = result["data_schema"].schema
|
||||||
|
assert _get_schema_default(schema, const.CONF_IGNORE_AVAILABILITY) == []
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={const.CONF_IGNORE_AVAILABILITY: [mock_dev_id]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["data"] == {
|
||||||
|
const.CONF_IGNORE_AVAILABILITY: [mock_dev_id],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_zeroconf(hass, aioclient_mock):
|
async def test_bridge_zeroconf(hass, aioclient_mock):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test the KNX config flow."""
|
"""Test the KNX config flow."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from xknx import XKNX
|
from xknx import XKNX
|
||||||
from xknx.io import DEFAULT_MCAST_GRP
|
from xknx.io import DEFAULT_MCAST_GRP
|
||||||
from xknx.io.gateway_scanner import GatewayDescriptor
|
from xknx.io.gateway_scanner import GatewayDescriptor
|
||||||
@ -8,6 +9,7 @@ from xknx.io.gateway_scanner import GatewayDescriptor
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.knx import ConnectionSchema
|
from homeassistant.components.knx import ConnectionSchema
|
||||||
from homeassistant.components.knx.config_flow import (
|
from homeassistant.components.knx.config_flow import (
|
||||||
|
CONF_DEFAULT_LOCAL_IP,
|
||||||
CONF_KNX_GATEWAY,
|
CONF_KNX_GATEWAY,
|
||||||
DEFAULT_ENTRY_DATA,
|
DEFAULT_ENTRY_DATA,
|
||||||
)
|
)
|
||||||
@ -585,6 +587,7 @@ async def test_options_flow(
|
|||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
|
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
|
||||||
CONF_HOST: "",
|
CONF_HOST: "",
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
|
||||||
@ -643,14 +646,65 @@ async def test_tunneling_options_flow(
|
|||||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 20,
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||||
CONF_HOST: "192.168.1.1",
|
CONF_HOST: "192.168.1.1",
|
||||||
CONF_PORT: 3675,
|
CONF_PORT: 3675,
|
||||||
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
ConnectionSchema.CONF_KNX_ROUTE_BACK: True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"user_input,config_entry_data",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
||||||
|
CONF_HOST: "",
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
||||||
|
CONF_HOST: "",
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||||
|
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||||
|
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
||||||
|
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
||||||
|
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_advanced_options(
|
async def test_advanced_options(
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
user_input,
|
||||||
|
config_entry_data,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test options config flow."""
|
"""Test options config flow."""
|
||||||
mock_config_entry.add_to_hass(hass)
|
mock_config_entry.add_to_hass(hass)
|
||||||
@ -668,28 +722,11 @@ async def test_advanced_options(
|
|||||||
|
|
||||||
result2 = await hass.config_entries.options.async_configure(
|
result2 = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={
|
user_input=user_input,
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
|
||||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
|
||||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
|
||||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||||
assert not result2.get("data")
|
assert not result2.get("data")
|
||||||
|
|
||||||
assert mock_config_entry.data == {
|
assert mock_config_entry.data == config_entry_data
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250",
|
|
||||||
CONF_HOST: "",
|
|
||||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
|
||||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
|
||||||
ConnectionSchema.CONF_KNX_RATE_LIMIT: 25,
|
|
||||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: False,
|
|
||||||
ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.112",
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,7 @@ from ipaddress import IPv4Address
|
|||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
import ifaddr
|
import ifaddr
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import network
|
from homeassistant.components import network
|
||||||
from homeassistant.components.network.const import (
|
from homeassistant.components.network.const import (
|
||||||
@ -13,6 +14,7 @@ from homeassistant.components.network.const import (
|
|||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
_NO_LOOPBACK_IPADDR = "192.168.1.5"
|
_NO_LOOPBACK_IPADDR = "192.168.1.5"
|
||||||
@ -602,3 +604,49 @@ async def test_async_get_ipv4_broadcast_addresses_multiple(hass, hass_storage):
|
|||||||
IPv4Address("192.168.1.255"),
|
IPv4Address("192.168.1.255"),
|
||||||
IPv4Address("169.254.255.255"),
|
IPv4Address("169.254.255.255"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_get_source_ip_no_enabled_addresses(hass, hass_storage, caplog):
|
||||||
|
"""Test getting the source ip address when all adapters are disabled."""
|
||||||
|
hass_storage[STORAGE_KEY] = {
|
||||||
|
"version": STORAGE_VERSION,
|
||||||
|
"key": STORAGE_KEY,
|
||||||
|
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||||
|
return_value=[],
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.network.util.socket.socket",
|
||||||
|
return_value=_mock_socket(["192.168.1.5"]),
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5"
|
||||||
|
|
||||||
|
assert "source address detection may be inaccurate" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses(
|
||||||
|
hass, hass_storage, caplog
|
||||||
|
):
|
||||||
|
"""Test getting the source ip address when all adapters are disabled and getting it fails."""
|
||||||
|
hass_storage[STORAGE_KEY] = {
|
||||||
|
"version": STORAGE_VERSION,
|
||||||
|
"key": STORAGE_KEY,
|
||||||
|
"data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]},
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.network.util.ifaddr.get_adapters",
|
||||||
|
return_value=[],
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.network.util.socket.socket",
|
||||||
|
return_value=_mock_socket([None]),
|
||||||
|
):
|
||||||
|
assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await network.async_get_source_ip(hass, MDNS_TARGET_IP)
|
||||||
|
@ -168,6 +168,42 @@ async def test_get_triggers_button(hass):
|
|||||||
assert_lists_same(triggers, expected_triggers)
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_triggers_non_initialized_devices(hass):
|
||||||
|
"""Test we get the empty triggers for non-initialized devices."""
|
||||||
|
await async_setup_component(hass, "shelly", {})
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={"sleep_period": 43200, "model": "SHDW-2", "host": "1.2.3.4"},
|
||||||
|
unique_id="12345678",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
device = Mock(
|
||||||
|
blocks=None,
|
||||||
|
settings=None,
|
||||||
|
shelly=None,
|
||||||
|
update=AsyncMock(),
|
||||||
|
initialized=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}}
|
||||||
|
hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {}
|
||||||
|
coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
|
||||||
|
BLOCK
|
||||||
|
] = BlockDeviceWrapper(hass, config_entry, device)
|
||||||
|
|
||||||
|
coap_wrapper.async_setup()
|
||||||
|
|
||||||
|
expected_triggers = []
|
||||||
|
|
||||||
|
triggers = await async_get_device_automations(
|
||||||
|
hass, "trigger", coap_wrapper.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_lists_same(triggers, expected_triggers)
|
||||||
|
|
||||||
|
|
||||||
async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper):
|
async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper):
|
||||||
"""Test error raised for invalid shelly device_id."""
|
"""Test error raised for invalid shelly device_id."""
|
||||||
assert coap_wrapper
|
assert coap_wrapper
|
||||||
|
Loading…
x
Reference in New Issue
Block a user