mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 22:57:17 +00:00
Merge pull request #54368 from home-assistant/rc
This commit is contained in:
commit
a21e3aed77
@ -15,7 +15,6 @@ from homeassistant.components.climate.const import (
|
|||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
|
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -166,19 +165,22 @@ class AdvantageAirZone(AdvantageAirClimateEntity):
|
|||||||
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
|
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
@property
|
||||||
"""When entity is added to hass."""
|
def hvac_mode(self):
|
||||||
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
|
"""Return the current state as HVAC mode."""
|
||||||
|
|
||||||
@callback
|
|
||||||
def _update_callback(self) -> None:
|
|
||||||
"""Load data from integration."""
|
|
||||||
self._attr_current_temperature = self._zone["measuredTemp"]
|
|
||||||
self._attr_target_temperature = self._zone["setTemp"]
|
|
||||||
self._attr_hvac_mode = HVAC_MODE_OFF
|
|
||||||
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
|
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
|
||||||
self._attr_hvac_mode = HVAC_MODE_FAN_ONLY
|
return HVAC_MODE_FAN_ONLY
|
||||||
self.async_write_ha_state()
|
return HVAC_MODE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self._zone["measuredTemp"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
"""Return the target temperature."""
|
||||||
|
return self._zone["setTemp"]
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode):
|
async def async_set_hvac_mode(self, hvac_mode):
|
||||||
"""Set the HVAC Mode and State."""
|
"""Set the HVAC Mode and State."""
|
||||||
|
@ -67,8 +67,6 @@ async def async_setup_entry(
|
|||||||
class AgentCamera(MjpegCamera):
|
class AgentCamera(MjpegCamera):
|
||||||
"""Representation of an Agent Device Stream."""
|
"""Representation of an Agent Device Stream."""
|
||||||
|
|
||||||
_attr_supported_features = SUPPORT_ON_OFF
|
|
||||||
|
|
||||||
def __init__(self, device):
|
def __init__(self, device):
|
||||||
"""Initialize as a subclass of MjpegCamera."""
|
"""Initialize as a subclass of MjpegCamera."""
|
||||||
device_info = {
|
device_info = {
|
||||||
@ -80,7 +78,6 @@ class AgentCamera(MjpegCamera):
|
|||||||
self._removed = False
|
self._removed = False
|
||||||
self._attr_name = f"{device.client.name} {device.name}"
|
self._attr_name = f"{device.client.name} {device.name}"
|
||||||
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
|
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
|
||||||
self._attr_should_poll = True
|
|
||||||
super().__init__(device_info)
|
super().__init__(device_info)
|
||||||
self._attr_device_info = {
|
self._attr_device_info = {
|
||||||
"identifiers": {(AGENT_DOMAIN, self.unique_id)},
|
"identifiers": {(AGENT_DOMAIN, self.unique_id)},
|
||||||
@ -102,10 +99,10 @@ class AgentCamera(MjpegCamera):
|
|||||||
if self.device.client.is_available and not self._removed:
|
if self.device.client.is_available and not self._removed:
|
||||||
_LOGGER.error("%s lost", self.name)
|
_LOGGER.error("%s lost", self.name)
|
||||||
self._removed = True
|
self._removed = True
|
||||||
self._attr_available = self.device.client.is_available
|
|
||||||
self._attr_icon = "mdi:camcorder-off"
|
self._attr_icon = "mdi:camcorder-off"
|
||||||
if self.is_on:
|
if self.is_on:
|
||||||
self._attr_icon = "mdi:camcorder"
|
self._attr_icon = "mdi:camcorder"
|
||||||
|
self._attr_available = self.device.client.is_available
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||||
"editable": False,
|
"editable": False,
|
||||||
@ -117,6 +114,11 @@ class AgentCamera(MjpegCamera):
|
|||||||
"alerts_enabled": self.device.alerts_active,
|
"alerts_enabled": self.device.alerts_active,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Update the state periodically."""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self) -> bool:
|
def is_recording(self) -> bool:
|
||||||
"""Return whether the monitor is recording."""
|
"""Return whether the monitor is recording."""
|
||||||
@ -137,6 +139,11 @@ class AgentCamera(MjpegCamera):
|
|||||||
"""Return True if entity is connected."""
|
"""Return True if entity is connected."""
|
||||||
return self.device.connected
|
return self.device.connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Return supported features."""
|
||||||
|
return SUPPORT_ON_OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if on."""
|
"""Return true if on."""
|
||||||
|
@ -111,13 +111,13 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
|||||||
def _fault_callback(self, zone):
|
def _fault_callback(self, zone):
|
||||||
"""Update the zone's state, if needed."""
|
"""Update the zone's state, if needed."""
|
||||||
if zone is None or int(zone) == self._zone_number:
|
if zone is None or int(zone) == self._zone_number:
|
||||||
self._attr_state = 1
|
self._attr_is_on = True
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _restore_callback(self, zone):
|
def _restore_callback(self, zone):
|
||||||
"""Update the zone's state, if needed."""
|
"""Update the zone's state, if needed."""
|
||||||
if zone is None or (int(zone) == self._zone_number and not self._loop):
|
if zone is None or (int(zone) == self._zone_number and not self._loop):
|
||||||
self._attr_state = 0
|
self._attr_is_on = False
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _rfx_message_callback(self, message):
|
def _rfx_message_callback(self, message):
|
||||||
@ -125,7 +125,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
|||||||
if self._rfid and message and message.serial_number == self._rfid:
|
if self._rfid and message and message.serial_number == self._rfid:
|
||||||
rfstate = message.value
|
rfstate = message.value
|
||||||
if self._loop:
|
if self._loop:
|
||||||
self._attr_state = 1 if message.loop[self._loop - 1] else 0
|
self._attr_is_on = bool(message.loop[self._loop - 1])
|
||||||
attr = {CONF_ZONE_NUMBER: self._zone_number}
|
attr = {CONF_ZONE_NUMBER: self._zone_number}
|
||||||
if self._rfid and rfstate is not None:
|
if self._rfid and rfstate is not None:
|
||||||
attr[ATTR_RF_BIT0] = bool(rfstate & 0x01)
|
attr[ATTR_RF_BIT0] = bool(rfstate & 0x01)
|
||||||
@ -150,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
|||||||
message.channel,
|
message.channel,
|
||||||
message.value,
|
message.value,
|
||||||
)
|
)
|
||||||
self._attr_state = message.value
|
self._attr_is_on = bool(message.value)
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@ -93,9 +93,10 @@ class AquaLogicSensor(SensorEntity):
|
|||||||
if panel is not None:
|
if panel is not None:
|
||||||
if panel.is_metric:
|
if panel.is_metric:
|
||||||
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0]
|
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0]
|
||||||
self._attr_state = getattr(panel, self._type)
|
|
||||||
self.async_write_ha_state()
|
|
||||||
else:
|
else:
|
||||||
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1]
|
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1]
|
||||||
|
|
||||||
|
self._attr_state = getattr(panel, self._type)
|
||||||
|
self.async_write_ha_state()
|
||||||
else:
|
else:
|
||||||
self._attr_unit_of_measurement = None
|
self._attr_unit_of_measurement = None
|
||||||
|
@ -14,12 +14,13 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
|
DEVICE_CLASS_ENERGY,
|
||||||
DEVICE_CLASS_POWER,
|
DEVICE_CLASS_POWER,
|
||||||
ENERGY_KILO_WATT_HOUR,
|
ENERGY_KILO_WATT_HOUR,
|
||||||
POWER_WATT,
|
POWER_WATT,
|
||||||
)
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle, dt as dt_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -87,12 +88,16 @@ class AtomeData:
|
|||||||
self._is_connected = None
|
self._is_connected = None
|
||||||
self._day_usage = None
|
self._day_usage = None
|
||||||
self._day_price = None
|
self._day_price = None
|
||||||
|
self._day_last_reset = None
|
||||||
self._week_usage = None
|
self._week_usage = None
|
||||||
self._week_price = None
|
self._week_price = None
|
||||||
|
self._week_last_reset = None
|
||||||
self._month_usage = None
|
self._month_usage = None
|
||||||
self._month_price = None
|
self._month_price = None
|
||||||
|
self._month_last_reset = None
|
||||||
self._year_usage = None
|
self._year_usage = None
|
||||||
self._year_price = None
|
self._year_price = None
|
||||||
|
self._year_last_reset = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def live_power(self):
|
def live_power(self):
|
||||||
@ -137,6 +142,11 @@ class AtomeData:
|
|||||||
"""Return latest daily usage value."""
|
"""Return latest daily usage value."""
|
||||||
return self._day_price
|
return self._day_price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def day_last_reset(self):
|
||||||
|
"""Return latest daily last reset."""
|
||||||
|
return self._day_last_reset
|
||||||
|
|
||||||
@Throttle(DAILY_SCAN_INTERVAL)
|
@Throttle(DAILY_SCAN_INTERVAL)
|
||||||
def update_day_usage(self):
|
def update_day_usage(self):
|
||||||
"""Return current daily power usage."""
|
"""Return current daily power usage."""
|
||||||
@ -144,6 +154,7 @@ class AtomeData:
|
|||||||
values = self.atome_client.get_consumption(DAILY_TYPE)
|
values = self.atome_client.get_consumption(DAILY_TYPE)
|
||||||
self._day_usage = values["total"] / 1000
|
self._day_usage = values["total"] / 1000
|
||||||
self._day_price = values["price"]
|
self._day_price = values["price"]
|
||||||
|
self._day_last_reset = dt_util.parse_datetime(values["startPeriod"])
|
||||||
_LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage)
|
_LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage)
|
||||||
|
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
@ -159,6 +170,11 @@ class AtomeData:
|
|||||||
"""Return latest weekly usage value."""
|
"""Return latest weekly usage value."""
|
||||||
return self._week_price
|
return self._week_price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def week_last_reset(self):
|
||||||
|
"""Return latest weekly last reset value."""
|
||||||
|
return self._week_last_reset
|
||||||
|
|
||||||
@Throttle(WEEKLY_SCAN_INTERVAL)
|
@Throttle(WEEKLY_SCAN_INTERVAL)
|
||||||
def update_week_usage(self):
|
def update_week_usage(self):
|
||||||
"""Return current weekly power usage."""
|
"""Return current weekly power usage."""
|
||||||
@ -166,6 +182,7 @@ class AtomeData:
|
|||||||
values = self.atome_client.get_consumption(WEEKLY_TYPE)
|
values = self.atome_client.get_consumption(WEEKLY_TYPE)
|
||||||
self._week_usage = values["total"] / 1000
|
self._week_usage = values["total"] / 1000
|
||||||
self._week_price = values["price"]
|
self._week_price = values["price"]
|
||||||
|
self._week_last_reset = dt_util.parse_datetime(values["startPeriod"])
|
||||||
_LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage)
|
_LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage)
|
||||||
|
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
@ -181,6 +198,11 @@ class AtomeData:
|
|||||||
"""Return latest monthly usage value."""
|
"""Return latest monthly usage value."""
|
||||||
return self._month_price
|
return self._month_price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def month_last_reset(self):
|
||||||
|
"""Return latest monthly last reset value."""
|
||||||
|
return self._month_last_reset
|
||||||
|
|
||||||
@Throttle(MONTHLY_SCAN_INTERVAL)
|
@Throttle(MONTHLY_SCAN_INTERVAL)
|
||||||
def update_month_usage(self):
|
def update_month_usage(self):
|
||||||
"""Return current monthly power usage."""
|
"""Return current monthly power usage."""
|
||||||
@ -188,6 +210,7 @@ class AtomeData:
|
|||||||
values = self.atome_client.get_consumption(MONTHLY_TYPE)
|
values = self.atome_client.get_consumption(MONTHLY_TYPE)
|
||||||
self._month_usage = values["total"] / 1000
|
self._month_usage = values["total"] / 1000
|
||||||
self._month_price = values["price"]
|
self._month_price = values["price"]
|
||||||
|
self._month_last_reset = dt_util.parse_datetime(values["startPeriod"])
|
||||||
_LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage)
|
_LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage)
|
||||||
|
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
@ -203,6 +226,11 @@ class AtomeData:
|
|||||||
"""Return latest yearly usage value."""
|
"""Return latest yearly usage value."""
|
||||||
return self._year_price
|
return self._year_price
|
||||||
|
|
||||||
|
@property
|
||||||
|
def year_last_reset(self):
|
||||||
|
"""Return latest yearly last reset value."""
|
||||||
|
return self._year_last_reset
|
||||||
|
|
||||||
@Throttle(YEARLY_SCAN_INTERVAL)
|
@Throttle(YEARLY_SCAN_INTERVAL)
|
||||||
def update_year_usage(self):
|
def update_year_usage(self):
|
||||||
"""Return current yearly power usage."""
|
"""Return current yearly power usage."""
|
||||||
@ -210,6 +238,7 @@ class AtomeData:
|
|||||||
values = self.atome_client.get_consumption(YEARLY_TYPE)
|
values = self.atome_client.get_consumption(YEARLY_TYPE)
|
||||||
self._year_usage = values["total"] / 1000
|
self._year_usage = values["total"] / 1000
|
||||||
self._year_price = values["price"]
|
self._year_price = values["price"]
|
||||||
|
self._year_last_reset = dt_util.parse_datetime(values["startPeriod"])
|
||||||
_LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage)
|
_LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage)
|
||||||
|
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
@ -219,19 +248,19 @@ class AtomeData:
|
|||||||
class AtomeSensor(SensorEntity):
|
class AtomeSensor(SensorEntity):
|
||||||
"""Representation of a sensor entity for Atome."""
|
"""Representation of a sensor entity for Atome."""
|
||||||
|
|
||||||
_attr_device_class = DEVICE_CLASS_POWER
|
|
||||||
|
|
||||||
def __init__(self, data, name, sensor_type):
|
def __init__(self, data, name, sensor_type):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
|
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||||
|
|
||||||
if sensor_type == LIVE_TYPE:
|
if sensor_type == LIVE_TYPE:
|
||||||
|
self._attr_device_class = DEVICE_CLASS_POWER
|
||||||
self._attr_unit_of_measurement = POWER_WATT
|
self._attr_unit_of_measurement = POWER_WATT
|
||||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
|
||||||
else:
|
else:
|
||||||
|
self._attr_device_class = DEVICE_CLASS_ENERGY
|
||||||
self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
@ -247,6 +276,9 @@ class AtomeSensor(SensorEntity):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
self._attr_state = getattr(self._data, f"{self._sensor_type}_usage")
|
self._attr_state = getattr(self._data, f"{self._sensor_type}_usage")
|
||||||
|
self._attr_last_reset = dt_util.as_utc(
|
||||||
|
getattr(self._data, f"{self._sensor_type}_last_reset")
|
||||||
|
)
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
"price": getattr(self._data, f"{self._sensor_type}_price")
|
"price": getattr(self._data, f"{self._sensor_type}_price")
|
||||||
}
|
}
|
||||||
|
@ -203,29 +203,33 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
class BluesoundPlayer(MediaPlayerEntity):
|
class BluesoundPlayer(MediaPlayerEntity):
|
||||||
"""Representation of a Bluesound Player."""
|
"""Representation of a Bluesound Player."""
|
||||||
|
|
||||||
_attr_media_content_type = MEDIA_TYPE_MUSIC
|
def __init__(self, hass, host, port=None, name=None, init_callback=None):
|
||||||
|
|
||||||
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
|
|
||||||
"""Initialize the media player."""
|
"""Initialize the media player."""
|
||||||
self.host = host
|
self.host = host
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.port = port
|
self.port = port
|
||||||
self._polling_session = async_get_clientsession(hass)
|
self._polling_session = async_get_clientsession(hass)
|
||||||
self._polling_task = None # The actual polling task.
|
self._polling_task = None # The actual polling task.
|
||||||
self._attr_name = name
|
self._name = name
|
||||||
|
self._icon = None
|
||||||
self._capture_items = []
|
self._capture_items = []
|
||||||
self._services_items = []
|
self._services_items = []
|
||||||
self._preset_items = []
|
self._preset_items = []
|
||||||
self._sync_status = {}
|
self._sync_status = {}
|
||||||
self._status = None
|
self._status = None
|
||||||
self._is_online = None
|
self._last_status_update = None
|
||||||
|
self._is_online = False
|
||||||
self._retry_remove = None
|
self._retry_remove = None
|
||||||
|
self._muted = False
|
||||||
self._master = None
|
self._master = None
|
||||||
self._group_name = None
|
|
||||||
self._bluesound_device_name = None
|
|
||||||
self._is_master = False
|
self._is_master = False
|
||||||
|
self._group_name = None
|
||||||
self._group_list = []
|
self._group_list = []
|
||||||
|
self._bluesound_device_name = None
|
||||||
|
|
||||||
self._init_callback = init_callback
|
self._init_callback = init_callback
|
||||||
|
if self.port is None:
|
||||||
|
self.port = DEFAULT_PORT
|
||||||
|
|
||||||
class _TimeoutException(Exception):
|
class _TimeoutException(Exception):
|
||||||
pass
|
pass
|
||||||
@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
return None
|
return None
|
||||||
self._sync_status = resp["SyncStatus"].copy()
|
self._sync_status = resp["SyncStatus"].copy()
|
||||||
|
|
||||||
if not self.name:
|
if not self._name:
|
||||||
self._attr_name = self._sync_status.get("@name", self.host)
|
self._name = self._sync_status.get("@name", self.host)
|
||||||
if not self._bluesound_device_name:
|
if not self._bluesound_device_name:
|
||||||
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
||||||
if not self.icon:
|
if not self._icon:
|
||||||
self._attr_icon = self._sync_status.get("@icon", self.host)
|
self._icon = self._sync_status.get("@icon", self.host)
|
||||||
|
|
||||||
master = self._sync_status.get("master")
|
master = self._sync_status.get("master")
|
||||||
if master is not None:
|
if master is not None:
|
||||||
@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
await self.async_update_status()
|
await self.async_update_status()
|
||||||
|
|
||||||
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
|
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
|
||||||
_LOGGER.info("Node %s is offline, retrying later", self.name)
|
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||||
self.start_polling()
|
self.start_polling()
|
||||||
|
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
_LOGGER.debug("Stopping the polling of node %s", self.name)
|
_LOGGER.debug("Stopping the polling of node %s", self._name)
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected error in %s", self.name)
|
_LOGGER.exception("Unexpected error in %s", self._name)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def start_polling(self):
|
def start_polling(self):
|
||||||
@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
if response.status == HTTP_OK:
|
if response.status == HTTP_OK:
|
||||||
result = await response.text()
|
result = await response.text()
|
||||||
self._is_online = True
|
self._is_online = True
|
||||||
self._attr_media_position_updated_at = dt_util.utcnow()
|
self._last_status_update = dt_util.utcnow()
|
||||||
self._status = xmltodict.parse(result)["status"].copy()
|
self._status = xmltodict.parse(result)["status"].copy()
|
||||||
|
|
||||||
group_name = self._status.get("groupName")
|
group_name = self._status.get("groupName")
|
||||||
@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
except (asyncio.TimeoutError, ClientError):
|
except (asyncio.TimeoutError, ClientError):
|
||||||
self._is_online = False
|
self._is_online = False
|
||||||
self._attr_media_position_updated_at = None
|
self._last_status_update = None
|
||||||
self._status = None
|
self._status = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
_LOGGER.info("Client connection error, marking %s as offline", self.name)
|
_LOGGER.info("Client connection error, marking %s as offline", self._name)
|
||||||
raise
|
raise
|
||||||
self.update_state_attr()
|
|
||||||
|
|
||||||
def update_state_attr(self):
|
|
||||||
"""Update state attributes."""
|
|
||||||
if self._status is None:
|
|
||||||
self._attr_state = STATE_OFF
|
|
||||||
self._attr_supported_features = 0
|
|
||||||
elif self.is_grouped and not self.is_master:
|
|
||||||
self._attr_state = STATE_GROUPED
|
|
||||||
self._attr_supported_features = (
|
|
||||||
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
status = self._status.get("state")
|
|
||||||
self._attr_state = STATE_IDLE
|
|
||||||
if status in ("pause", "stop"):
|
|
||||||
self._attr_state = STATE_PAUSED
|
|
||||||
elif status in ("stream", "play"):
|
|
||||||
self._attr_state = STATE_PLAYING
|
|
||||||
supported = SUPPORT_CLEAR_PLAYLIST
|
|
||||||
if self._status.get("indexing", "0") == "0":
|
|
||||||
supported = (
|
|
||||||
supported
|
|
||||||
| SUPPORT_PAUSE
|
|
||||||
| SUPPORT_PREVIOUS_TRACK
|
|
||||||
| SUPPORT_NEXT_TRACK
|
|
||||||
| SUPPORT_PLAY_MEDIA
|
|
||||||
| SUPPORT_STOP
|
|
||||||
| SUPPORT_PLAY
|
|
||||||
| SUPPORT_SELECT_SOURCE
|
|
||||||
| SUPPORT_SHUFFLE_SET
|
|
||||||
)
|
|
||||||
if self.volume_level is not None and self.volume_level >= 0:
|
|
||||||
supported = (
|
|
||||||
supported
|
|
||||||
| SUPPORT_VOLUME_STEP
|
|
||||||
| SUPPORT_VOLUME_SET
|
|
||||||
| SUPPORT_VOLUME_MUTE
|
|
||||||
)
|
|
||||||
if self._status.get("canSeek", "") == "1":
|
|
||||||
supported = supported | SUPPORT_SEEK
|
|
||||||
self._attr_supported_features = supported
|
|
||||||
self._attr_extra_state_attributes = {}
|
|
||||||
if self._group_list:
|
|
||||||
self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
|
||||||
self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master
|
|
||||||
self._attr_shuffle = self._status.get("shuffle", "0") == "1"
|
|
||||||
|
|
||||||
async def async_trigger_sync_on_all(self):
|
async def async_trigger_sync_on_all(self):
|
||||||
"""Trigger sync status update on all devices."""
|
"""Trigger sync status update on all devices."""
|
||||||
@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
return self._services_items
|
return self._services_items
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
"""Content type of current playing media."""
|
||||||
|
return MEDIA_TYPE_MUSIC
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self._status is None:
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
if self.is_grouped and not self.is_master:
|
||||||
|
return STATE_GROUPED
|
||||||
|
|
||||||
|
status = self._status.get("state")
|
||||||
|
if status in ("pause", "stop"):
|
||||||
|
return STATE_PAUSED
|
||||||
|
if status in ("stream", "play"):
|
||||||
|
return STATE_PLAYING
|
||||||
|
return STATE_IDLE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
mediastate = self.state
|
mediastate = self.state
|
||||||
if self.media_position_updated_at is None or mediastate == STATE_IDLE:
|
if self._last_status_update is None or mediastate == STATE_IDLE:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
position = self._status.get("secs")
|
position = self._status.get("secs")
|
||||||
@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
|
|
||||||
position = float(position)
|
position = float(position)
|
||||||
if mediastate == STATE_PLAYING:
|
if mediastate == STATE_PLAYING:
|
||||||
position += (
|
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||||
dt_util.utcnow() - self.media_position_updated_at
|
|
||||||
).total_seconds()
|
|
||||||
|
|
||||||
return position
|
return position
|
||||||
|
|
||||||
@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
return None
|
return None
|
||||||
return float(duration)
|
return float(duration)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""Last time status was updated."""
|
||||||
|
return self._last_status_update
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
mute = bool(int(mute))
|
mute = bool(int(mute))
|
||||||
return mute
|
return mute
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bluesound_device_name(self):
|
def bluesound_device_name(self):
|
||||||
"""Return the device name as returned by the device."""
|
"""Return the device name as returned by the device."""
|
||||||
return self._bluesound_device_name
|
return self._bluesound_device_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon of the device."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
"""List of available input sources."""
|
"""List of available input sources."""
|
||||||
@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_master(self) -> bool:
|
def supported_features(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
if self._status is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if self.is_grouped and not self.is_master:
|
||||||
|
return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||||
|
|
||||||
|
supported = SUPPORT_CLEAR_PLAYLIST
|
||||||
|
|
||||||
|
if self._status.get("indexing", "0") == "0":
|
||||||
|
supported = (
|
||||||
|
supported
|
||||||
|
| SUPPORT_PAUSE
|
||||||
|
| SUPPORT_PREVIOUS_TRACK
|
||||||
|
| SUPPORT_NEXT_TRACK
|
||||||
|
| SUPPORT_PLAY_MEDIA
|
||||||
|
| SUPPORT_STOP
|
||||||
|
| SUPPORT_PLAY
|
||||||
|
| SUPPORT_SELECT_SOURCE
|
||||||
|
| SUPPORT_SHUFFLE_SET
|
||||||
|
)
|
||||||
|
|
||||||
|
current_vol = self.volume_level
|
||||||
|
if current_vol is not None and current_vol >= 0:
|
||||||
|
supported = (
|
||||||
|
supported
|
||||||
|
| SUPPORT_VOLUME_STEP
|
||||||
|
| SUPPORT_VOLUME_SET
|
||||||
|
| SUPPORT_VOLUME_MUTE
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._status.get("canSeek", "") == "1":
|
||||||
|
supported = supported | SUPPORT_SEEK
|
||||||
|
|
||||||
|
return supported
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_master(self):
|
||||||
"""Return true if player is a coordinator."""
|
"""Return true if player is a coordinator."""
|
||||||
return self._is_master
|
return self._is_master
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_grouped(self) -> bool:
|
def is_grouped(self):
|
||||||
"""Return true if player is a coordinator."""
|
"""Return true if player is a coordinator."""
|
||||||
return self._master is not None or self._is_master
|
return self._master is not None or self._is_master
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shuffle(self):
|
||||||
|
"""Return true if shuffle is active."""
|
||||||
|
return self._status.get("shuffle", "0") == "1"
|
||||||
|
|
||||||
async def async_join(self, master):
|
async def async_join(self, master):
|
||||||
"""Join the player to a group."""
|
"""Join the player to a group."""
|
||||||
master_device = [
|
master_device = [
|
||||||
@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity):
|
|||||||
else:
|
else:
|
||||||
_LOGGER.error("Master not found %s", master_device)
|
_LOGGER.error("Master not found %s", master_device)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self):
|
||||||
|
"""List members in group."""
|
||||||
|
attributes = {}
|
||||||
|
if self._group_list:
|
||||||
|
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
||||||
|
|
||||||
|
attributes[ATTR_MASTER] = self._is_master
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
def rebuild_bluesound_group(self):
|
def rebuild_bluesound_group(self):
|
||||||
"""Rebuild the list of entities in speaker group."""
|
"""Rebuild the list of entities in speaker group."""
|
||||||
if self._group_name is None:
|
if self._group_name is None:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "bmw_connected_drive",
|
"domain": "bmw_connected_drive",
|
||||||
"name": "BMW Connected Drive",
|
"name": "BMW Connected Drive",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"requirements": ["bimmer_connected==0.7.16"],
|
"requirements": ["bimmer_connected==0.7.18"],
|
||||||
"codeowners": ["@gerard33", "@rikroe"],
|
"codeowners": ["@gerard33", "@rikroe"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
|
@ -66,7 +66,7 @@ DEFAULT_FORECAST_TYPE = DAILY
|
|||||||
DOMAIN = "climacell"
|
DOMAIN = "climacell"
|
||||||
ATTRIBUTION = "Powered by ClimaCell"
|
ATTRIBUTION = "Powered by ClimaCell"
|
||||||
|
|
||||||
MAX_REQUESTS_PER_DAY = 500
|
MAX_REQUESTS_PER_DAY = 100
|
||||||
|
|
||||||
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
|
CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "cloud",
|
"domain": "cloud",
|
||||||
"name": "Home Assistant Cloud",
|
"name": "Home Assistant Cloud",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||||
"requirements": ["hass-nabucasa==0.44.0"],
|
"requirements": ["hass-nabucasa==0.45.1"],
|
||||||
"dependencies": ["http", "webhook"],
|
"dependencies": ["http", "webhook"],
|
||||||
"after_dependencies": ["google_assistant", "alexa"],
|
"after_dependencies": ["google_assistant", "alexa"],
|
||||||
"codeowners": ["@home-assistant/cloud"],
|
"codeowners": ["@home-assistant/cloud"],
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": ["home-assistant-frontend==20210804.0"],
|
"requirements": [
|
||||||
|
"home-assistant-frontend==20210809.0"
|
||||||
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
"auth",
|
"auth",
|
||||||
@ -15,6 +17,8 @@
|
|||||||
"system_log",
|
"system_log",
|
||||||
"websocket_api"
|
"websocket_api"
|
||||||
],
|
],
|
||||||
"codeowners": ["@home-assistant/frontend"],
|
"codeowners": [
|
||||||
|
"@home-assistant/frontend"
|
||||||
|
],
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
@ -63,12 +63,19 @@ def async_setup_forwarded(
|
|||||||
an HTTP 400 status code is thrown.
|
an HTTP 400 status code is thrown.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
|
||||||
|
except ImportError:
|
||||||
|
remote = None
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def forwarded_middleware(
|
async def forwarded_middleware(
|
||||||
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
|
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
|
||||||
) -> StreamResponse:
|
) -> StreamResponse:
|
||||||
"""Process forwarded data by a reverse proxy."""
|
"""Process forwarded data by a reverse proxy."""
|
||||||
overrides: dict[str, str] = {}
|
# Skip requests from Remote UI
|
||||||
|
if remote is not None and remote.is_cloud_request.get():
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
# Handle X-Forwarded-For
|
# Handle X-Forwarded-For
|
||||||
forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, [])
|
forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, [])
|
||||||
@ -120,6 +127,8 @@ def async_setup_forwarded(
|
|||||||
)
|
)
|
||||||
raise HTTPBadRequest from err
|
raise HTTPBadRequest from err
|
||||||
|
|
||||||
|
overrides: dict[str, str] = {}
|
||||||
|
|
||||||
# Find the last trusted index in the X-Forwarded-For list
|
# Find the last trusted index in the X-Forwarded-For list
|
||||||
forwarded_for_index = 0
|
forwarded_for_index = 0
|
||||||
for forwarded_ip in forwarded_for:
|
for forwarded_ip in forwarded_for:
|
||||||
|
@ -71,7 +71,7 @@ class ShadeEntity(HDEntity):
|
|||||||
"name": self._shade_name,
|
"name": self._shade_name,
|
||||||
"suggested_area": self._room_name,
|
"suggested_area": self._room_name,
|
||||||
"manufacturer": MANUFACTURER,
|
"manufacturer": MANUFACTURER,
|
||||||
"model": self._shade.raw_data[ATTR_TYPE],
|
"model": str(self._shade.raw_data[ATTR_TYPE]),
|
||||||
"via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]),
|
"via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,6 +145,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
|||||||
)
|
)
|
||||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||||
|
|
||||||
|
self._unit_of_measurement = state.attributes.get(
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def calc_integration(event):
|
def calc_integration(event):
|
||||||
"""Handle the sensor state changes."""
|
"""Handle the sensor state changes."""
|
||||||
|
@ -141,9 +141,9 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
|
|||||||
self._poolid = self.coordinator.data[poolidx]["id"]
|
self._poolid = self.coordinator.data[poolidx]["id"]
|
||||||
|
|
||||||
pooldata = self._pooldata()
|
pooldata = self._pooldata()
|
||||||
self._unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}"
|
self._attr_unique_id = f"{pooldata['ICO']['serial_number']}-{description.key}"
|
||||||
self._device_name = pooldata["name"]
|
self._device_name = pooldata["name"]
|
||||||
self._name = f"{self._device_name} {description.name}"
|
self._attr_name = f"{self._device_name} {description.name}"
|
||||||
|
|
||||||
def _pooldata(self):
|
def _pooldata(self):
|
||||||
"""Get pool data dict."""
|
"""Get pool data dict."""
|
||||||
@ -168,11 +168,6 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
|
|||||||
"""Last value of the sensor."""
|
"""Last value of the sensor."""
|
||||||
return self._devdata()["value"]
|
return self._devdata()["value"]
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return the unique ID of this entity."""
|
|
||||||
return self._unique_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return the device info for the sensor."""
|
"""Return the device info for the sensor."""
|
||||||
|
@ -431,7 +431,7 @@ class SimpliSafeEntity(CoordinatorEntity):
|
|||||||
self._attr_device_info = {
|
self._attr_device_info = {
|
||||||
"identifiers": {(DOMAIN, system.system_id)},
|
"identifiers": {(DOMAIN, system.system_id)},
|
||||||
"manufacturer": "SimpliSafe",
|
"manufacturer": "SimpliSafe",
|
||||||
"model": system.version,
|
"model": str(system.version),
|
||||||
"name": name,
|
"name": name,
|
||||||
"via_device": (DOMAIN, system.serial),
|
"via_device": (DOMAIN, system.serial),
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Sonos",
|
"name": "Sonos",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||||
"requirements": ["soco==0.23.2"],
|
"requirements": ["soco==0.23.3"],
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"after_dependencies": ["plex", "zeroconf"],
|
"after_dependencies": ["plex", "zeroconf"],
|
||||||
"zeroconf": ["_sonos._tcp.local."],
|
"zeroconf": ["_sonos._tcp.local."],
|
||||||
|
@ -496,9 +496,7 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
self.async_write_entity_states()
|
self.async_write_entity_states()
|
||||||
|
|
||||||
async def async_unseen(
|
async def async_unseen(self, now: datetime.datetime | None = None) -> None:
|
||||||
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""Make this player unavailable when it was not seen recently."""
|
"""Make this player unavailable when it was not seen recently."""
|
||||||
if self._seen_timer:
|
if self._seen_timer:
|
||||||
self._seen_timer()
|
self._seen_timer()
|
||||||
@ -527,9 +525,8 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
await self.async_unsubscribe()
|
await self.async_unsubscribe()
|
||||||
|
|
||||||
if not will_reconnect:
|
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
|
||||||
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
|
self.async_write_entity_states()
|
||||||
self.async_write_entity_states()
|
|
||||||
|
|
||||||
async def async_rebooted(self, soco: SoCo) -> None:
|
async def async_rebooted(self, soco: SoCo) -> None:
|
||||||
"""Handle a detected speaker reboot."""
|
"""Handle a detected speaker reboot."""
|
||||||
@ -538,8 +535,24 @@ class SonosSpeaker:
|
|||||||
self.zone_name,
|
self.zone_name,
|
||||||
soco,
|
soco,
|
||||||
)
|
)
|
||||||
await self.async_unseen(will_reconnect=True)
|
await self.async_unsubscribe()
|
||||||
await self.async_seen(soco)
|
self.soco = soco
|
||||||
|
await self.async_subscribe()
|
||||||
|
if self._seen_timer:
|
||||||
|
self._seen_timer()
|
||||||
|
self._seen_timer = self.hass.helpers.event.async_call_later(
|
||||||
|
SEEN_EXPIRE_TIME.total_seconds(), self.async_unseen
|
||||||
|
)
|
||||||
|
if not self._poll_timer:
|
||||||
|
self._poll_timer = self.hass.helpers.event.async_track_time_interval(
|
||||||
|
partial(
|
||||||
|
async_dispatcher_send,
|
||||||
|
self.hass,
|
||||||
|
f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
|
||||||
|
),
|
||||||
|
SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
self.async_write_entity_states()
|
||||||
|
|
||||||
#
|
#
|
||||||
# Battery management
|
# Battery management
|
||||||
|
@ -615,6 +615,10 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
result = await self._try_command(
|
||||||
|
"Turning the miio device on failed.", self._device.on
|
||||||
|
)
|
||||||
|
|
||||||
# Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented
|
# Remove the async_set_speed call is async_set_percentage and async_set_preset_modes have been implemented
|
||||||
if speed:
|
if speed:
|
||||||
await self.async_set_speed(speed)
|
await self.async_set_speed(speed)
|
||||||
@ -623,10 +627,6 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
|
|||||||
await self.async_set_percentage(percentage)
|
await self.async_set_percentage(percentage)
|
||||||
if preset_mode:
|
if preset_mode:
|
||||||
await self.async_set_preset_mode(preset_mode)
|
await self.async_set_preset_mode(preset_mode)
|
||||||
else:
|
|
||||||
result = await self._try_command(
|
|
||||||
"Turning the miio device on failed.", self._device.on
|
|
||||||
)
|
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self._state = True
|
self._state = True
|
||||||
@ -1138,6 +1138,7 @@ class XiaomiAirFresh(XiaomiGenericDevice):
|
|||||||
self._speed_list = OPERATION_MODES_AIRFRESH
|
self._speed_list = OPERATION_MODES_AIRFRESH
|
||||||
self._speed_count = 4
|
self._speed_count = 4
|
||||||
self._preset_modes = PRESET_MODES_AIRFRESH
|
self._preset_modes = PRESET_MODES_AIRFRESH
|
||||||
|
self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
|
||||||
self._state_attrs.update(
|
self._state_attrs.update(
|
||||||
{attribute: None for attribute in self._available_attributes}
|
{attribute: None for attribute in self._available_attributes}
|
||||||
)
|
)
|
||||||
|
@ -142,7 +142,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|||||||
zc_args: dict = {}
|
zc_args: dict = {}
|
||||||
|
|
||||||
adapters = await network.async_get_adapters(hass)
|
adapters = await network.async_get_adapters(hass)
|
||||||
if _async_use_default_interface(adapters):
|
|
||||||
|
ipv6 = True
|
||||||
|
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
||||||
|
ipv6 = False
|
||||||
|
zc_args["ip_version"] = IPVersion.V4Only
|
||||||
|
else:
|
||||||
|
zc_args["ip_version"] = IPVersion.All
|
||||||
|
|
||||||
|
if not ipv6 and _async_use_default_interface(adapters):
|
||||||
zc_args["interfaces"] = InterfaceChoice.Default
|
zc_args["interfaces"] = InterfaceChoice.Default
|
||||||
else:
|
else:
|
||||||
interfaces = zc_args["interfaces"] = []
|
interfaces = zc_args["interfaces"] = []
|
||||||
@ -158,13 +166,6 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
|||||||
if adapter["ipv6"] and adapter["index"] not in interfaces:
|
if adapter["ipv6"] and adapter["index"] not in interfaces:
|
||||||
interfaces.append(adapter["index"])
|
interfaces.append(adapter["index"])
|
||||||
|
|
||||||
ipv6 = True
|
|
||||||
if not any(adapter["enabled"] and adapter["ipv6"] for adapter in adapters):
|
|
||||||
ipv6 = False
|
|
||||||
zc_args["ip_version"] = IPVersion.V4Only
|
|
||||||
else:
|
|
||||||
zc_args["ip_version"] = IPVersion.All
|
|
||||||
|
|
||||||
aio_zc = await _async_get_instance(hass, **zc_args)
|
aio_zc = await _async_get_instance(hass, **zc_args)
|
||||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||||
zeroconf_types, homekit_models = await asyncio.gather(
|
zeroconf_types, homekit_models = await asyncio.gather(
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "zeroconf",
|
"domain": "zeroconf",
|
||||||
"name": "Zero-configuration networking (zeroconf)",
|
"name": "Zero-configuration networking (zeroconf)",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||||
"requirements": ["zeroconf==0.33.4"],
|
"requirements": ["zeroconf==0.34.3"],
|
||||||
"dependencies": ["network", "api"],
|
"dependencies": ["network", "api"],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
|
@ -301,7 +301,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
|||||||
# fallback to setting the color(s) one by one if multicolor fails
|
# fallback to setting the color(s) one by one if multicolor fails
|
||||||
# not sure this is needed at all, but just in case
|
# not sure this is needed at all, but just in case
|
||||||
for color, value in colors.items():
|
for color, value in colors.items():
|
||||||
await self._async_set_color(color, value, zwave_transition)
|
await self._async_set_color(color, value)
|
||||||
|
|
||||||
async def _async_set_color(
|
async def _async_set_color(
|
||||||
self,
|
self,
|
||||||
|
@ -5,7 +5,7 @@ from typing import Final
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2021
|
MAJOR_VERSION: Final = 2021
|
||||||
MINOR_VERSION: Final = 8
|
MINOR_VERSION: Final = 8
|
||||||
PATCH_VERSION: Final = "4"
|
PATCH_VERSION: Final = "5"
|
||||||
__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)
|
||||||
|
@ -16,8 +16,8 @@ cryptography==3.3.2
|
|||||||
defusedxml==0.7.1
|
defusedxml==0.7.1
|
||||||
distro==1.5.0
|
distro==1.5.0
|
||||||
emoji==1.2.0
|
emoji==1.2.0
|
||||||
hass-nabucasa==0.44.0
|
hass-nabucasa==0.45.1
|
||||||
home-assistant-frontend==20210804.0
|
home-assistant-frontend==20210809.0
|
||||||
httpx==0.18.2
|
httpx==0.18.2
|
||||||
ifaddr==0.1.7
|
ifaddr==0.1.7
|
||||||
jinja2==3.0.1
|
jinja2==3.0.1
|
||||||
@ -33,7 +33,7 @@ sqlalchemy==1.4.17
|
|||||||
voluptuous-serialize==2.4.0
|
voluptuous-serialize==2.4.0
|
||||||
voluptuous==0.12.1
|
voluptuous==0.12.1
|
||||||
yarl==1.6.3
|
yarl==1.6.3
|
||||||
zeroconf==0.33.4
|
zeroconf==0.34.3
|
||||||
|
|
||||||
pycryptodome>=3.6.6
|
pycryptodome>=3.6.6
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@ beautifulsoup4==4.9.3
|
|||||||
bellows==0.26.0
|
bellows==0.26.0
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer_connected==0.7.16
|
bimmer_connected==0.7.18
|
||||||
|
|
||||||
# homeassistant.components.bizkaibus
|
# homeassistant.components.bizkaibus
|
||||||
bizkaibus==0.1.1
|
bizkaibus==0.1.1
|
||||||
@ -750,7 +750,7 @@ habitipy==0.2.0
|
|||||||
hangups==0.4.14
|
hangups==0.4.14
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.44.0
|
hass-nabucasa==0.45.1
|
||||||
|
|
||||||
# homeassistant.components.splunk
|
# homeassistant.components.splunk
|
||||||
hass_splunk==0.1.1
|
hass_splunk==0.1.1
|
||||||
@ -783,7 +783,7 @@ hole==0.5.1
|
|||||||
holidays==0.11.2
|
holidays==0.11.2
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20210804.0
|
home-assistant-frontend==20210809.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -2152,7 +2152,7 @@ smhi-pkg==1.0.15
|
|||||||
snapcast==2.1.3
|
snapcast==2.1.3
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
soco==0.23.2
|
soco==0.23.3
|
||||||
|
|
||||||
# homeassistant.components.solaredge_local
|
# homeassistant.components.solaredge_local
|
||||||
solaredge-local==0.2.0
|
solaredge-local==0.2.0
|
||||||
@ -2439,7 +2439,7 @@ zeep[async]==4.0.0
|
|||||||
zengge==0.2
|
zengge==0.2
|
||||||
|
|
||||||
# homeassistant.components.zeroconf
|
# homeassistant.components.zeroconf
|
||||||
zeroconf==0.33.4
|
zeroconf==0.34.3
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zha-quirks==0.0.59
|
zha-quirks==0.0.59
|
||||||
|
@ -220,7 +220,7 @@ base36==0.1.1
|
|||||||
bellows==0.26.0
|
bellows==0.26.0
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer_connected==0.7.16
|
bimmer_connected==0.7.18
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox_uniapi==1.3.3
|
blebox_uniapi==1.3.3
|
||||||
@ -428,7 +428,7 @@ habitipy==0.2.0
|
|||||||
hangups==0.4.14
|
hangups==0.4.14
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.44.0
|
hass-nabucasa==0.45.1
|
||||||
|
|
||||||
# homeassistant.components.tasmota
|
# homeassistant.components.tasmota
|
||||||
hatasmota==0.2.20
|
hatasmota==0.2.20
|
||||||
@ -449,7 +449,7 @@ hole==0.5.1
|
|||||||
holidays==0.11.2
|
holidays==0.11.2
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20210804.0
|
home-assistant-frontend==20210809.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -1177,7 +1177,7 @@ smarthab==0.21
|
|||||||
smhi-pkg==1.0.15
|
smhi-pkg==1.0.15
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
soco==0.23.2
|
soco==0.23.3
|
||||||
|
|
||||||
# homeassistant.components.solaredge
|
# homeassistant.components.solaredge
|
||||||
solaredge==0.0.2
|
solaredge==0.0.2
|
||||||
@ -1341,7 +1341,7 @@ youless-api==0.10
|
|||||||
zeep[async]==4.0.0
|
zeep[async]==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.zeroconf
|
# homeassistant.components.zeroconf
|
||||||
zeroconf==0.33.4
|
zeroconf==0.34.3
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zha-quirks==0.0.59
|
zha-quirks==0.0.59
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test real forwarded middleware."""
|
"""Test real forwarded middleware."""
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
|
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
|
||||||
@ -441,3 +442,22 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog):
|
|||||||
|
|
||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
assert "Empty value received in X-Forward-Host header" in caplog.text
|
assert "Empty value received in X-Forward-Host header" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_x_forwarded_cloud(aiohttp_client, caplog):
|
||||||
|
"""Test that cloud requests are not processed."""
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get("/", mock_handler)
|
||||||
|
async_setup_forwarded(app, True, [ip_network("127.0.0.1")])
|
||||||
|
|
||||||
|
mock_api_client = await aiohttp_client(app)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True))
|
||||||
|
):
|
||||||
|
resp = await mock_api_client.get(
|
||||||
|
"/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
# This request would normally fail because it's invalid, now it works.
|
||||||
|
assert resp.status == 200
|
||||||
|
@ -73,6 +73,7 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
|||||||
{
|
{
|
||||||
"last_reset": "2019-10-06T21:00:00",
|
"last_reset": "2019-10-06T21:00:00",
|
||||||
"device_class": DEVICE_CLASS_ENERGY,
|
"device_class": DEVICE_CLASS_ENERGY,
|
||||||
|
"unit_of_measurement": ENERGY_KILO_WATT_HOUR,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -794,11 +794,6 @@ async def test_async_detect_interfaces_setting_empty_route(hass, mock_async_zero
|
|||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||||
side_effect=get_service_info_mock,
|
side_effect=get_service_info_mock,
|
||||||
), patch(
|
|
||||||
"socket.if_nametoindex",
|
|
||||||
side_effect=lambda iface: {"eth0": 1, "eth1": 2, "eth2": 3, "vtun0": 4}.get(
|
|
||||||
iface, 0
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
@ -827,3 +822,45 @@ async def test_get_announced_addresses(hass, mock_async_zeroconf):
|
|||||||
first_ip = ip_address("192.168.1.5").packed
|
first_ip = ip_address("192.168.1.5").packed
|
||||||
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
|
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
|
||||||
assert actual[0] == first_ip and set(actual) == expected
|
assert actual[0] == first_ip and set(actual) == expected
|
||||||
|
|
||||||
|
|
||||||
|
_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6 = [
|
||||||
|
{
|
||||||
|
"auto": True,
|
||||||
|
"default": True,
|
||||||
|
"enabled": True,
|
||||||
|
"index": 1,
|
||||||
|
"ipv4": [{"address": "192.168.1.5", "network_prefix": 23}],
|
||||||
|
"ipv6": [
|
||||||
|
{
|
||||||
|
"address": "fe80::dead:beef:dead:beef",
|
||||||
|
"network_prefix": 64,
|
||||||
|
"flowinfo": 1,
|
||||||
|
"scope_id": 3,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "eth1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_detect_interfaces_explicitly_set_ipv6(hass, mock_async_zeroconf):
|
||||||
|
"""Test interfaces are explicitly set when IPv6 is present."""
|
||||||
|
with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object(
|
||||||
|
hass.config_entries.flow, "async_init"
|
||||||
|
), patch.object(
|
||||||
|
zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.zeroconf.network.async_get_adapters",
|
||||||
|
return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||||
|
side_effect=get_service_info_mock,
|
||||||
|
):
|
||||||
|
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_zc.mock_calls[0] == call(
|
||||||
|
interfaces=["192.168.1.5", 1], ip_version=IPVersion.All
|
||||||
|
)
|
||||||
|
@ -1021,8 +1021,7 @@ async def test_multicast_set_value_options(
|
|||||||
],
|
],
|
||||||
ATTR_COMMAND_CLASS: 51,
|
ATTR_COMMAND_CLASS: 51,
|
||||||
ATTR_PROPERTY: "targetColor",
|
ATTR_PROPERTY: "targetColor",
|
||||||
ATTR_PROPERTY_KEY: 2,
|
ATTR_VALUE: '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }',
|
||||||
ATTR_VALUE: 2,
|
|
||||||
ATTR_OPTIONS: {"transitionDuration": 1},
|
ATTR_OPTIONS: {"transitionDuration": 1},
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
@ -1038,9 +1037,11 @@ async def test_multicast_set_value_options(
|
|||||||
assert args["valueId"] == {
|
assert args["valueId"] == {
|
||||||
"commandClass": 51,
|
"commandClass": 51,
|
||||||
"property": "targetColor",
|
"property": "targetColor",
|
||||||
"propertyKey": 2,
|
|
||||||
}
|
}
|
||||||
assert args["value"] == 2
|
assert (
|
||||||
|
args["value"]
|
||||||
|
== '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }'
|
||||||
|
)
|
||||||
assert args["options"] == {"transitionDuration": 1}
|
assert args["options"] == {"transitionDuration": 1}
|
||||||
|
|
||||||
client.async_send_command.reset_mock()
|
client.async_send_command.reset_mock()
|
||||||
|
@ -267,8 +267,7 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 255,
|
"max": 255,
|
||||||
"label": "Target value (Warm White)",
|
"label": "Target value (Warm White)",
|
||||||
"description": "The target value of the Warm White color.",
|
"description": "The target value of the Warm White color."
|
||||||
"valueChangeOptions": ["transitionDuration"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -286,8 +285,7 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 255,
|
"max": 255,
|
||||||
"label": "Target value (Cold White)",
|
"label": "Target value (Cold White)",
|
||||||
"description": "The target value of the Cold White color.",
|
"description": "The target value of the Cold White color."
|
||||||
"valueChangeOptions": ["transitionDuration"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -305,8 +303,7 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 255,
|
"max": 255,
|
||||||
"label": "Target value (Red)",
|
"label": "Target value (Red)",
|
||||||
"description": "The target value of the Red color.",
|
"description": "The target value of the Red color."
|
||||||
"valueChangeOptions": ["transitionDuration"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -324,8 +321,7 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 255,
|
"max": 255,
|
||||||
"label": "Target value (Green)",
|
"label": "Target value (Green)",
|
||||||
"description": "The target value of the Green color.",
|
"description": "The target value of the Green color."
|
||||||
"valueChangeOptions": ["transitionDuration"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -343,8 +339,7 @@
|
|||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 255,
|
"max": 255,
|
||||||
"label": "Target value (Blue)",
|
"label": "Target value (Blue)",
|
||||||
"description": "The target value of the Blue color.",
|
"description": "The target value of the Blue color."
|
||||||
"valueChangeOptions": ["transitionDuration"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user