def __init__(self, opp): """Initialize the dashboards collection.""" super().__init__( storage.Store(opp, DASHBOARDS_STORAGE_VERSION, DASHBOARDS_STORAGE_KEY), _LOGGER, )
async def test_removing_while_delay_in_progress(tmpdir): """Test removing while delay in progress.""" loop = asyncio.get_event_loop() opp = await async_test_open_peer_power(loop) test_dir = await opp.async_add_executor_job(tmpdir.mkdir, "storage") with patch.object(storage, "STORAGE_DIR", test_dir): real_store = storage.Store(opp, 1, "remove_me") await real_store.async_save({"delay": "no"}) assert await opp.async_add_executor_job(os.path.exists, real_store.path) real_store.async_delay_save(lambda: {"delay": "yes"}, 1) await real_store.async_remove() assert not await opp.async_add_executor_job(os.path.exists, real_store.path) async_fire_time_changed(opp, dt.utcnow() + timedelta(seconds=1)) await opp.async_block_till_done() assert not await opp.async_add_executor_job(os.path.exists, real_store.path) await opp.async_stop()
def __init__(self, opp: OpenPeerPower, ll_config: LovelaceConfig) -> None: """Initialize the storage collection.""" super().__init__( storage.Store(opp, RESOURCES_STORAGE_VERSION, RESOURCE_STORAGE_KEY), _LOGGER, ) self.ll_config = ll_config
async def async_setup(opp: OpenPeerPower, config: dict) -> bool: """Set up configured zones as well as Open Peer Power zone if necessary.""" component = entity_component.EntityComponent(_LOGGER, DOMAIN, opp) id_manager = collection.IDManager() yaml_collection = collection.IDLessCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager) collection.sync_entity_lifecycle(opp, DOMAIN, DOMAIN, component, yaml_collection, Zone.from_yaml) storage_collection = ZoneStorageCollection( storage.Store(opp, STORAGE_VERSION, STORAGE_KEY), logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) collection.sync_entity_lifecycle(opp, DOMAIN, DOMAIN, component, storage_collection, Zone) if config[DOMAIN]: await yaml_collection.async_load(config[DOMAIN]) await storage_collection.async_load() collection.StorageCollectionWebsocket(storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS).async_setup(opp) async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all zones and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: return await yaml_collection.async_load(conf[DOMAIN]) service.async_register_admin_service( opp, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA, ) if component.get_entity("zone.home"): return True home_zone = Zone(_home_conf(opp)) home_zone.entity_id = ENTITY_ID_HOME await component.async_add_entities([home_zone]) async def core_config_updated(_: Event) -> None: """Handle core config updated.""" await home_zone.async_update_config(_home_conf(opp)) opp.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) opp.data[DOMAIN] = storage_collection return True
async def test_not_delayed_saving_while_stopping(opp, opp_storage): """Test delayed saves don't write after the stop event has fired.""" store = storage.Store(opp, MOCK_VERSION, MOCK_KEY) opp.bus.async_fire(EVENT_OPENPEERPOWER_STOP) await opp.async_block_till_done() opp.state = CoreState.stopping store.async_delay_save(lambda: MOCK_DATA, 1) async_fire_time_changed(opp, dt.utcnow() + timedelta(seconds=2)) await opp.async_block_till_done() assert store.key not in opp_storage
async def test_custom_encoder(opp): """Test we can save and load data.""" class JSONEncoder(json.JSONEncoder): """Mock JSON encoder.""" def default(self, o): """Mock JSON encode method.""" return "9" store = storage.Store(opp, MOCK_VERSION, MOCK_KEY, encoder=JSONEncoder) await store.async_save(Mock()) data = await store.async_load() assert data == "9"
def __init__(self, opp, config): """Initialize Lovelace config based on storage helper.""" if config is None: url_path = None storage_key = CONFIG_STORAGE_KEY_DEFAULT else: url_path = config[CONF_URL_PATH] storage_key = CONFIG_STORAGE_KEY.format(config["id"]) super().__init__(opp, url_path, config) self._store = storage.Store(opp, CONFIG_STORAGE_VERSION, storage_key) self._data = None
async def test_not_delayed_saving_after_stopping(opp, opp_storage): """Test delayed saves don't write after stop if issued before stopping Open Peer Power.""" store = storage.Store(opp, MOCK_VERSION, MOCK_KEY) store.async_delay_save(lambda: MOCK_DATA, 10) assert store.key not in opp_storage opp.bus.async_fire(EVENT_OPENPEERPOWER_STOP) opp.state = CoreState.stopping await opp.async_block_till_done() assert store.key not in opp_storage async_fire_time_changed(opp, dt.utcnow() + timedelta(seconds=15)) await opp.async_block_till_done() assert store.key not in opp_storage
async def start_http_server_and_save_config(opp: OpenPeerPower, conf: dict, server: OpenPeerPowerHTTP) -> None: """Startup the http server and save the config.""" await server.start() # If we are set up successful, we store the HTTP settings for safe mode. store = storage.Store(opp, STORAGE_VERSION, STORAGE_KEY) if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ str(ip.network_address) for ip in conf[CONF_TRUSTED_PROXIES] ] store.async_delay_save(lambda: conf, SAVE_DELAY)
async def start_server(event): """Start the server.""" opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP, stop_server) await server.start() # If we are set up successful, we store the HTTP settings for safe mode. store = storage.Store(opp, STORAGE_VERSION, STORAGE_KEY) if CONF_TRUSTED_PROXIES in conf: conf_to_save = dict(conf) conf_to_save[CONF_TRUSTED_PROXIES] = [ str(ip.network_address) for ip in conf_to_save[CONF_TRUSTED_PROXIES] ] else: conf_to_save = conf await store.async_save(conf_to_save)
async def test_saving_on_final_write(opp, opp_storage): """Test delayed saves trigger when we quit Open Peer Power.""" store = storage.Store(opp, MOCK_VERSION, MOCK_KEY) store.async_delay_save(lambda: MOCK_DATA, 5) assert store.key not in opp_storage opp.bus.async_fire(EVENT_OPENPEERPOWER_STOP) opp.state = CoreState.stopping await opp.async_block_till_done() async_fire_time_changed(opp, dt.utcnow() + timedelta(seconds=10)) await opp.async_block_till_done() assert store.key not in opp_storage opp.bus.async_fire(EVENT_OPENPEERPOWER_FINAL_WRITE) await opp.async_block_till_done() assert opp_storage[store.key] == { "version": MOCK_VERSION, "key": MOCK_KEY, "data": MOCK_DATA, }
async def _configure_almond_for_ha(opp: OpenPeerPower, entry: config_entries.ConfigEntry, api: WebAlmondAPI): """Configure Almond to connect to HA.""" if entry.data["type"] == TYPE_OAUTH2: # If we're connecting over OAuth2, we will only set up connection # with Open Peer Power if we're remotely accessible. opp_url = network.async_get_external_url(opp) else: opp_url = opp.config.api.base_url # If opp_url is None, we're not going to configure Almond to connect to HA. if opp_url is None: return _LOGGER.debug("Configuring Almond to connect to Open Peer Power at %s", opp_url) store = storage.Store(opp, STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() if data is None: data = {} user = None if "almond_user" in data: user = await opp.auth.async_get_user(data["almond_user"]) if user is None: user = await opp.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) data["almond_user"] = user.id await store.async_save(data) refresh_token = await opp.auth.async_create_refresh_token( user, # Almond will be fine as long as we restart once every 5 years access_token_expiration=timedelta(days=365 * 5), ) # Create long lived access token access_token = opp.auth.async_create_access_token(refresh_token) # Store token in Almond try: with async_timeout.timeout(30): await api.async_create_device({ "kind": "io.open-peer-power", "oppUrl": opp_url, "accessToken": access_token, "refreshToken": "", # 5 years from now in ms. "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, }) except (asyncio.TimeoutError, ClientError) as err: if isinstance(err, asyncio.TimeoutError): msg = "Request timeout" else: msg = err _LOGGER.warning("Unable to configure Almond: %s", msg) await opp.auth.async_remove_refresh_token(refresh_token) raise ConfigEntryNotReady # Clear all other refresh tokens for token in list(user.refresh_tokens.values()): if token.id != refresh_token.id: await opp.auth.async_remove_refresh_token(token)
def store(opp): """Fixture of a store that prevents writing on Open Peer Power stop.""" yield storage.Store(opp, MOCK_VERSION, MOCK_KEY)
async def test_storage_collection_websocket(opp, opp_ws_client): """Test exposing a storage collection via websockets.""" store = storage.Store(opp, 1, "test-data") coll = MockStorageCollection(store, _LOGGER) changes = track_changes(coll) collection.StorageCollectionWebsocket( coll, "test_item/collection", "test_item", { vol.Required("name"): str, vol.Required("immutable_string"): str }, { vol.Optional("name"): str }, ).async_setup(opp) client = await opp_ws_client(opp) # Create invalid await client.send_json({ "id": 1, "type": "test_item/collection/create", "name": 1, # Forgot to add immutable_string }) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "invalid_format" assert len(changes) == 0 # Create await client.send_json({ "id": 2, "type": "test_item/collection/create", "name": "Initial Name", "immutable_string": "no-changes", }) response = await client.receive_json() assert response["success"] assert response["result"] == { "id": "initial_name", "name": "Initial Name", "immutable_string": "no-changes", } assert len(changes) == 1 assert changes[0] == (collection.CHANGE_ADDED, "initial_name", response["result"]) # List await client.send_json({"id": 3, "type": "test_item/collection/list"}) response = await client.receive_json() assert response["success"] assert response["result"] == [{ "id": "initial_name", "name": "Initial Name", "immutable_string": "no-changes", }] assert len(changes) == 1 # Update invalid data await client.send_json({ "id": 4, "type": "test_item/collection/update", "test_item_id": "initial_name", "immutable_string": "no-changes", }) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "invalid_format" assert len(changes) == 1 # Update invalid item await client.send_json({ "id": 5, "type": "test_item/collection/update", "test_item_id": "non-existing", "name": "Updated name", }) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "not_found" assert len(changes) == 1 # Update await client.send_json({ "id": 6, "type": "test_item/collection/update", "test_item_id": "initial_name", "name": "Updated name", }) response = await client.receive_json() assert response["success"] assert response["result"] == { "id": "initial_name", "name": "Updated name", "immutable_string": "no-changes", } assert len(changes) == 2 assert changes[1] == (collection.CHANGE_UPDATED, "initial_name", response["result"]) # Delete invalid ID await client.send_json({ "id": 7, "type": "test_item/collection/update", "test_item_id": "non-existing" }) response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "not_found" assert len(changes) == 2 # Delete await client.send_json({ "id": 8, "type": "test_item/collection/delete", "test_item_id": "initial_name" }) response = await client.receive_json() assert response["success"] assert len(changes) == 3 assert changes[2] == ( collection.CHANGE_REMOVED, "initial_name", { "id": "initial_name", "immutable_string": "no-changes", "name": "Updated name", }, )
async def test_not_saving_while_stopping(opp, opp_storage): """Test saves don't write when stopping Open Peer Power.""" store = storage.Store(opp, MOCK_VERSION, MOCK_KEY) opp.state = CoreState.stopping await store.async_save(MOCK_DATA) assert store.key not in opp_storage
async def async_setup(opp: OpenPeerPower, config: Dict) -> bool: """Set up configured zones as well as Open Peer Power zone if necessary.""" component = entity_component.EntityComponent(_LOGGER, DOMAIN, opp) id_manager = collection.IDManager() yaml_collection = IDLessCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager) collection.attach_entity_component_collection( component, yaml_collection, lambda conf: Zone(conf, False)) storage_collection = ZoneStorageCollection( storage.Store(opp, STORAGE_VERSION, STORAGE_KEY), logging.getLogger(f"{__name__}_storage_collection"), id_manager, ) collection.attach_entity_component_collection( component, storage_collection, lambda conf: Zone(conf, True)) if DOMAIN in config: await yaml_collection.async_load(config[DOMAIN]) await storage_collection.async_load() collection.StorageCollectionWebsocket(storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS).async_setup(opp) async def _collection_changed(change_type: str, item_id: str, config: Optional[Dict]) -> None: """Handle a collection change: clean up entity registry on removals.""" if change_type != collection.CHANGE_REMOVED: return ent_reg = await entity_registry.async_get_registry(opp) ent_reg.async_remove( cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id))) storage_collection.async_add_listener(_collection_changed) async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all zones and load new ones from config.""" conf = await component.async_prepare_reload(skip_reset=True) if conf is None: return await yaml_collection.async_load(conf.get(DOMAIN, [])) service.async_register_admin_service( opp, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA, ) if component.get_entity("zone.home"): return True home_zone = Zone( _home_conf(opp), True, ) home_zone.entity_id = ENTITY_ID_HOME await component.async_add_entities([home_zone]) # type: ignore async def core_config_updated(_: Event) -> None: """Handle core config updated.""" await home_zone.async_update_config(_home_conf(opp)) opp.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) opp.data[DOMAIN] = storage_collection return True
async def async_get_last_config(opp: OpenPeerPower) -> Optional[dict]: """Return the last known working config.""" store = storage.Store(opp, STORAGE_VERSION, STORAGE_KEY) return cast(Optional[dict], await store.async_load())
async def async_setup(self): """Set up and migrate to storage.""" self.store = storage.Store(self.opp, DATA_VERSION, DATA_KEY) self.numbers = (await storage.async_migrator( self.opp, self.opp.config.path(NUMBERS_FILE), self.store) or {})
async def test_storage_collection(opp): """Test storage collection.""" store = storage.Store(opp, 1, "test-data") await store.async_save({ "items": [ { "id": "mock-1", "name": "Mock 1", "data": 1 }, { "id": "mock-2", "name": "Mock 2", "data": 2 }, ] }) id_manager = collection.IDManager() coll = MockStorageCollection(store, _LOGGER, id_manager) changes = track_changes(coll) await coll.async_load() assert id_manager.has_id("mock-1") assert id_manager.has_id("mock-2") assert len(changes) == 2 assert changes[0] == ( collection.CHANGE_ADDED, "mock-1", { "id": "mock-1", "name": "Mock 1", "data": 1 }, ) assert changes[1] == ( collection.CHANGE_ADDED, "mock-2", { "id": "mock-2", "name": "Mock 2", "data": 2 }, ) item = await coll.async_create_item({"name": "Mock 3"}) assert item["id"] == "mock_3" assert len(changes) == 3 assert changes[2] == ( collection.CHANGE_ADDED, "mock_3", { "id": "mock_3", "name": "Mock 3" }, ) updated_item = await coll.async_update_item("mock-2", {"name": "Mock 2 updated"}) assert id_manager.has_id("mock-2") assert updated_item == { "id": "mock-2", "name": "Mock 2 updated", "data": 2 } assert len(changes) == 4 assert changes[3] == (collection.CHANGE_UPDATED, "mock-2", updated_item) with pytest.raises(ValueError): await coll.async_update_item("mock-2", {"id": "mock-2-updated"}) assert id_manager.has_id("mock-2") assert not id_manager.has_id("mock-2-updated") assert len(changes) == 4 await flush_store(store) assert await storage.Store(opp, 1, "test-data").async_load() == { "items": [ { "id": "mock-1", "name": "Mock 1", "data": 1 }, { "id": "mock-2", "name": "Mock 2 updated", "data": 2 }, { "id": "mock_3", "name": "Mock 3" }, ] }