mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity_platform
|
||||
|
||||
from .const import (
|
||||
@ -166,19 +165,22 @@ class AdvantageAirZone(AdvantageAirClimateEntity):
|
||||
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
|
||||
|
||||
@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
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return the current state as HVAC mode."""
|
||||
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
|
||||
self._attr_hvac_mode = HVAC_MODE_FAN_ONLY
|
||||
self.async_write_ha_state()
|
||||
return HVAC_MODE_FAN_ONLY
|
||||
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):
|
||||
"""Set the HVAC Mode and State."""
|
||||
|
@ -67,8 +67,6 @@ async def async_setup_entry(
|
||||
class AgentCamera(MjpegCamera):
|
||||
"""Representation of an Agent Device Stream."""
|
||||
|
||||
_attr_supported_features = SUPPORT_ON_OFF
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
device_info = {
|
||||
@ -80,7 +78,6 @@ class AgentCamera(MjpegCamera):
|
||||
self._removed = False
|
||||
self._attr_name = f"{device.client.name} {device.name}"
|
||||
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
|
||||
self._attr_should_poll = True
|
||||
super().__init__(device_info)
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(AGENT_DOMAIN, self.unique_id)},
|
||||
@ -102,10 +99,10 @@ class AgentCamera(MjpegCamera):
|
||||
if self.device.client.is_available and not self._removed:
|
||||
_LOGGER.error("%s lost", self.name)
|
||||
self._removed = True
|
||||
self._attr_available = self.device.client.is_available
|
||||
self._attr_icon = "mdi:camcorder-off"
|
||||
if self.is_on:
|
||||
self._attr_icon = "mdi:camcorder"
|
||||
self._attr_available = self.device.client.is_available
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
"editable": False,
|
||||
@ -117,6 +114,11 @@ class AgentCamera(MjpegCamera):
|
||||
"alerts_enabled": self.device.alerts_active,
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Update the state periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
"""Return whether the monitor is recording."""
|
||||
@ -137,6 +139,11 @@ class AgentCamera(MjpegCamera):
|
||||
"""Return True if entity is connected."""
|
||||
return self.device.connected
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return supported features."""
|
||||
return SUPPORT_ON_OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if on."""
|
||||
|
@ -111,13 +111,13 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._attr_state = 1
|
||||
self._attr_is_on = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
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()
|
||||
|
||||
def _rfx_message_callback(self, message):
|
||||
@ -125,7 +125,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
rfstate = message.value
|
||||
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}
|
||||
if self._rfid and rfstate is not None:
|
||||
attr[ATTR_RF_BIT0] = bool(rfstate & 0x01)
|
||||
@ -150,5 +150,5 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
|
||||
message.channel,
|
||||
message.value,
|
||||
)
|
||||
self._attr_state = message.value
|
||||
self._attr_is_on = bool(message.value)
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -93,9 +93,10 @@ class AquaLogicSensor(SensorEntity):
|
||||
if panel is not None:
|
||||
if panel.is_metric:
|
||||
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][0]
|
||||
self._attr_state = getattr(panel, self._type)
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
self._attr_unit_of_measurement = SENSOR_TYPES[self._type][1][1]
|
||||
|
||||
self._attr_state = getattr(panel, self._type)
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
self._attr_unit_of_measurement = None
|
||||
|
@ -14,12 +14,13 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
POWER_WATT,
|
||||
)
|
||||
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__)
|
||||
|
||||
@ -87,12 +88,16 @@ class AtomeData:
|
||||
self._is_connected = None
|
||||
self._day_usage = None
|
||||
self._day_price = None
|
||||
self._day_last_reset = None
|
||||
self._week_usage = None
|
||||
self._week_price = None
|
||||
self._week_last_reset = None
|
||||
self._month_usage = None
|
||||
self._month_price = None
|
||||
self._month_last_reset = None
|
||||
self._year_usage = None
|
||||
self._year_price = None
|
||||
self._year_last_reset = None
|
||||
|
||||
@property
|
||||
def live_power(self):
|
||||
@ -137,6 +142,11 @@ class AtomeData:
|
||||
"""Return latest daily usage value."""
|
||||
return self._day_price
|
||||
|
||||
@property
|
||||
def day_last_reset(self):
|
||||
"""Return latest daily last reset."""
|
||||
return self._day_last_reset
|
||||
|
||||
@Throttle(DAILY_SCAN_INTERVAL)
|
||||
def update_day_usage(self):
|
||||
"""Return current daily power usage."""
|
||||
@ -144,6 +154,7 @@ class AtomeData:
|
||||
values = self.atome_client.get_consumption(DAILY_TYPE)
|
||||
self._day_usage = values["total"] / 1000
|
||||
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)
|
||||
|
||||
except KeyError as error:
|
||||
@ -159,6 +170,11 @@ class AtomeData:
|
||||
"""Return latest weekly usage value."""
|
||||
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)
|
||||
def update_week_usage(self):
|
||||
"""Return current weekly power usage."""
|
||||
@ -166,6 +182,7 @@ class AtomeData:
|
||||
values = self.atome_client.get_consumption(WEEKLY_TYPE)
|
||||
self._week_usage = values["total"] / 1000
|
||||
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)
|
||||
|
||||
except KeyError as error:
|
||||
@ -181,6 +198,11 @@ class AtomeData:
|
||||
"""Return latest monthly usage value."""
|
||||
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)
|
||||
def update_month_usage(self):
|
||||
"""Return current monthly power usage."""
|
||||
@ -188,6 +210,7 @@ class AtomeData:
|
||||
values = self.atome_client.get_consumption(MONTHLY_TYPE)
|
||||
self._month_usage = values["total"] / 1000
|
||||
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)
|
||||
|
||||
except KeyError as error:
|
||||
@ -203,6 +226,11 @@ class AtomeData:
|
||||
"""Return latest yearly usage value."""
|
||||
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)
|
||||
def update_year_usage(self):
|
||||
"""Return current yearly power usage."""
|
||||
@ -210,6 +238,7 @@ class AtomeData:
|
||||
values = self.atome_client.get_consumption(YEARLY_TYPE)
|
||||
self._year_usage = values["total"] / 1000
|
||||
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)
|
||||
|
||||
except KeyError as error:
|
||||
@ -219,19 +248,19 @@ class AtomeData:
|
||||
class AtomeSensor(SensorEntity):
|
||||
"""Representation of a sensor entity for Atome."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_POWER
|
||||
|
||||
def __init__(self, data, name, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = name
|
||||
self._data = data
|
||||
|
||||
self._sensor_type = sensor_type
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
|
||||
if sensor_type == LIVE_TYPE:
|
||||
self._attr_device_class = DEVICE_CLASS_POWER
|
||||
self._attr_unit_of_measurement = POWER_WATT
|
||||
self._attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
else:
|
||||
self._attr_device_class = DEVICE_CLASS_ENERGY
|
||||
self._attr_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||
|
||||
def update(self):
|
||||
@ -247,6 +276,9 @@ class AtomeSensor(SensorEntity):
|
||||
}
|
||||
else:
|
||||
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 = {
|
||||
"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):
|
||||
"""Representation of a Bluesound Player."""
|
||||
|
||||
_attr_media_content_type = MEDIA_TYPE_MUSIC
|
||||
|
||||
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
|
||||
def __init__(self, hass, host, port=None, name=None, init_callback=None):
|
||||
"""Initialize the media player."""
|
||||
self.host = host
|
||||
self._hass = hass
|
||||
self.port = port
|
||||
self._polling_session = async_get_clientsession(hass)
|
||||
self._polling_task = None # The actual polling task.
|
||||
self._attr_name = name
|
||||
self._name = name
|
||||
self._icon = None
|
||||
self._capture_items = []
|
||||
self._services_items = []
|
||||
self._preset_items = []
|
||||
self._sync_status = {}
|
||||
self._status = None
|
||||
self._is_online = None
|
||||
self._last_status_update = None
|
||||
self._is_online = False
|
||||
self._retry_remove = None
|
||||
self._muted = False
|
||||
self._master = None
|
||||
self._group_name = None
|
||||
self._bluesound_device_name = None
|
||||
self._is_master = False
|
||||
self._group_name = None
|
||||
self._group_list = []
|
||||
self._bluesound_device_name = None
|
||||
|
||||
self._init_callback = init_callback
|
||||
if self.port is None:
|
||||
self.port = DEFAULT_PORT
|
||||
|
||||
class _TimeoutException(Exception):
|
||||
pass
|
||||
@ -248,12 +252,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
self._sync_status = resp["SyncStatus"].copy()
|
||||
|
||||
if not self.name:
|
||||
self._attr_name = self._sync_status.get("@name", self.host)
|
||||
if not self._name:
|
||||
self._name = self._sync_status.get("@name", self.host)
|
||||
if not self._bluesound_device_name:
|
||||
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
||||
if not self.icon:
|
||||
self._attr_icon = self._sync_status.get("@icon", self.host)
|
||||
if not self._icon:
|
||||
self._icon = self._sync_status.get("@icon", self.host)
|
||||
|
||||
master = self._sync_status.get("master")
|
||||
if master is not None:
|
||||
@ -287,14 +291,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
await self.async_update_status()
|
||||
|
||||
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)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
_LOGGER.debug("Stopping the polling of node %s", self.name)
|
||||
_LOGGER.debug("Stopping the polling of node %s", self._name)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error in %s", self.name)
|
||||
_LOGGER.exception("Unexpected error in %s", self._name)
|
||||
raise
|
||||
|
||||
def start_polling(self):
|
||||
@ -398,7 +402,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if response.status == HTTP_OK:
|
||||
result = await response.text()
|
||||
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()
|
||||
|
||||
group_name = self._status.get("groupName")
|
||||
@ -434,58 +438,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
self._is_online = False
|
||||
self._attr_media_position_updated_at = None
|
||||
self._last_status_update = None
|
||||
self._status = None
|
||||
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
|
||||
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):
|
||||
"""Trigger sync status update on all devices."""
|
||||
@ -585,6 +542,27 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
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
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
@ -639,7 +617,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
position = self._status.get("secs")
|
||||
@ -648,9 +626,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
position = float(position)
|
||||
if mediastate == STATE_PLAYING:
|
||||
position += (
|
||||
dt_util.utcnow() - self.media_position_updated_at
|
||||
).total_seconds()
|
||||
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||
|
||||
return position
|
||||
|
||||
@ -665,6 +641,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
return float(duration)
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
"""Last time status was updated."""
|
||||
return self._last_status_update
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
@ -687,11 +668,21 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
mute = bool(int(mute))
|
||||
return mute
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def bluesound_device_name(self):
|
||||
"""Return the device name as returned by the device."""
|
||||
return self._bluesound_device_name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the device."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
@ -787,15 +778,58 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
@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 self._is_master
|
||||
|
||||
@property
|
||||
def is_grouped(self) -> bool:
|
||||
def is_grouped(self):
|
||||
"""Return true if player is a coordinator."""
|
||||
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):
|
||||
"""Join the player to a group."""
|
||||
master_device = [
|
||||
@ -815,6 +849,17 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
else:
|
||||
_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):
|
||||
"""Rebuild the list of entities in speaker group."""
|
||||
if self._group_name is None:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "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"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
|
@ -66,7 +66,7 @@ DEFAULT_FORECAST_TYPE = DAILY
|
||||
DOMAIN = "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}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "cloud",
|
||||
"name": "Home Assistant Cloud",
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||
"requirements": ["hass-nabucasa==0.44.0"],
|
||||
"requirements": ["hass-nabucasa==0.45.1"],
|
||||
"dependencies": ["http", "webhook"],
|
||||
"after_dependencies": ["google_assistant", "alexa"],
|
||||
"codeowners": ["@home-assistant/cloud"],
|
||||
|
@ -2,7 +2,9 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20210804.0"],
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20210809.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
@ -15,6 +17,8 @@
|
||||
"system_log",
|
||||
"websocket_api"
|
||||
],
|
||||
"codeowners": ["@home-assistant/frontend"],
|
||||
"codeowners": [
|
||||
"@home-assistant/frontend"
|
||||
],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
}
|
@ -63,12 +63,19 @@ def async_setup_forwarded(
|
||||
an HTTP 400 status code is thrown.
|
||||
"""
|
||||
|
||||
try:
|
||||
from hass_nabucasa import remote # pylint: disable=import-outside-toplevel
|
||||
except ImportError:
|
||||
remote = None
|
||||
|
||||
@middleware
|
||||
async def forwarded_middleware(
|
||||
request: Request, handler: Callable[[Request], Awaitable[StreamResponse]]
|
||||
) -> StreamResponse:
|
||||
"""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
|
||||
forwarded_for_headers: list[str] = request.headers.getall(X_FORWARDED_FOR, [])
|
||||
@ -120,6 +127,8 @@ def async_setup_forwarded(
|
||||
)
|
||||
raise HTTPBadRequest from err
|
||||
|
||||
overrides: dict[str, str] = {}
|
||||
|
||||
# Find the last trusted index in the X-Forwarded-For list
|
||||
forwarded_for_index = 0
|
||||
for forwarded_ip in forwarded_for:
|
||||
|
@ -71,7 +71,7 @@ class ShadeEntity(HDEntity):
|
||||
"name": self._shade_name,
|
||||
"suggested_area": self._room_name,
|
||||
"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]),
|
||||
}
|
||||
|
||||
|
@ -145,6 +145,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
)
|
||||
self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
self._unit_of_measurement = state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
||||
@callback
|
||||
def calc_integration(event):
|
||||
"""Handle the sensor state changes."""
|
||||
|
@ -141,9 +141,9 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
|
||||
self._poolid = self.coordinator.data[poolidx]["id"]
|
||||
|
||||
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._name = f"{self._device_name} {description.name}"
|
||||
self._attr_name = f"{self._device_name} {description.name}"
|
||||
|
||||
def _pooldata(self):
|
||||
"""Get pool data dict."""
|
||||
@ -168,11 +168,6 @@ class OndiloICO(CoordinatorEntity, SensorEntity):
|
||||
"""Last value of the sensor."""
|
||||
return self._devdata()["value"]
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info for the sensor."""
|
||||
|
@ -431,7 +431,7 @@ class SimpliSafeEntity(CoordinatorEntity):
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, system.system_id)},
|
||||
"manufacturer": "SimpliSafe",
|
||||
"model": system.version,
|
||||
"model": str(system.version),
|
||||
"name": name,
|
||||
"via_device": (DOMAIN, system.serial),
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Sonos",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"requirements": ["soco==0.23.2"],
|
||||
"requirements": ["soco==0.23.3"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["plex", "zeroconf"],
|
||||
"zeroconf": ["_sonos._tcp.local."],
|
||||
|
@ -496,9 +496,7 @@ class SonosSpeaker:
|
||||
|
||||
self.async_write_entity_states()
|
||||
|
||||
async def async_unseen(
|
||||
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
||||
) -> None:
|
||||
async def async_unseen(self, now: datetime.datetime | None = None) -> None:
|
||||
"""Make this player unavailable when it was not seen recently."""
|
||||
if self._seen_timer:
|
||||
self._seen_timer()
|
||||
@ -527,9 +525,8 @@ class SonosSpeaker:
|
||||
|
||||
await self.async_unsubscribe()
|
||||
|
||||
if not will_reconnect:
|
||||
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
|
||||
self.async_write_entity_states()
|
||||
self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid)
|
||||
self.async_write_entity_states()
|
||||
|
||||
async def async_rebooted(self, soco: SoCo) -> None:
|
||||
"""Handle a detected speaker reboot."""
|
||||
@ -538,8 +535,24 @@ class SonosSpeaker:
|
||||
self.zone_name,
|
||||
soco,
|
||||
)
|
||||
await self.async_unseen(will_reconnect=True)
|
||||
await self.async_seen(soco)
|
||||
await self.async_unsubscribe()
|
||||
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
|
||||
|
@ -615,6 +615,10 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""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
|
||||
if speed:
|
||||
await self.async_set_speed(speed)
|
||||
@ -623,10 +627,6 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity):
|
||||
await self.async_set_percentage(percentage)
|
||||
if 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:
|
||||
self._state = True
|
||||
@ -1138,6 +1138,7 @@ class XiaomiAirFresh(XiaomiGenericDevice):
|
||||
self._speed_list = OPERATION_MODES_AIRFRESH
|
||||
self._speed_count = 4
|
||||
self._preset_modes = PRESET_MODES_AIRFRESH
|
||||
self._supported_features = SUPPORT_SET_SPEED | SUPPORT_PRESET_MODE
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes}
|
||||
)
|
||||
|
@ -142,7 +142,15 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
zc_args: dict = {}
|
||||
|
||||
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
|
||||
else:
|
||||
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:
|
||||
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)
|
||||
zeroconf = cast(HaZeroconf, aio_zc.zeroconf)
|
||||
zeroconf_types, homekit_models = await asyncio.gather(
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.33.4"],
|
||||
"requirements": ["zeroconf==0.34.3"],
|
||||
"dependencies": ["network", "api"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
|
@ -301,7 +301,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
# fallback to setting the color(s) one by one if multicolor fails
|
||||
# not sure this is needed at all, but just in case
|
||||
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(
|
||||
self,
|
||||
|
@ -5,7 +5,7 @@ from typing import Final
|
||||
|
||||
MAJOR_VERSION: Final = 2021
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "4"
|
||||
PATCH_VERSION: Final = "5"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)
|
||||
|
@ -16,8 +16,8 @@ cryptography==3.3.2
|
||||
defusedxml==0.7.1
|
||||
distro==1.5.0
|
||||
emoji==1.2.0
|
||||
hass-nabucasa==0.44.0
|
||||
home-assistant-frontend==20210804.0
|
||||
hass-nabucasa==0.45.1
|
||||
home-assistant-frontend==20210809.0
|
||||
httpx==0.18.2
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.0.1
|
||||
@ -33,7 +33,7 @@ sqlalchemy==1.4.17
|
||||
voluptuous-serialize==2.4.0
|
||||
voluptuous==0.12.1
|
||||
yarl==1.6.3
|
||||
zeroconf==0.33.4
|
||||
zeroconf==0.34.3
|
||||
|
||||
pycryptodome>=3.6.6
|
||||
|
||||
|
@ -365,7 +365,7 @@ beautifulsoup4==4.9.3
|
||||
bellows==0.26.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.7.16
|
||||
bimmer_connected==0.7.18
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@ -750,7 +750,7 @@ habitipy==0.2.0
|
||||
hangups==0.4.14
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.44.0
|
||||
hass-nabucasa==0.45.1
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass_splunk==0.1.1
|
||||
@ -783,7 +783,7 @@ hole==0.5.1
|
||||
holidays==0.11.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210804.0
|
||||
home-assistant-frontend==20210809.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -2152,7 +2152,7 @@ smhi-pkg==1.0.15
|
||||
snapcast==2.1.3
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.23.2
|
||||
soco==0.23.3
|
||||
|
||||
# homeassistant.components.solaredge_local
|
||||
solaredge-local==0.2.0
|
||||
@ -2439,7 +2439,7 @@ zeep[async]==4.0.0
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.33.4
|
||||
zeroconf==0.34.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.59
|
||||
|
@ -220,7 +220,7 @@ base36==0.1.1
|
||||
bellows==0.26.0
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.7.16
|
||||
bimmer_connected==0.7.18
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==1.3.3
|
||||
@ -428,7 +428,7 @@ habitipy==0.2.0
|
||||
hangups==0.4.14
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.44.0
|
||||
hass-nabucasa==0.45.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.2.20
|
||||
@ -449,7 +449,7 @@ hole==0.5.1
|
||||
holidays==0.11.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20210804.0
|
||||
home-assistant-frontend==20210809.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -1177,7 +1177,7 @@ smarthab==0.21
|
||||
smhi-pkg==1.0.15
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.23.2
|
||||
soco==0.23.3
|
||||
|
||||
# homeassistant.components.solaredge
|
||||
solaredge==0.0.2
|
||||
@ -1341,7 +1341,7 @@ youless-api==0.10
|
||||
zeep[async]==4.0.0
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.33.4
|
||||
zeroconf==0.34.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.59
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test real forwarded middleware."""
|
||||
from ipaddress import ip_network
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import web
|
||||
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 "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",
|
||||
"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(
|
||||
"homeassistant.components.zeroconf.AsyncServiceInfo",
|
||||
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: {}})
|
||||
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
|
||||
actual = _get_announced_addresses(_ADAPTERS_WITH_MANUAL_CONFIG, first_ip)
|
||||
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_PROPERTY: "targetColor",
|
||||
ATTR_PROPERTY_KEY: 2,
|
||||
ATTR_VALUE: 2,
|
||||
ATTR_VALUE: '{ "warmWhite": 0, "coldWhite": 0, "red": 255, "green": 0, "blue": 0 }',
|
||||
ATTR_OPTIONS: {"transitionDuration": 1},
|
||||
},
|
||||
blocking=True,
|
||||
@ -1038,9 +1037,11 @@ async def test_multicast_set_value_options(
|
||||
assert args["valueId"] == {
|
||||
"commandClass": 51,
|
||||
"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}
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
@ -267,8 +267,7 @@
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"label": "Target value (Warm White)",
|
||||
"description": "The target value of the Warm White color.",
|
||||
"valueChangeOptions": ["transitionDuration"]
|
||||
"description": "The target value of the Warm White color."
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -286,8 +285,7 @@
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"label": "Target value (Cold White)",
|
||||
"description": "The target value of the Cold White color.",
|
||||
"valueChangeOptions": ["transitionDuration"]
|
||||
"description": "The target value of the Cold White color."
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -305,8 +303,7 @@
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"label": "Target value (Red)",
|
||||
"description": "The target value of the Red color.",
|
||||
"valueChangeOptions": ["transitionDuration"]
|
||||
"description": "The target value of the Red color."
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -324,8 +321,7 @@
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"label": "Target value (Green)",
|
||||
"description": "The target value of the Green color.",
|
||||
"valueChangeOptions": ["transitionDuration"]
|
||||
"description": "The target value of the Green color."
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -343,8 +339,7 @@
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"label": "Target value (Blue)",
|
||||
"description": "The target value of the Blue color.",
|
||||
"valueChangeOptions": ["transitionDuration"]
|
||||
"description": "The target value of the Blue color."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user