Skip to content
Merged

0.110.3 #36155

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 48 additions & 25 deletions homeassistant/components/airvisual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
PLATFORMS = ["air_quality", "sensor"]

DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1)
DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}

GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
Expand Down Expand Up @@ -95,27 +95,43 @@ def async_get_cloud_api_update_interval(hass, api_key):
This will shift based on the number of active consumers, thus keeping the user
under the monthly API limit.
"""
num_consumers = len(
{
config_entry
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.data.get(CONF_API_KEY) == api_key
}
)
num_consumers = len(async_get_cloud_coordinators_by_api_key(hass, api_key))

# Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note
# that we give a buffer of 1500 API calls for any drift, restarts, etc.:
minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers))

LOGGER.debug(
"Leveling API key usage (%s): %s consumers, %s minutes between updates",
api_key,
num_consumers,
minutes_between_api_calls,
)

return timedelta(minutes=minutes_between_api_calls)


@callback
def async_reset_coordinator_update_intervals(hass, update_interval):
"""Update any existing data coordinators with a new update interval."""
if not hass.data[DOMAIN][DATA_COORDINATOR]:
return
def async_get_cloud_coordinators_by_api_key(hass, api_key):
"""Get all DataUpdateCoordinator objects related to a particular API key."""
coordinators = []
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
config_entry = hass.config_entries.async_get_entry(entry_id)
if config_entry.data.get(CONF_API_KEY) == api_key:
coordinators.append(coordinator)
return coordinators


for coordinator in hass.data[DOMAIN][DATA_COORDINATOR].values():
@callback
def async_sync_geo_coordinator_update_intervals(hass, api_key):
"""Sync the update interval for geography-based data coordinators (by API key)."""
update_interval = async_get_cloud_api_update_interval(hass, api_key)
for coordinator in async_get_cloud_coordinators_by_api_key(hass, api_key):
LOGGER.debug(
"Updating interval for coordinator: %s, %s",
coordinator.name,
update_interval,
)
coordinator.update_interval = update_interval


Expand Down Expand Up @@ -194,10 +210,6 @@ async def async_setup_entry(hass, config_entry):

client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)

update_interval = async_get_cloud_api_update_interval(
hass, config_entry.data[CONF_API_KEY]
)

async def async_update_data():
"""Get new data from the API."""
if CONF_CITY in config_entry.data:
Expand All @@ -219,14 +231,19 @@ async def async_update_data():
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name="geography data",
update_interval=update_interval,
name=async_get_geography_id(config_entry.data),
# We give a placeholder update interval in order to create the coordinator;
# then, below, we use the coordinator's presence (along with any other
# coordinators using the same API key) to calculate an actual, leveled
# update interval:
update_interval=timedelta(minutes=5),
update_method=async_update_data,
)

# Ensure any other, existing config entries that use this API key are updated
# with the new scan interval:
async_reset_coordinator_update_intervals(hass, update_interval)
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
async_sync_geo_coordinator_update_intervals(
hass, config_entry.data[CONF_API_KEY]
)

# Only geography-based entries have options:
config_entry.add_update_listener(async_update_options)
Expand All @@ -251,13 +268,13 @@ async def async_update_data():
hass,
LOGGER,
name="Node/Pro data",
update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL,
update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
update_method=async_update_data,
)

await coordinator.async_refresh()
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator

hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
await coordinator.async_refresh()

for component in PLATFORMS:
hass.async_create_task(
Expand Down Expand Up @@ -317,6 +334,12 @@ async def async_unload_entry(hass, config_entry):
)
if unload_ok:
hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
# Re-calculate the update interval period for any remaining consumes of this
# API key:
async_sync_geo_coordinator_update_intervals(
hass, config_entry.data[CONF_API_KEY]
)

return unload_ok

Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/airvisual/air_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
INTEGRATION_TYPE_GEOGRAPHY,
INTEGRATION_TYPE_NODE_PRO,
)

ATTR_HUMIDITY = "humidity"
Expand All @@ -18,12 +18,12 @@

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up AirVisual air quality entities based on a config entry."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]

# Geography-based AirVisual integrations don't utilize this platform:
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO:
return

coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]

async_add_entities([AirVisualNodeProSensor(coordinator)], True)


Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/auth/indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri):
# Whitelist the iOS and Android callbacks so that people can link apps
# without being connected to the internet.
if redirect_uri == "homeassistant://auth-callback" and client_id in (
"https://www.home-assistant.io/android",
"https://www.home-assistant.io/iOS",
"https://home-assistant.io/android",
"https://home-assistant.io/iOS",
):
return True

Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/emulated_hue/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Constants for emulated_hue."""

HUE_SERIAL_NUMBER = "001788FFFE23BFC2"
HUE_UUID = "2f402f80-da50-11e1-9b23-001788255acc"
55 changes: 36 additions & 19 deletions homeassistant/components/emulated_hue/upnp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from homeassistant import core
from homeassistant.components.http import HomeAssistantView

from .const import HUE_SERIAL_NUMBER, HUE_UUID

