Merge pull request #35001 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-05-01 10:35:19 +02:00 committed by GitHub
commit 02cca1a4da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 242 additions and 44 deletions

View File

@ -195,6 +195,7 @@ homeassistant/components/ipp/* @ctalkington
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/islamic_prayer_times/* @engrbm87
homeassistant/components/isy994/* @bdraco
homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/juicenet/* @jesserockz

View File

@ -190,6 +190,9 @@ stages:
pip install -U pip setuptools wheel
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
# This is a TEMP. Eventually we should make sure our 4 dependencies drop typing.
# Find offending deps with `pipdeptree -r -p typing`
pip uninstall -y typing
- script: |
. venv/bin/activate
pip install -e .

View File

@ -3,7 +3,7 @@
"name": "Brother Printer",
"documentation": "https://www.home-assistant.io/integrations/brother",
"codeowners": ["@bieniu"],
"requirements": ["brother==0.1.13"],
"requirements": ["brother==0.1.14"],
"zeroconf": ["_printer._tcp.local."],
"config_flow": true,
"quality_scale": "platinum"

View File

@ -5,7 +5,7 @@ from typing import Any, Callable, Iterable, List
from directv import DIRECTV, DIRECTVError
from homeassistant.components.remote import RemoteDevice
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
@ -95,6 +95,9 @@ class DIRECTVRemote(DIRECTVEntity, RemoteDevice):
blue, chanup, chandown, prev, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9, dash, enter
"""
num_repeats = kwargs[ATTR_NUM_REPEATS]
for _ in range(num_repeats):
for single_command in command:
try:
await self.dtv.remote(single_command, self._address)

View File

@ -165,7 +165,9 @@ class Fan(HomeAccessory):
self.char_direction.set_value(hk_direction)
# Handle Speed
if self.char_speed is not None:
if self.char_speed is not None and state != STATE_OFF:
# We do not change the homekit speed when turning off
# as it will clear the restore state
speed = new_state.attributes.get(ATTR_SPEED)
hk_speed_value = self.speed_mapping.speed_to_homekit(speed)
if hk_speed_value is not None and self.char_speed.value != hk_speed_value:

View File

@ -200,7 +200,7 @@ class HomeKitSpeedMapping:
if speed is None:
return None
speed_range = self.speed_ranges[speed]
return speed_range.target
return round(speed_range.target)
def speed_to_states(self, speed):
"""Map HomeKit speed to Home Assistant speed state."""

View File

@ -3,12 +3,15 @@ import logging
from typing import Callable
from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import ISY994_NODES, ISYDevice
_LOGGER = logging.getLogger(__name__)
ATTR_LAST_BRIGHTNESS = "last_brightness"
def setup_platform(
hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None
@ -21,13 +24,13 @@ def setup_platform(
add_entities(devices)
class ISYLightDevice(ISYDevice, Light):
class ISYLightDevice(ISYDevice, Light, RestoreEntity):
"""Representation of an ISY994 light device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 light device."""
super().__init__(node)
self._last_brightness = self.brightness
self._last_brightness = None
@property
def is_on(self) -> bool:
@ -56,7 +59,7 @@ class ISYLightDevice(ISYDevice, Light):
# pylint: disable=arguments-differ
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
if brightness is None and self._last_brightness is not None:
if brightness is None and self._last_brightness:
brightness = self._last_brightness
if not self._node.on(val=brightness):
_LOGGER.debug("Unable to turn on light")
@ -65,3 +68,23 @@ class ISYLightDevice(ISYDevice, Light):
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
@property
def device_state_attributes(self):
"""Return the light attributes."""
return {ATTR_LAST_BRIGHTNESS: self._last_brightness}
async def async_added_to_hass(self) -> None:
"""Restore last_brightness on restart."""
await super().async_added_to_hass()
self._last_brightness = self.brightness or 255
last_state = await self.async_get_last_state()
if not last_state:
return
if (
ATTR_LAST_BRIGHTNESS in last_state.attributes
and last_state.attributes[ATTR_LAST_BRIGHTNESS]
):
self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS]

View File

@ -3,5 +3,5 @@
"name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["PyISY==1.1.2"],
"codeowners": []
"codeowners": ["@bdraco"]
}

View File

@ -23,7 +23,7 @@ def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType
debug_info = hass.data[DATA_MQTT_DEBUG_INFO]
messages = debug_info["entities"][entity_id]["subscriptions"][
msg.subscribed_topic
]
]["messages"]
if msg not in messages:
messages.append(msg)
@ -50,13 +50,24 @@ def add_subscription(hass, message_callback, subscription):
entity_info = debug_info["entities"].setdefault(
entity_id, {"subscriptions": {}, "discovery_data": {}}
)
entity_info["subscriptions"][subscription] = deque([], STORED_MESSAGES)
if subscription not in entity_info["subscriptions"]:
entity_info["subscriptions"][subscription] = {
"count": 0,
"messages": deque([], STORED_MESSAGES),
}
entity_info["subscriptions"][subscription]["count"] += 1
def remove_subscription(hass, message_callback, subscription):
"""Remove debug data for subscription."""
"""Remove debug data for subscription if it exists."""
entity_id = getattr(message_callback, "__entity_id", None)
if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]:
hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][
subscription
]["count"] -= 1
if not hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][
subscription
]["count"]:
hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"].pop(
subscription
)
@ -127,10 +138,10 @@ async def info_for_device(hass, device_id):
"topic": topic,
"messages": [
{"payload": msg.payload, "time": msg.timestamp, "topic": msg.topic}
for msg in list(messages)
for msg in list(subscription["messages"])
],
}
for topic, messages in entity_info["subscriptions"].items()
for topic, subscription in entity_info["subscriptions"].items()
]
discovery_data = {
"topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""),

View File

@ -123,7 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not person.controllers:
_LOGGER.error("No Rachio devices found in account %s", person.username)
return False
_LOGGER.info("%d Rachio device(s) found", len(person.controllers))
_LOGGER.info(
"%d Rachio device(s) found; The url %s must be accessible from the internet in order to receive updates",
len(person.controllers),
webhook_url,
)
# Enable component
hass.data[DOMAIN][entry.entry_id] = person

View File

@ -7,7 +7,7 @@ from requests.exceptions import (
)
from roku import RokuException
from homeassistant.components.remote import RemoteDevice
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
@ -84,6 +84,9 @@ class RokuRemote(RemoteDevice):
def send_command(self, command, **kwargs):
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]
for _ in range(num_repeats):
for single_command in command:
if not hasattr(self.roku, single_command):
continue

View File

@ -3,6 +3,7 @@ import asyncio
import logging
from homeassistant.components.vacuum import (
ATTR_STATUS,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
@ -16,6 +17,7 @@ from homeassistant.components.vacuum import (
SUPPORT_SEND_COMMAND,
SUPPORT_START,
SUPPORT_STATE,
SUPPORT_STATUS,
SUPPORT_STOP,
StateVacuumDevice,
)
@ -40,6 +42,7 @@ SUPPORT_IROBOT = (
| SUPPORT_SEND_COMMAND
| SUPPORT_START
| SUPPORT_STATE
| SUPPORT_STATUS
| SUPPORT_STOP
| SUPPORT_LOCATE
)
@ -143,7 +146,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumDevice):
state = STATE_MAP[phase]
except KeyError:
return STATE_ERROR
if cycle != "none" and state != STATE_CLEANING and state != STATE_RETURNING:
if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED):
state = STATE_PAUSED
return state
@ -173,6 +176,9 @@ class IRobotVacuum(IRobotEntity, StateVacuumDevice):
# Set properties that are to appear in the GUI
state_attrs = {ATTR_SOFTWARE_VERSION: software_version}
# Set legacy status to avoid break changes
state_attrs[ATTR_STATUS] = self.vacuum.current_state
# Only add cleaning time and cleaned area attrs when the vacuum is
# currently on
if self.state == STATE_CLEANING:

View File

@ -24,7 +24,8 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): cv.string,
}
)
}
},
extra=vol.ALLOW_EXTRA,
)

View File

@ -101,7 +101,7 @@ def add_entities(controller, async_add_entities):
if tracker_class is UniFiClientTracker:
if item.is_wired:
if mac not in controller.wireless_clients:
if not controller.option_track_wired_clients:
continue
else:

View File

@ -132,7 +132,7 @@ class VizioDevice(MediaPlayerDevice):
self._state = None
self._volume_level = None
self._volume_step = config_entry.options[CONF_VOLUME_STEP]
self._is_muted = None
self._is_volume_muted = None
self._current_input = None
self._current_app = None
self._current_app_config = None
@ -190,7 +190,7 @@ class VizioDevice(MediaPlayerDevice):
if not is_on:
self._state = STATE_OFF
self._volume_level = None
self._is_muted = None
self._is_volume_muted = None
self._current_input = None
self._available_inputs = None
self._current_app = None
@ -207,7 +207,10 @@ class VizioDevice(MediaPlayerDevice):
)
if audio_settings is not None:
self._volume_level = float(audio_settings["volume"]) / self._max_volume
self._is_muted = audio_settings["mute"].lower() == "on"
if "mute" in audio_settings:
self._is_volume_muted = audio_settings["mute"].lower() == "on"
else:
self._is_volume_muted = None
if VIZIO_SOUND_MODE in audio_settings:
self._supported_commands |= SUPPORT_SELECT_SOUND_MODE
@ -324,7 +327,7 @@ class VizioDevice(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._is_muted
return self._is_volume_muted
@property
def source(self) -> str:
@ -428,10 +431,10 @@ class VizioDevice(MediaPlayerDevice):
"""Mute the volume."""
if mute:
await self._device.mute_on()
self._is_muted = True
self._is_volume_muted = True
else:
await self._device.mute_off()
self._is_muted = False
self._is_volume_muted = False
async def async_media_previous_track(self) -> None:
"""Send previous channel command."""

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 109
PATCH_VERSION = "1"
PATCH_VERSION = "2"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@ -365,7 +365,7 @@ bravia-tv==1.0.2
broadlink==0.13.2
# homeassistant.components.brother
brother==0.1.13
brother==0.1.14
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1

View File

@ -147,7 +147,7 @@ bravia-tv==1.0.2
broadlink==0.13.2
# homeassistant.components.brother
brother==0.1.13
brother==0.1.14
# homeassistant.components.buienradar
buienradar==1.0.4

View File

@ -304,6 +304,7 @@ async def test_fan_speed(hass, hk_driver, cls, events):
call_set_speed = async_mock_service(hass, DOMAIN, "set_speed")
char_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID]
char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
@ -320,12 +321,37 @@ async def test_fan_speed(hass, hk_driver, cls, events):
await hass.async_add_executor_job(acc.char_speed.client_update_value, 42)
await hass.async_block_till_done()
acc.speed_mapping.speed_to_states.assert_called_with(42)
assert acc.char_speed.value == 42
assert acc.char_active.value == 1
assert call_set_speed[0]
assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id
assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous"
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "ludicrous"
# Verify speed is preserved from off to on
hass.states.async_set(entity_id, STATE_OFF, {ATTR_SPEED: SPEED_OFF})
await hass.async_block_till_done()
assert acc.char_speed.value == 42
assert acc.char_active.value == 0
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_active_iid,
HAP_REPR_VALUE: 1,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert acc.char_speed.value == 42
assert acc.char_active.value == 1
async def test_fan_set_all_one_shot(hass, hk_driver, cls, events):
"""Test fan with speed."""

View File

@ -960,6 +960,42 @@ async def test_mqtt_ws_remove_discovered_device_twice(
assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND
async def test_mqtt_ws_remove_discovered_device_same_topic(
hass, device_reg, hass_ws_client, mqtt_mock
):
"""Test MQTT websocket device removal."""
config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
config_entry.add_to_hass(hass)
await async_start(hass, "homeassistant", {}, config_entry)
data = (
'{ "device":{"identifiers":["0AFFD2"]},'
' "state_topic": "foobar/sensor",'
' "availability_topic": "foobar/sensor",'
' "unique_id": "unique" }'
)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set())
assert device_entry is not None
client = await hass_ws_client(hass)
await client.send_json(
{"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id}
)
response = await client.receive_json()
assert response["success"]
await client.send_json(
{"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND
async def test_mqtt_ws_remove_non_mqtt_device(
hass, device_reg, hass_ws_client, mqtt_mock
):
@ -1306,7 +1342,60 @@ async def test_debug_info_filter_same(hass, mqtt_mock):
assert {
"topic": "sensor/#",
"messages": [
{"topic": "sensor/abc", "payload": "123", "time": dt1},
{"topic": "sensor/abc", "payload": "123", "time": dt2},
{"payload": "123", "time": dt1, "topic": "sensor/abc"},
{"payload": "123", "time": dt2, "topic": "sensor/abc"},
],
} == debug_info_data["entities"][0]["subscriptions"][0]
async def test_debug_info_same_topic(hass, mqtt_mock):
"""Test debug info."""
config = {
"device": {"identifiers": ["helloworld"]},
"platform": "mqtt",
"name": "test",
"state_topic": "sensor/status",
"availability_topic": "sensor/status",
"unique_id": "veryunique",
}
entry = MockConfigEntry(domain=mqtt.DOMAIN)
entry.add_to_hass(hass)
await async_start(hass, "homeassistant", {}, entry)
registry = await hass.helpers.device_registry.async_get_registry()
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
device = registry.async_get_device({("mqtt", "helloworld")}, set())
assert device is not None
debug_info_data = await debug_info.info_for_device(hass, device.id)
assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1
assert {"topic": "sensor/status", "messages": []} in debug_info_data["entities"][0][
"subscriptions"
]
start_dt = datetime(2019, 1, 1, 0, 0, 0)
with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow:
dt_utcnow.return_value = start_dt
async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False)
debug_info_data = await debug_info.info_for_device(hass, device.id)
assert len(debug_info_data["entities"][0]["subscriptions"]) == 1
assert {
"payload": "123",
"time": start_dt,
"topic": "sensor/status",
} in debug_info_data["entities"][0]["subscriptions"][0]["messages"]
config["availability_topic"] = "sensor/availability"
data = json.dumps(config)
async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data)
await hass.async_block_till_done()
start_dt = datetime(2019, 1, 1, 0, 0, 0)
with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow:
dt_utcnow.return_value = start_dt
async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False)

View File

@ -621,3 +621,26 @@ async def test_setup_with_no_running_app(
assert attr["source"] == "CAST"
assert "app_id" not in attr
assert "app_name" not in attr
async def test_setup_tv_without_mute(
hass: HomeAssistantType,
vizio_connect: pytest.fixture,
vizio_update: pytest.fixture,
) -> None:
"""Test Vizio TV entity setup when mute property isn't returned by Vizio API."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=vol.Schema(VIZIO_SCHEMA)(MOCK_USER_VALID_TV_CONFIG),
unique_id=UNIQUE_ID,
)
async with _cm_for_test_setup_without_apps(
{"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)}, STATE_ON,
):
await _add_config_entry_to_hass(hass, config_entry)
attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON)
_assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV)
assert "sound_mode" not in attr
assert "is_volume_muted" not in attr