Skip to content

Commit

Permalink
Migrate emulate_hue to use storage to fix I/O in event loop (home-ass…
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored May 12, 2021
1 parent 72f342a commit 70961c7
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 105 deletions.
34 changes: 17 additions & 17 deletions homeassistant/components/emulated_hue/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,9 +11,8 @@
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,
Expand All @@ -34,6 +32,9 @@
_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"
Expand Down Expand Up @@ -155,6 +156,7 @@ async def start_emulated_hue_bridge(event):
nonlocal protocol
nonlocal site
nonlocal runner
await config.async_setup()

_, protocol = await listen

Expand Down Expand Up @@ -189,6 +191,7 @@ def __init__(self, hass, conf):
self.hass = hass
self.type = conf.get(CONF_TYPE)
self.numbers = None
self.store = None
self.cached_states = {}
self._exposed_cache = {}

Expand Down Expand Up @@ -257,14 +260,21 @@ def __init__(self, hass, conf):
# 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:
Expand All @@ -274,17 +284,14 @@ def entity_id_to_number(self, entity_id):
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):
"""Convert unique number to entity id."""
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)
Expand Down Expand Up @@ -338,10 +345,3 @@ def _is_entity_exposed(self, entity):
return True

return False


def _load_json(filename):
"""Load JSON, handling invalid syntax."""
with suppress(HomeAssistantError):
return load_json(filename)
return {}
171 changes: 83 additions & 88 deletions tests/components/emulated_hue/test_init.py
Original file line number Diff line number Diff line change
@@ -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"})

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

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"
assert json_saver.call_count == 1

number = conf.entity_id_to_number("light.test2")
assert number == "21"
assert json_saver.call_count == 1

entity_id = conf.number_to_entity_id("21")
assert entity_id == "light.test2"


def test_config_google_home_entity_id_to_number_empty():
conf = Config(hass, {"type": "google_home"})
hass_storage[DATA_KEY] = {
"version": DATA_VERSION,
"key": DATA_KEY,
"data": {"21": "light.test2"},
}

await conf.async_setup()

number = conf.entity_id_to_number("light.test")
assert number == "22"

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.test")
assert number == "22"

number = conf.entity_id_to_number("light.test2")
assert number == "21"

entity_id = conf.number_to_entity_id("21")
assert entity_id == "light.test2"


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"})

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

assert json_saver.mock_calls[0][1][1] == {"1": "light.test"}

number = conf.entity_id_to_number("light.test")
assert number == "1"
assert json_saver.call_count == 1

number = conf.entity_id_to_number("light.test2")
assert number == "2"
assert json_saver.call_count == 2

entity_id = conf.number_to_entity_id("2")
assert entity_id == "light.test2"
conf = Config(hass, {"type": "google_home"})
hass_storage[DATA_KEY] = {"version": DATA_VERSION, "key": DATA_KEY, "data": {}}

await conf.async_setup()

number = conf.entity_id_to_number("light.test")
assert number == "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.test")
assert number == "1"

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():
Expand Down

0 comments on commit 70961c7

Please sign in to comment.