Merge pull request #63412 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-01-04 21:58:36 -08:00 committed by GitHub
commit cf84ba1da1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 422 additions and 145 deletions

View File

@ -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,

View File

@ -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",

View File

@ -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"
] ]

View File

@ -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: (

View File

@ -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"
} }

View File

@ -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:

View File

@ -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(

View File

@ -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),
}
),
)

View File

@ -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"

View File

@ -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"
} }
} }
} }

View File

@ -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"
} }
} }
} }

View File

@ -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"
} }
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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"
} }

View File

@ -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[

View File

@ -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"
} }

View File

@ -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 @@
} }
} }
} }
} }

View File

@ -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():

View File

@ -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",

View File

@ -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[

View File

@ -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]

View File

@ -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(

View File

@ -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

View File

@ -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]"

View File

@ -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

View File

@ -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:

View File

@ -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."""

View File

@ -307,6 +307,7 @@ CPU_SENSOR_PREFIXES = [
"soc_thermal 1", "soc_thermal 1",
"Tctl", "Tctl",
"cpu0-thermal", "cpu0-thermal",
"cpu0_thermal",
] ]

View File

@ -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:

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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: (

View File

@ -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):

View File

@ -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",
}

View File

@ -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)

View File

@ -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