Migrate emulate_hue to use storage to fix I/O in event loop (#50473)

pull/50531/head
J. Nick Koston 2021-05-12 09:10:28 -05:00 committed by GitHub
parent 72f342aa5b
commit 70961c79a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 88 additions and 93 deletions

View File

@ -1,5 +1,4 @@
"""Support for local control of entities by emulating a Philips Hue bridge."""
from contextlib import suppress
import logging
from aiohttp import web
@ -12,9 +11,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import storage
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .hue_api import (
HueAllGroupsStateView,
@ -34,6 +32,9 @@ DOMAIN = "emulated_hue"
_LOGGER = logging.getLogger(__name__)
NUMBERS_FILE = "emulated_hue_ids.json"
DATA_KEY = "emulated_hue.ids"
DATA_VERSION = "1"
SAVE_DELAY = 60
CONF_ADVERTISE_IP = "advertise_ip"
CONF_ADVERTISE_PORT = "advertise_port"
@ -155,6 +156,7 @@ async def async_setup(hass, yaml_config):
nonlocal protocol
nonlocal site
nonlocal runner
await config.async_setup()
_, protocol = await listen
@ -189,6 +191,7 @@ class Config:
self.hass = hass
self.type = conf.get(CONF_TYPE)
self.numbers = None
self.store = None
self.cached_states = {}
self._exposed_cache = {}
@ -257,14 +260,21 @@ class Config:
# for compatibility with older installations.
self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE)
async def async_setup(self):
"""Set up and migrate to storage."""
self.store = storage.Store(self.hass, DATA_VERSION, DATA_KEY)
self.numbers = (
await storage.async_migrator(
self.hass, self.hass.config.path(NUMBERS_FILE), self.store
)
or {}
)
def entity_id_to_number(self, entity_id):
"""Get a unique number for the entity id."""
if self.type == TYPE_ALEXA:
return entity_id
if self.numbers is None:
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home
for number, ent_id in self.numbers.items():
if entity_id == ent_id:
@ -274,7 +284,7 @@ class Config:
if self.numbers:
number = str(max(int(k) for k in self.numbers) + 1)
self.numbers[number] = entity_id
save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
self.store.async_delay_save(lambda: self.numbers, SAVE_DELAY)
return number
def number_to_entity_id(self, number):
@ -282,9 +292,6 @@ class Config:
if self.type == TYPE_ALEXA:
return number
if self.numbers is None:
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home
assert isinstance(number, str)
return self.numbers.get(number)
@ -338,10 +345,3 @@ class Config:
return True
return False
def _load_json(filename):
"""Load JSON, handling invalid syntax."""
with suppress(HomeAssistantError):
return load_json(filename)
return {}

View File

@ -1,106 +1,101 @@
"""Test the Emulated Hue component."""
from unittest.mock import MagicMock, Mock, patch
from datetime import timedelta
from homeassistant.components.emulated_hue import Config
from homeassistant.components.emulated_hue import (
DATA_KEY,
DATA_VERSION,
SAVE_DELAY,
Config,
)
from homeassistant.util import utcnow
from tests.common import async_fire_time_changed
def test_config_google_home_entity_id_to_number():
async def test_config_google_home_entity_id_to_number(hass, hass_storage):
"""Test config adheres to the type."""
mock_hass = Mock()
mock_hass.config.path = MagicMock("path", return_value="test_path")
conf = Config(mock_hass, {"type": "google_home"})
conf = Config(hass, {"type": "google_home"})
hass_storage[DATA_KEY] = {
"version": DATA_VERSION,
"key": DATA_KEY,
"data": {"1": "light.test2"},
}
with patch(
"homeassistant.components.emulated_hue.load_json",
return_value={"1": "light.test2"},
) as json_loader, patch(
"homeassistant.components.emulated_hue.save_json"
) as json_saver:
number = conf.entity_id_to_number("light.test")
assert number == "2"
await conf.async_setup()
assert json_saver.mock_calls[0][1][1] == {
"1": "light.test2",
"2": "light.test",
}
number = conf.entity_id_to_number("light.test")
assert number == "2"
assert json_saver.call_count == 1
assert json_loader.call_count == 1
async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
await hass.async_block_till_done()
assert hass_storage[DATA_KEY]["data"] == {
"1": "light.test2",
"2": "light.test",
}
number = conf.entity_id_to_number("light.test")
assert number == "2"
assert json_saver.call_count == 1
number = conf.entity_id_to_number("light.test")
assert number == "2"
number = conf.entity_id_to_number("light.test2")
assert number == "1"
assert json_saver.call_count == 1
number = conf.entity_id_to_number("light.test2")
assert number == "1"
entity_id = conf.number_to_entity_id("1")
assert entity_id == "light.test2"
entity_id = conf.number_to_entity_id("1")
assert entity_id == "light.test2"
def test_config_google_home_entity_id_to_number_altered():
async def test_config_google_home_entity_id_to_number_altered(hass, hass_storage):
"""Test config adheres to the type."""
mock_hass = Mock()
mock_hass.config.path = MagicMock("path", return_value="test_path")
conf = Config(mock_hass, {"type": "google_home"})
conf = Config(hass, {"type": "google_home"})
hass_storage[DATA_KEY] = {
"version": DATA_VERSION,
"key": DATA_KEY,
"data": {"21": "light.test2"},
}
with patch(
"homeassistant.components.emulated_hue.load_json",
return_value={"21": "light.test2"},
) as json_loader, patch(
"homeassistant.components.emulated_hue.save_json"
) as json_saver:
number = conf.entity_id_to_number("light.test")
assert number == "22"
assert json_saver.call_count == 1
assert json_loader.call_count == 1
await conf.async_setup()
assert json_saver.mock_calls[0][1][1] == {
"21": "light.test2",
"22": "light.test",
}
number = conf.entity_id_to_number("light.test")
assert number == "22"
number = conf.entity_id_to_number("light.test")
assert number == "22"
assert json_saver.call_count == 1
async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
await hass.async_block_till_done()
assert hass_storage[DATA_KEY]["data"] == {
"21": "light.test2",
"22": "light.test",
}
number = conf.entity_id_to_number("light.test2")
assert number == "21"
assert json_saver.call_count == 1
number = conf.entity_id_to_number("light.test")
assert number == "22"
entity_id = conf.number_to_entity_id("21")
assert entity_id == "light.test2"
number = conf.entity_id_to_number("light.test2")
assert number == "21"
entity_id = conf.number_to_entity_id("21")
assert entity_id == "light.test2"
def test_config_google_home_entity_id_to_number_empty():
async def test_config_google_home_entity_id_to_number_empty(hass, hass_storage):
"""Test config adheres to the type."""
mock_hass = Mock()
mock_hass.config.path = MagicMock("path", return_value="test_path")
conf = Config(mock_hass, {"type": "google_home"})
conf = Config(hass, {"type": "google_home"})
hass_storage[DATA_KEY] = {"version": DATA_VERSION, "key": DATA_KEY, "data": {}}
with patch(
"homeassistant.components.emulated_hue.load_json", return_value={}
) as json_loader, patch(
"homeassistant.components.emulated_hue.save_json"
) as json_saver:
number = conf.entity_id_to_number("light.test")
assert number == "1"
assert json_saver.call_count == 1
assert json_loader.call_count == 1
await conf.async_setup()
assert json_saver.mock_calls[0][1][1] == {"1": "light.test"}
number = conf.entity_id_to_number("light.test")
assert number == "1"
number = conf.entity_id_to_number("light.test")
assert number == "1"
assert json_saver.call_count == 1
async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
await hass.async_block_till_done()
assert hass_storage[DATA_KEY]["data"] == {"1": "light.test"}
number = conf.entity_id_to_number("light.test2")
assert number == "2"
assert json_saver.call_count == 2
number = conf.entity_id_to_number("light.test")
assert number == "1"
entity_id = conf.number_to_entity_id("2")
assert entity_id == "light.test2"
number = conf.entity_id_to_number("light.test2")
assert number == "2"
entity_id = conf.number_to_entity_id("2")
assert entity_id == "light.test2"
def test_config_alexa_entity_id_to_number():