_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -42,8 +44,8 @@ def get(self, request):
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>001788FFFE23BFC2</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
<serialNumber>{HUE_SERIAL_NUMBER}</serialNumber>
<UDN>uuid:{HUE_UUID}</UDN>
</device>
</root>
"""
Expand All @@ -70,21 +72,8 @@ def __init__(
self.host_ip_addr = host_ip_addr
self.listen_port = listen_port
self.upnp_bind_multicast = upnp_bind_multicast

# Note that the double newline at the end of
# this string is required per the SSDP spec
resp_template = f"""HTTP/1.1 200 OK
CACHE-CONTROL: max-age=60
EXT:
LOCATION: http://{advertise_ip}:{advertise_port}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
hue-bridgeid: 001788FFFE23BFC2
ST: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice

"""

self.upnp_response = resp_template.replace("\n", "\r\n").encode("utf-8")
self.advertise_ip = advertise_ip
self.advertise_port = advertise_port

def run(self):
"""Run the server."""
Expand Down Expand Up @@ -136,10 +125,13 @@ def run(self):
continue

if "M-SEARCH" in data.decode("utf-8", errors="ignore"):
_LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data)
# SSDP M-SEARCH method received, respond to it with our info
resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
response = self._handle_request(data)

resp_socket.sendto(self.upnp_response, addr)
resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
resp_socket.sendto(response, addr)
_LOGGER.debug("UPNP Responder responding with: %s", response)
resp_socket.close()

def stop(self):
Expand All @@ -148,6 +140,31 @@ def stop(self):
self._interrupted = True
self.join()

def _handle_request(self, data):
if "upnp:rootdevice" in data.decode("utf-8", errors="ignore"):
return self._prepare_response(
"upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice"
)

return self._prepare_response(
"urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}"
)

def _prepare_response(self, search_target, unique_service_name):
# Note that the double newline at the end of
# this string is required per the SSDP spec
response = f"""HTTP/1.1 200 OK
CACHE-CONTROL: max-age=60
EXT:
LOCATION: http://{self.advertise_ip}:{self.advertise_port}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
hue-bridgeid: {HUE_SERIAL_NUMBER}
ST: {search_target}
USN: {unique_service_name}

"""
return response.replace("\n", "\r\n").encode("utf-8")


def clean_socket_close(sock):
"""Close a socket connection and logs its closure."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20200519.4"],
"requirements": ["home-assistant-frontend==20200519.5"],
"dependencies": [
"api",
"auth",
Expand Down
44 changes: 41 additions & 3 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import asyncio
import ipaddress
import logging
import os

from aiohttp import web
import voluptuous as vol
Expand Down Expand Up @@ -49,6 +50,7 @@
ATTR_SOFTWARE_VERSION,
ATTR_VALUE,
BRIDGE_NAME,
BRIDGE_SERIAL_NUMBER,
CONF_ADVERTISE_IP,
CONF_AUTO_START,
CONF_ENTITY_CONFIG,
Expand Down Expand Up @@ -434,6 +436,13 @@ def setup(self):
interface_choice=self._interface_choice,
)

# If we do not load the mac address will be wrong
# as pyhap uses a random one until state is restored
if os.path.exists(persist_file):
self.driver.load()
else:
self.driver.persist()

self.bridge = HomeBridge(self.hass, self.driver, self._name)
if self._safe_mode:
_LOGGER.debug("Safe_mode selected for %s", self._name)
Expand Down Expand Up @@ -540,16 +549,45 @@ async def async_start(self, *args):
@callback
def _async_register_bridge(self, dev_reg):
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
formatted_mac = device_registry.format_mac(self.driver.state.mac)
# Connections and identifiers are both used here.
#
# connections exists so homekit_controller can know the
# virtual mac address of the bridge and know to not offer
# it via discovery.
#
# identifiers is used as well since the virtual mac may change
# because it will not survive manual pairing resets (deleting state file)
# which we have trained users to do over the past few years
# because this was the way you had to fix homekit when pairing
# failed.
#
connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac)
identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER)
self._async_purge_old_bridges(dev_reg, identifier, connection)
dev_reg.async_get_or_create(
config_entry_id=self._entry_id,
connections={
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
},
identifiers={identifier},
connections={connection},
manufacturer=MANUFACTURER,
name=self._name,
model="Home Assistant HomeKit Bridge",
)

@callback
def _async_purge_old_bridges(self, dev_reg, identifier, connection):
"""Purge bridges that exist from failed pairing or manual resets."""
devices_to_purge = []
for entry in dev_reg.devices.values():
if self._entry_id in entry.config_entries and (
identifier not in entry.identifiers
or connection not in entry.connections
):
devices_to_purge.append(entry.id)

for device_id in devices_to_purge:
dev_reg.async_remove_device(device_id)

def _start(self, bridged_states):
from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel
type_cameras,
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/hunterdouglas_powerview/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
DEVICE_INFO,
DEVICE_MODEL,
DOMAIN,
LEGACY_DEVICE_MODEL,
PV_API,
PV_ROOM_DATA,
PV_SHADE_DATA,
Expand Down Expand Up @@ -118,7 +119,7 @@ def device_state_attributes(self):
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
if self._device_info[DEVICE_MODEL] != "1":
if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL:
supported_features |= SUPPORT_STOP
return supported_features

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/iaqualink/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
"codeowners": ["@flz"],
"requirements": ["iaqualink==0.3.1"]
"requirements": ["iaqualink==0.3.3"]
}
Loading