mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +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:
|
||||
hass_url = self.custom_url
|
||||
|
||||
favorites = self.device.favorites()
|
||||
|
||||
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)
|
||||
|
||||
@ -261,15 +263,15 @@ class ConfiguredDoorBird:
|
||||
def _get_event_name(self, 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."""
|
||||
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
|
||||
|
||||
# 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)
|
||||
|
||||
if not self.get_webhook_id(url):
|
||||
if not self.get_webhook_id(url, favs=favs):
|
||||
_LOGGER.warning(
|
||||
'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"',
|
||||
url,
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Flux LED/MagicHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.27.21"],
|
||||
"requirements": ["flux_led==0.27.32"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch"],
|
||||
"iot_class": "local_push",
|
||||
|
@ -160,7 +160,10 @@ class FroniusSolarNet:
|
||||
)
|
||||
if self.logger_coordinator:
|
||||
_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"][
|
||||
"value"
|
||||
]
|
||||
|
@ -2296,8 +2296,8 @@ class SensorStateTrait(_Trait):
|
||||
|
||||
sensor_types = {
|
||||
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
||||
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
||||
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
||||
sensor.DEVICE_CLASS_CO: ("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_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Gree Climate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"requirements": ["greeclimate==0.12.5"],
|
||||
"requirements": ["greeclimate==1.0.1"],
|
||||
"codeowners": ["@cmroche"],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
@ -619,7 +619,9 @@ class GTFSDepartureSensor(SensorEntity):
|
||||
if not self._departure:
|
||||
self._state = None
|
||||
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
|
||||
if not self._departure:
|
||||
|
@ -134,7 +134,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
if (
|
||||
hdrs.CONTENT_LENGTH in result.headers
|
||||
and int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000
|
||||
):
|
||||
) or result.status in (204, 304):
|
||||
# Return Response
|
||||
body = await result.read()
|
||||
return web.Response(
|
||||
|
@ -17,13 +17,15 @@ from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import callback
|
||||
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 .const import (
|
||||
CONF_ALLOW_HUE_GROUPS,
|
||||
CONF_ALLOW_UNREACHABLE,
|
||||
CONF_API_VERSION,
|
||||
CONF_IGNORE_AVAILABILITY,
|
||||
DEFAULT_ALLOW_HUE_GROUPS,
|
||||
DEFAULT_ALLOW_UNREACHABLE,
|
||||
DOMAIN,
|
||||
@ -46,17 +48,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> HueOptionsFlowHandler:
|
||||
) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return HueOptionsFlowHandler(config_entry)
|
||||
|
||||
@classmethod
|
||||
@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
|
||||
if config_entry.data.get(CONF_API_VERSION, 1) == 1:
|
||||
return HueV1OptionsFlowHandler(config_entry)
|
||||
return HueV2OptionsFlowHandler(config_entry)
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Hue flow."""
|
||||
@ -288,8 +284,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_link()
|
||||
|
||||
|
||||
class HueOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Hue options."""
|
||||
class HueV1OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Hue options for V1 implementation."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""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"
|
||||
|
||||
CONF_API_VERSION = "api_version"
|
||||
CONF_IGNORE_AVAILABILITY = "ignore_availability"
|
||||
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
|
@ -70,7 +70,8 @@
|
||||
"data": {
|
||||
"allow_hue_groups": "Allow Hue groups",
|
||||
"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": {
|
||||
"allow_hue_groups": "Allow Hue groups",
|
||||
"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": {
|
||||
"allow_hue_groups": "Sta Hue-groepen 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 ..bridge import HueBridge
|
||||
from ..const import DOMAIN
|
||||
from ..const import CONF_IGNORE_AVAILABILITY, DOMAIN
|
||||
|
||||
RESOURCE_TYPE_NAMES = {
|
||||
# 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:
|
||||
"""Call when entity is added."""
|
||||
self._check_availability_workaround()
|
||||
self._check_availability()
|
||||
# Add value_changed callbacks.
|
||||
self.async_on_remove(
|
||||
self.controller.subscribe(
|
||||
@ -80,7 +80,7 @@ class HueBaseEntity(Entity):
|
||||
(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:
|
||||
return
|
||||
self.async_on_remove(
|
||||
@ -92,25 +92,27 @@ class HueBaseEntity(Entity):
|
||||
)
|
||||
# subscribe to zigbee_connectivity to catch availability changes
|
||||
if zigbee := self.bridge.api.devices.get_zigbee_connectivity(self.device.id):
|
||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||
self._handle_event,
|
||||
zigbee.id,
|
||||
EventType.RESOURCE_UPDATED,
|
||||
self.async_on_remove(
|
||||
self.bridge.api.sensors.zigbee_connectivity.subscribe(
|
||||
self._handle_event,
|
||||
zigbee.id,
|
||||
EventType.RESOURCE_UPDATED,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return entity availability."""
|
||||
# entities without a device attached should be always available
|
||||
if self.device is None:
|
||||
# entities without a device attached should be always available
|
||||
return True
|
||||
# the zigbee connectivity sensor itself should be always available
|
||||
if self.resource.type == ResourceTypes.ZIGBEE_CONNECTIVITY:
|
||||
# the zigbee connectivity sensor itself should be always available
|
||||
return True
|
||||
if self._ignore_availability:
|
||||
return True
|
||||
# all device-attached entities get availability from the zigbee connectivity
|
||||
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 True
|
||||
|
||||
@ -130,30 +132,41 @@ class HueBaseEntity(Entity):
|
||||
ent_reg.async_remove(self.entity_id)
|
||||
else:
|
||||
self.logger.debug("Received status update for %s", self.entity_id)
|
||||
self._check_availability_workaround()
|
||||
self._check_availability()
|
||||
self.on_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _check_availability_workaround(self):
|
||||
def _check_availability(self):
|
||||
"""Check availability of the device."""
|
||||
if self.resource.type != ResourceTypes.LIGHT:
|
||||
return
|
||||
# return if we already processed this entity
|
||||
if self._ignore_availability is not None:
|
||||
# already processed
|
||||
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:
|
||||
# certified products report their state correctly
|
||||
self._ignore_availability = False
|
||||
return
|
||||
# some (3th party) Hue lights report their connection status incorrectly
|
||||
# causing the zigbee availability to report as disconnected while in fact
|
||||
# it can be controlled. Although this is in fact something the device manufacturer
|
||||
# should fix, we work around it here. If the light is reported unavailable
|
||||
# it can be controlled. If the light is reported unavailable
|
||||
# by the zigbee connectivity but the state changes its considered as a
|
||||
# malfunctioning device and we report it.
|
||||
# while the user should actually fix this issue instead of ignoring it, we
|
||||
# ignore the availability for this light from this point.
|
||||
# While the user should actually fix this issue, we allow to
|
||||
# ignore the availability for this light/device from the config options.
|
||||
cur_state = self.resource.on.on
|
||||
if self._last_state is None:
|
||||
self._last_state = cur_state
|
||||
@ -166,9 +179,10 @@ class HueBaseEntity(Entity):
|
||||
# the device state changed from on->off or off->on
|
||||
# while it was reported as not connected!
|
||||
self.logger.warning(
|
||||
"Light %s changed state while reported as disconnected. "
|
||||
"This might be an indicator that routing is not working for this device. "
|
||||
"Home Assistant will ignore availability for this light from now on. "
|
||||
"Device %s changed state while reported as disconnected. "
|
||||
"This might be an indicator that routing is not working for this device "
|
||||
"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",
|
||||
self.name,
|
||||
self.device.product_data.manufacturer_name,
|
||||
@ -178,6 +192,4 @@ class HueBaseEntity(Entity):
|
||||
)
|
||||
# do we want to store this in some persistent storage?
|
||||
self._ignore_availability = True
|
||||
else:
|
||||
self._ignore_availability = False
|
||||
self._last_state = cur_state
|
||||
|
@ -7,7 +7,6 @@ from typing import Any
|
||||
from aiohue.v2 import HueBridgeV2
|
||||
from aiohue.v2.controllers.events import EventType
|
||||
from aiohue.v2.controllers.groups import GroupedLight, Room, Zone
|
||||
from aiohue.v2.models.feature import AlertEffectType
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@ -103,7 +102,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
|
||||
# Entities for Hue groups are disabled by default
|
||||
# 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
|
||||
)
|
||||
|
||||
@ -193,7 +192,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
color_xy=xy_color if light.supports_color else None,
|
||||
color_temp=color_temp if light.supports_color_temperature else None,
|
||||
transition_time=transition,
|
||||
alert=AlertEffectType.BREATHE if flash is not None else None,
|
||||
allowed_errors=ALLOWED_ERRORS,
|
||||
)
|
||||
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)
|
||||
self._attr_supported_color_modes = supported_color_modes
|
||||
# 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
|
||||
elif lights_with_color_support > 0:
|
||||
self._attr_color_mode = COLOR_MODE_XY
|
||||
|
@ -91,6 +91,9 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||
# support transition if brightness control
|
||||
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
|
||||
def brightness(self) -> int | None:
|
||||
@ -100,18 +103,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
return round((dimming.brightness / 100) * 255)
|
||||
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
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on (brightness above 0)."""
|
||||
@ -158,6 +149,11 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
"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:
|
||||
"""Turn the device on."""
|
||||
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
|
||||
@ -212,3 +208,43 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
id=self.resource.id,
|
||||
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 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
|
||||
def unique_id(self):
|
||||
"""Return the ID of the controller device."""
|
||||
|
@ -2,11 +2,11 @@
|
||||
"domain": "izone",
|
||||
"name": "iZone",
|
||||
"documentation": "https://www.home-assistant.io/integrations/izone",
|
||||
"requirements": ["python-izone==1.1.8"],
|
||||
"requirements": ["python-izone==1.2.3"],
|
||||
"codeowners": ["@Swamp-Ig"],
|
||||
"config_flow": true,
|
||||
"homekit": {
|
||||
"models": ["iZone"]
|
||||
},
|
||||
"iot_class": "local_push"
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ from .schema import ConnectionSchema
|
||||
|
||||
CONF_KNX_GATEWAY: Final = "gateway"
|
||||
CONF_MAX_RATE_LIMIT: Final = 60
|
||||
CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0"
|
||||
|
||||
DEFAULT_ENTRY_DATA: Final = {
|
||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: ConnectionSchema.CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||
@ -328,6 +329,12 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
entry_data = {
|
||||
**DEFAULT_ENTRY_DATA,
|
||||
**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, ""),
|
||||
}
|
||||
|
||||
@ -337,7 +344,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
**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:
|
||||
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
|
||||
|
||||
@ -388,12 +395,16 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
}
|
||||
|
||||
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[
|
||||
vol.Optional(
|
||||
vol.Required(
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
||||
default=self.current_config.get(
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP,
|
||||
),
|
||||
default=local_ip,
|
||||
)
|
||||
] = str
|
||||
data_schema[
|
||||
|
@ -20,7 +20,7 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"individual_address": "Individual address for the connection",
|
||||
"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": {
|
||||
@ -29,7 +29,7 @@
|
||||
"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 (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",
|
||||
"multicast_group": "Multicast group 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",
|
||||
"rate_limit": "Maximum outgoing telegrams per second"
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"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",
|
||||
"route_back": "Route Back / NAT Mode"
|
||||
},
|
||||
@ -21,9 +21,9 @@
|
||||
"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",
|
||||
"local_ip": "Local IP (leave empty if unsure)"
|
||||
"multicast_port": "The multicast port used for routing"
|
||||
},
|
||||
"description": "Please configure the routing options."
|
||||
},
|
||||
@ -47,9 +47,9 @@
|
||||
"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",
|
||||
"local_ip": "Local IP (leave empty if unsure)",
|
||||
"rate_limit": "Maximum outgoing telegrams per second",
|
||||
"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:
|
||||
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:
|
||||
await data_handler.register_data_class(
|
||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
||||
)
|
||||
|
||||
if (climate_state := data_handler.data[signal_name]) is None:
|
||||
continue
|
||||
|
||||
climate_state = data_handler.data[signal_name]
|
||||
climate_topology.register_handler(home_id, climate_state.process_topology)
|
||||
|
||||
for room in climate_state.homes[home_id].rooms.values():
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Netatmo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||
"requirements": [
|
||||
"pyatmo==6.2.0"
|
||||
"pyatmo==6.2.2"
|
||||
],
|
||||
"after_dependencies": [
|
||||
"cloud",
|
||||
|
@ -49,17 +49,13 @@ async def async_setup_entry(
|
||||
for home_id in climate_topology.home_ids:
|
||||
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(
|
||||
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)
|
||||
|
||||
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[
|
||||
|
@ -2,8 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_interface
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
@ -12,6 +14,8 @@ from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_network
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bind_hass
|
||||
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])
|
||||
|
||||
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]
|
||||
|
||||
|
||||
|
@ -35,7 +35,7 @@ NO_IP_ERRORS = {
|
||||
"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}"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
|
@ -32,6 +32,8 @@ from .const import (
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
NUT_FAKE_SERIAL = ["unknown", "blank"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -140,7 +142,9 @@ def _firmware_from_status(status):
|
||||
def _serial_from_status(status):
|
||||
"""Find the best serialvalue from the status."""
|
||||
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 serial
|
||||
|
||||
|
@ -380,6 +380,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
for user in plex_server.option_monitored_users
|
||||
if plex_server.option_monitored_users[user]["enabled"]
|
||||
}
|
||||
default_accounts.intersection_update(plex_server.accounts)
|
||||
for user in plex_server.accounts:
|
||||
if user not in known_accounts:
|
||||
available_accounts[user] += " [New]"
|
||||
|
@ -219,7 +219,7 @@ class BlockSleepingClimate(
|
||||
return CURRENT_HVAC_OFF
|
||||
|
||||
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
|
||||
|
@ -123,6 +123,9 @@ async def async_get_triggers(
|
||||
append_input_triggers(triggers, input_triggers, device_id)
|
||||
return triggers
|
||||
|
||||
if not block_wrapper.device.initialized:
|
||||
return triggers
|
||||
|
||||
assert block_wrapper.device.blocks
|
||||
|
||||
for block in block_wrapper.device.blocks:
|
||||
|
@ -163,7 +163,7 @@ class SisyphusPlayer(MediaPlayerEntity):
|
||||
if self._table.active_track:
|
||||
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):
|
||||
"""Wake up a sleeping table."""
|
||||
|
@ -307,6 +307,7 @@ CPU_SENSOR_PREFIXES = [
|
||||
"soc_thermal 1",
|
||||
"Tctl",
|
||||
"cpu0-thermal",
|
||||
"cpu0_thermal",
|
||||
]
|
||||
|
||||
|
||||
|
@ -112,9 +112,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
||||
self._supported_features |= SUPPORT_FAN_SPEED
|
||||
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._battery_level_type = IntegerTypeData.from_json(function.values)
|
||||
self._battery_level_type = IntegerTypeData.from_json(status_range.values)
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Xiaomi Miio",
|
||||
"config_flow": true,
|
||||
"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"],
|
||||
"zeroconf": ["_miio._udp.local."],
|
||||
"iot_class": "local_polling"
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
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.core import callback
|
||||
|
||||
@ -248,6 +249,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
return
|
||||
|
||||
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 (
|
||||
description.key == ATTR_OSCILLATION_ANGLE
|
||||
|
@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "7"
|
||||
PATCH_VERSION: Final = "8"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.21
|
||||
flux_led==0.27.32
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -754,7 +754,7 @@ gpiozero==1.5.1
|
||||
gps3==0.33.3
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.12.5
|
||||
greeclimate==1.0.1
|
||||
|
||||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==2.1
|
||||
@ -1006,7 +1006,7 @@ meteofrance-api==1.0.2
|
||||
mficlient==0.3.0
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
micloud==0.4
|
||||
micloud==0.5
|
||||
|
||||
# homeassistant.components.miflora
|
||||
miflora==0.7.0
|
||||
@ -1364,7 +1364,7 @@ pyarlo==0.2.4
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==6.2.0
|
||||
pyatmo==6.2.2
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyatome==0.1.1
|
||||
@ -1890,7 +1890,7 @@ python-gitlab==1.6.0
|
||||
python-hpilo==4.3
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.1.8
|
||||
python-izone==1.2.3
|
||||
|
||||
# homeassistant.components.joaoapps_join
|
||||
python-join-api==0.0.6
|
||||
|
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.27.21
|
||||
flux_led==0.27.32
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -467,7 +467,7 @@ google-nest-sdm==0.4.9
|
||||
googlemaps==2.5.1
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.12.5
|
||||
greeclimate==1.0.1
|
||||
|
||||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==2.1
|
||||
@ -612,7 +612,7 @@ meteofrance-api==1.0.2
|
||||
mficlient==0.3.0
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
micloud==0.4
|
||||
micloud==0.5
|
||||
|
||||
# homeassistant.components.mill
|
||||
mill-local==0.1.0
|
||||
@ -832,7 +832,7 @@ pyarlo==0.2.4
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==6.2.0
|
||||
pyatmo==6.2.2
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.8.2
|
||||
@ -1136,7 +1136,7 @@ python-ecobee-api==0.2.14
|
||||
python-forecastio==1.4.0
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.1.8
|
||||
python-izone==1.2.3
|
||||
|
||||
# homeassistant.components.juicenet
|
||||
python-juicenet==1.0.2
|
||||
|
@ -3030,8 +3030,8 @@ async def test_sensorstate(hass):
|
||||
"""Test SensorState trait support for sensor domain."""
|
||||
sensor_types = {
|
||||
sensor.DEVICE_CLASS_AQI: ("AirQuality", "AQI"),
|
||||
sensor.DEVICE_CLASS_CO: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
||||
sensor.DEVICE_CLASS_CO2: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
||||
sensor.DEVICE_CLASS_CO: ("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_PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: (
|
||||
|
@ -11,6 +11,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.components.hue import config_flow, const
|
||||
from homeassistant.components.hue.errors import CannotConnect
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -701,12 +702,33 @@ async def test_options_flow_v2(hass):
|
||||
"""Test options config flow for a V2 bridge."""
|
||||
entry = MockConfigEntry(
|
||||
domain="hue",
|
||||
unique_id="v2bridge",
|
||||
unique_id="aabbccddeeff",
|
||||
data={"host": "0.0.0.0", "api_version": 2},
|
||||
)
|
||||
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):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test the KNX config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from xknx import XKNX
|
||||
from xknx.io import DEFAULT_MCAST_GRP
|
||||
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.components.knx import ConnectionSchema
|
||||
from homeassistant.components.knx.config_flow import (
|
||||
CONF_DEFAULT_LOCAL_IP,
|
||||
CONF_KNX_GATEWAY,
|
||||
DEFAULT_ENTRY_DATA,
|
||||
)
|
||||
@ -585,6 +587,7 @@ async def test_options_flow(
|
||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC,
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255",
|
||||
CONF_HOST: "",
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||
ConnectionSchema.CONF_KNX_MCAST_PORT: 3675,
|
||||
ConnectionSchema.CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
||||
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_RATE_LIMIT: 20,
|
||||
ConnectionSchema.CONF_KNX_STATE_UPDATER: True,
|
||||
ConnectionSchema.CONF_KNX_LOCAL_IP: None,
|
||||
CONF_HOST: "192.168.1.1",
|
||||
CONF_PORT: 3675,
|
||||
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(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
user_input,
|
||||
config_entry_data,
|
||||
) -> None:
|
||||
"""Test options config flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@ -668,28 +722,11 @@ async def test_advanced_options(
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
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",
|
||||
},
|
||||
user_input=user_input,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||
assert not result2.get("data")
|
||||
|
||||
assert mock_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",
|
||||
}
|
||||
assert mock_config_entry.data == config_entry_data
|
||||
|
@ -3,6 +3,7 @@ from ipaddress import IPv4Address
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import ifaddr
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import network
|
||||
from homeassistant.components.network.const import (
|
||||
@ -13,6 +14,7 @@ from homeassistant.components.network.const import (
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
_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("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)
|
||||
|
||||
|
||||
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):
|
||||
"""Test error raised for invalid shelly device_id."""
|
||||
assert coap_wrapper
|
||||
|
Loading…
x
Reference in New Issue
Block a user