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

This commit is contained in:
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.""" """Support for local control of entities by emulating a Philips Hue bridge."""
from contextlib import suppress
import logging import logging
from aiohttp import web from aiohttp import web
@ -12,9 +11,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import storage
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .hue_api import ( from .hue_api import (
HueAllGroupsStateView, HueAllGroupsStateView,
@ -34,6 +32,9 @@ DOMAIN = "emulated_hue"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NUMBERS_FILE = "emulated_hue_ids.json" NUMBERS_FILE = "emulated_hue_ids.json"
DATA_KEY = "emulated_hue.ids"
DATA_VERSION = "1"
SAVE_DELAY = 60
CONF_ADVERTISE_IP = "advertise_ip" CONF_ADVERTISE_IP = "advertise_ip"
CONF_ADVERTISE_PORT = "advertise_port" CONF_ADVERTISE_PORT = "advertise_port"
@ -155,6 +156,7 @@ async def async_setup(hass, yaml_config):
nonlocal protocol nonlocal protocol
nonlocal site nonlocal site
nonlocal runner nonlocal runner
await config.async_setup()
_, protocol = await listen _, protocol = await listen
@ -189,6 +191,7 @@ class Config:
self.hass = hass self.hass = hass
self.type = conf.get(CONF_TYPE) self.type = conf.get(CONF_TYPE)
self.numbers = None self.numbers = None
self.store = None
self.cached_states = {} self.cached_states = {}
self._exposed_cache = {} self._exposed_cache = {}
@ -257,14 +260,21 @@ class Config:
# for compatibility with older installations. # for compatibility with older installations.
self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) 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): def entity_id_to_number(self, entity_id):
"""Get a unique number for the entity id.""" """Get a unique number for the entity id."""
if self.type == TYPE_ALEXA: if self.type == TYPE_ALEXA:
return entity_id return entity_id
if self.numbers is None:
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
for number, ent_id in self.numbers.items(): for number, ent_id in self.numbers.items():
if entity_id == ent_id: if entity_id == ent_id:
@ -274,7 +284,7 @@ class Config:
if self.numbers: if self.numbers:
number = str(max(int(k) for k in self.numbers) + 1) number = str(max(int(k) for k in self.numbers) + 1)
self.numbers[number] = entity_id 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 return number
def number_to_entity_id(self, number): def number_to_entity_id(self, number):
@ -282,9 +292,6 @@ class Config:
if self.type == TYPE_ALEXA: if self.type == TYPE_ALEXA:
return number return number
if self.numbers is None:
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
assert isinstance(number, str) assert isinstance(number, str)
return self.numbers.get(number) return self.numbers.get(number)
@ -338,10 +345,3 @@ class Config:
return True return True
return False 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.""" """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.""" """Test config adheres to the type."""
mock_hass = Mock() conf = Config(hass, {"type": "google_home"})
mock_hass.config.path = MagicMock("path", return_value="test_path") hass_storage[DATA_KEY] = {
conf = Config(mock_hass, {"type": "google_home"}) "version": DATA_VERSION,
"key": DATA_KEY,
"data": {"1": "light.test2"},
}
with patch( await conf.async_setup()
"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"
assert json_saver.mock_calls[0][1][1] == { number = conf.entity_id_to_number("light.test")
"1": "light.test2", assert number == "2"
"2": "light.test",
}
assert json_saver.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
assert json_loader.call_count == 1 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") number = conf.entity_id_to_number("light.test")
assert number == "2" assert number == "2"
assert json_saver.call_count == 1
number = conf.entity_id_to_number("light.test2") number = conf.entity_id_to_number("light.test2")
assert number == "1" assert number == "1"
assert json_saver.call_count == 1
entity_id = conf.number_to_entity_id("1") entity_id = conf.number_to_entity_id("1")
assert entity_id == "light.test2" 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.""" """Test config adheres to the type."""
mock_hass = Mock() conf = Config(hass, {"type": "google_home"})
mock_hass.config.path = MagicMock("path", return_value="test_path") hass_storage[DATA_KEY] = {
conf = Config(mock_hass, {"type": "google_home"}) "version": DATA_VERSION,
"key": DATA_KEY,
"data": {"21": "light.test2"},
}
with patch( await conf.async_setup()
"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
assert json_saver.mock_calls[0][1][1] == { number = conf.entity_id_to_number("light.test")
"21": "light.test2", assert number == "22"
"22": "light.test",
}
number = conf.entity_id_to_number("light.test") async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
assert number == "22" await hass.async_block_till_done()
assert json_saver.call_count == 1 assert hass_storage[DATA_KEY]["data"] == {
"21": "light.test2",
"22": "light.test",
}
number = conf.entity_id_to_number("light.test2") number = conf.entity_id_to_number("light.test")
assert number == "21" assert number == "22"
assert json_saver.call_count == 1
entity_id = conf.number_to_entity_id("21") number = conf.entity_id_to_number("light.test2")
assert entity_id == "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.""" """Test config adheres to the type."""
mock_hass = Mock() conf = Config(hass, {"type": "google_home"})
mock_hass.config.path = MagicMock("path", return_value="test_path") hass_storage[DATA_KEY] = {"version": DATA_VERSION, "key": DATA_KEY, "data": {}}
conf = Config(mock_hass, {"type": "google_home"})
with patch( await conf.async_setup()
"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
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") async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY))
assert number == "1" await hass.async_block_till_done()
assert json_saver.call_count == 1 assert hass_storage[DATA_KEY]["data"] == {"1": "light.test"}
number = conf.entity_id_to_number("light.test2") number = conf.entity_id_to_number("light.test")
assert number == "2" assert number == "1"
assert json_saver.call_count == 2
entity_id = conf.number_to_entity_id("2") number = conf.entity_id_to_number("light.test2")
assert entity_id == "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(): def test_config_alexa_entity_id_to_number():