Exemplo n.º 1
0
 def __init__(self, hass):
     """Initialize the dashboards collection."""
     super().__init__(
         storage.Store(hass, DASHBOARDS_STORAGE_VERSION,
                       DASHBOARDS_STORAGE_KEY),
         _LOGGER,
     )
Exemplo n.º 2
0
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up configured zones as well as Home Assistant zone if necessary."""
    component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass)
    id_manager = collection.IDManager()

    yaml_collection = collection.IDLessCollection(
        logging.getLogger(f"{__name__}.yaml_collection"), id_manager)
    collection.sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component,
                                     yaml_collection, Zone.from_yaml)

    storage_collection = ZoneStorageCollection(
        storage.Store(hass, STORAGE_VERSION, STORAGE_KEY),
        logging.getLogger(f"{__name__}.storage_collection"),
        id_manager,
    )
    collection.sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component,
                                     storage_collection, Zone)

    if config[DOMAIN]:
        # AIS dom config can be empty
        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(hass)

    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(
        hass,
        DOMAIN,
        SERVICE_RELOAD,
        reload_service_handler,
        schema=RELOAD_SERVICE_SCHEMA,
    )

    if component.get_entity("zone.home"):
        return True

    home_zone = Zone(_home_conf(hass))
    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(hass))

    hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)

    hass.data[DOMAIN] = storage_collection

    return True
async def test_removing_while_delay_in_progress(tmpdir):
    """Test removing while delay in progress."""

    loop = asyncio.get_event_loop()
    hass = await async_test_home_assistant(loop)

    test_dir = await hass.async_add_executor_job(tmpdir.mkdir, "storage")

    with patch.object(storage, "STORAGE_DIR", test_dir):
        real_store = storage.Store(hass, 1, "remove_me")

        await real_store.async_save({"delay": "no"})

        assert await hass.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 hass.async_add_executor_job(os.path.exists,
                                                     real_store.path)

        async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1))
        await hass.async_block_till_done()
        assert not await hass.async_add_executor_job(os.path.exists,
                                                     real_store.path)
        await hass.async_stop()
Exemplo n.º 4
0
 def __init__(self, hass: HomeAssistant, ll_config: LovelaceConfig):
     """Initialize the storage collection."""
     super().__init__(
         storage.Store(hass, RESOURCES_STORAGE_VERSION, RESOURCE_STORAGE_KEY),
         _LOGGER,
     )
     self.ll_config = ll_config
Exemplo n.º 5
0
 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 {}
     )
Exemplo n.º 6
0
async def test_not_delayed_saving_while_stopping(hass, hass_storage):
    """Test delayed saves don't write after the stop event has fired."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    hass.state = CoreState.stopping

    store.async_delay_save(lambda: MOCK_DATA, 1)
    async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2))
    await hass.async_block_till_done()
    assert store.key not in hass_storage
Exemplo n.º 7
0
async def test_saving_load_round_trip(tmpdir):
    """Test saving and loading round trip."""
    loop = asyncio.get_running_loop()
    hass = await async_test_home_assistant(loop)

    hass.config.config_dir = await hass.async_add_executor_job(
        tmpdir.mkdir, "temp_storage")

    class NamedTupleSubclass(NamedTuple):
        """A NamedTuple subclass."""

        name: str

    nts = NamedTupleSubclass("a")

    data = {
        "named_tuple_subclass": nts,
        "rgb_color": RGBColor(255, 255, 0),
        "set": {1, 2, 3},
        "list": [1, 2, 3],
        "tuple": (1, 2, 3),
        "dict_with_int": {
            1: 1,
            2: 2
        },
        "dict_with_named_tuple": {
            1: nts,
            2: nts
        },
    }

    store = storage.Store(hass,
                          MOCK_VERSION_2,
                          MOCK_KEY,
                          minor_version=MOCK_MINOR_VERSION_1)
    await store.async_save(data)
    load = await store.async_load()
    assert load == {
        "dict_with_int": {
            "1": 1,
            "2": 2
        },
        "dict_with_named_tuple": {
            "1": ["a"],
            "2": ["a"]
        },
        "list": [1, 2, 3],
        "named_tuple_subclass": ["a"],
        "rgb_color": [255, 255, 0],
        "set": [1, 2, 3],
        "tuple": [1, 2, 3],
    }

    await hass.async_stop(force=True)
Exemplo n.º 8
0
async def test_custom_encoder(hass):
    """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(hass, MOCK_VERSION, MOCK_KEY, encoder=JSONEncoder)
    await store.async_save(Mock())
    data = await store.async_load()
    assert data == "9"
Exemplo n.º 9
0
 async def async_setup(self) -> None:
     """Set up tracking and migrate to storage."""
     hass = self.hass
     self.store = storage.Store(hass, DATA_VERSION,
                                DATA_KEY)  # type: ignore[arg-type]
     numbers_path = hass.config.path(NUMBERS_FILE)
     self.numbers = (await storage.async_migrator(hass, numbers_path,
                                                  self.store) or {})
     async_track_state_added_domain(hass, self.track_domains,
                                    self._clear_exposed_cache)
     async_track_state_removed_domain(hass, self.track_domains,
                                      self._clear_exposed_cache)
Exemplo n.º 10
0
    def __init__(self, hass, 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__(hass, url_path, config)

        self._store = storage.Store(hass, CONFIG_STORAGE_VERSION, storage_key)
        self._data = None
Exemplo n.º 11
0
async def test_saving_on_stop(hass, hass_storage):
    """Test delayed saves trigger when we quit Home Assistant."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    store.async_delay_save(lambda: MOCK_DATA, 1)
    assert store.key not in hass_storage

    hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
    await hass.async_block_till_done()
    assert hass_storage[store.key] == {
        "version": MOCK_VERSION,
        "key": MOCK_KEY,
        "data": MOCK_DATA,
    }
Exemplo n.º 12
0
async def test_saving_on_stop(hass, hass_storage):
    """Test delayed saves trigger when we quit Home Assistant."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    await store.async_save(MOCK_DATA, delay=1)
    assert store.key not in hass_storage

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    await hass.async_block_till_done()
    assert hass_storage[store.key] == {
        'version': MOCK_VERSION,
        'key': MOCK_KEY,
        'data': MOCK_DATA,
    }
Exemplo n.º 13
0
async def start_http_server_and_save_config(hass: HomeAssistant, conf: dict,
                                            server: HomeAssistantHTTP) -> None:
    """Startup the http server and save the config."""
    await server.start()  # type: ignore

    # If we are set up successful, we store the HTTP settings for safe mode.
    store = storage.Store(hass, 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]
        ]

    await store.async_save(conf)
Exemplo n.º 14
0
async def test_not_delayed_saving_after_stopping(hass, hass_storage):
    """Test delayed saves don't write after stop if issued before stopping Home Assistant."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    store.async_delay_save(lambda: MOCK_DATA, 10)
    assert store.key not in hass_storage

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    hass.state = CoreState.stopping
    await hass.async_block_till_done()
    assert store.key not in hass_storage

    async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=15))
    await hass.async_block_till_done()
    assert store.key not in hass_storage
Exemplo n.º 15
0
async def start_http_server_and_save_config(hass: HomeAssistant, conf: dict,
                                            server: HomeAssistantHTTP) -> 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(hass, STORAGE_VERSION, STORAGE_KEY)

    if CONF_TRUSTED_PROXIES in conf:
        conf[CONF_TRUSTED_PROXIES] = [
            str(cast(Union[IPv4Network, IPv6Network], ip).network_address)
            for ip in conf[CONF_TRUSTED_PROXIES]
        ]

    store.async_delay_save(lambda: conf, SAVE_DELAY)
Exemplo n.º 16
0
    async def start_server(event):
        """Start the server."""
        hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
        await server.start()

        # If we are set up successful, we store the HTTP settings for safe mode.
        store = storage.Store(hass, 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)
Exemplo n.º 17
0
async def async_setup_platform(hass, config, add_devices, discovery_info=None):
    """Configure Heatzy API using Home Assistant configuration and fetch all Heatzy devices."""
    # retrieve platform config
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)

    session = aiohttp_client.async_get_clientsession(hass)
    store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)

    authenticator = HeatzyAuthenticator(session, store, username, password)
    api = HeatzyAPI(session, authenticator)

    # fetch configured Heatzy devices
    devices = await api.async_get_devices()
    # add all Heatzy devices with HA implementation to home assistant
    add_devices(filter(None.__ne__, map(setup_heatzy_device(api), devices)))
    return True
Exemplo n.º 18
0
async def test_saving_on_final_write(hass, hass_storage):
    """Test delayed saves trigger when we quit Home Assistant."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    store.async_delay_save(lambda: MOCK_DATA, 5)
    assert store.key not in hass_storage

    hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
    hass.state = CoreState.stopping
    await hass.async_block_till_done()

    async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=10))
    await hass.async_block_till_done()
    assert store.key not in hass_storage

    hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
    await hass.async_block_till_done()
    assert hass_storage[store.key] == {
        "version": MOCK_VERSION,
        "key": MOCK_KEY,
        "data": MOCK_DATA,
    }
Exemplo n.º 19
0
async def test_storage_collection(hass):
    """Test storage collection."""
    store = storage.Store(hass, 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(hass, 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"},
        ]
    }
Exemplo n.º 20
0
async def async_get_last_config(hass: HomeAssistant) -> dict | None:
    """Return the last known working config."""
    store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
    return cast(Optional[dict], await store.async_load())
Exemplo n.º 21
0
async def async_setup_entry(hass, entry):
    """Set up Almond config entry."""
    websession = aiohttp_client.async_get_clientsession(hass)

    if entry.data["type"] == TYPE_LOCAL:
        auth = AlmondLocalAuth(entry.data["host"], websession)
    else:
        # OAuth2
        implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
            hass, entry)
        oauth_session = config_entry_oauth2_flow.OAuth2Session(
            hass, entry, implementation)
        auth = AlmondOAuth(entry.data["host"], websession, oauth_session)

    api = WebAlmondAPI(auth)
    agent = AlmondAgent(hass, api, entry)

    # Hass.io does its own configuration of Almond.
    if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL:
        conversation.async_set_agent(hass, agent)
        return True

    # Configure Almond to connect to Home Assistant
    store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
    data = await store.async_load()

    if data is None:
        data = {}

    user = None
    if "almond_user" in data:
        user = await hass.auth.async_get_user(data["almond_user"])

    if user is None:
        user = await hass.auth.async_create_system_user(
            "Almond", [GROUP_ID_ADMIN])
        data["almond_user"] = user.id
        await store.async_save(data)

    refresh_token = await hass.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 = hass.auth.async_create_access_token(refresh_token)

    # Store token in Almond
    try:
        with async_timeout.timeout(10):
            await api.async_create_device({
                "kind":
                "io.home-assistant",
                "hassUrl":
                hass.config.api.base_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 hass.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 hass.auth.async_remove_refresh_token(token)

    conversation.async_set_agent(hass, agent)
    return True
Exemplo n.º 22
0
def store(hass):
    """Fixture of a store that prevents writing on Home Assistant stop."""
    yield storage.Store(hass, MOCK_VERSION, MOCK_KEY)
Exemplo n.º 23
0
async def test_not_saving_while_stopping(hass, hass_storage):
    """Test saves don't write when stopping Home Assistant."""
    store = storage.Store(hass, MOCK_VERSION, MOCK_KEY)
    hass.state = CoreState.stopping
    await store.async_save(MOCK_DATA)
    assert store.key not in hass_storage
Exemplo n.º 24
0
def store(hass):
    """Fixture of a store that prevents writing on Safegate Pro stop."""
    yield storage.Store(hass, MOCK_VERSION, MOCK_KEY)
Exemplo n.º 25
0
async def _configure_almond_for_ha(hass: HomeAssistant,
                                   entry: config_entries.ConfigEntry,
                                   api: WebAlmondAPI):
    """Configure Almond to connect to HA."""
    try:
        if entry.data["type"] == TYPE_OAUTH2:
            # If we're connecting over OAuth2, we will only set up connection
            # with Home Assistant if we're remotely accessible.
            hass_url = network.get_url(hass,
                                       allow_internal=False,
                                       prefer_cloud=True)
        else:
            hass_url = network.get_url(hass)
    except network.NoURLAvailableError:
        # If no URL is available, we're not going to configure Almond to connect to HA.
        return

    _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s",
                  hass_url)
    store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
    data = await store.async_load()

    if data is None:
        data = {}

    user = None
    if "almond_user" in data:
        user = await hass.auth.async_get_user(data["almond_user"])

    if user is None:
        user = await hass.auth.async_create_system_user(
            "Almond", [GROUP_ID_ADMIN])
        data["almond_user"] = user.id
        await store.async_save(data)

    refresh_token = await hass.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 = hass.auth.async_create_access_token(refresh_token)

    # Store token in Almond
    try:
        with async_timeout.timeout(30):
            await api.async_create_device({
                "kind":
                "io.home-assistant",
                "hassUrl":
                hass_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 hass.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 hass.auth.async_remove_refresh_token(token)
Exemplo n.º 26
0
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
    """Set up configured zones as well as Home Assistant zone if necessary."""
    component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass)
    id_manager = collection.IDManager()

    yaml_collection = 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(hass, 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 config[DOMAIN]:
        # AIS dom config can be empty
        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(hass)

    async def _collection_changed(change_type: str, item_id: str, config: 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(hass)
        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[DOMAIN])

    service.async_register_admin_service(
        hass,
        DOMAIN,
        SERVICE_RELOAD,
        reload_service_handler,
        schema=RELOAD_SERVICE_SCHEMA,
    )

    if component.get_entity("zone.home"):
        return True

    home_zone = Zone(
        _home_conf(hass),
        True,
    )
    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(hass))

    hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)

    hass.data[DOMAIN] = storage_collection

    return True
Exemplo n.º 27
0
def store_v_2_1(hass):
    """Fixture of a store that prevents writing on Home Assistant stop."""
    return storage.Store(hass,
                         MOCK_VERSION_2,
                         MOCK_KEY,
                         minor_version=MOCK_MINOR_VERSION_1)
Exemplo n.º 28
0
 def __init__(self, hass: HomeAssistant) -> None:
     """Initialize energy manager."""
     self._hass = hass
     self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY)
     self.data: EnergyPreferences | None = None
     self._update_listeners: list[Callable[[], Awaitable]] = []
Exemplo n.º 29
0
async def test_storage_collection_websocket(hass, hass_ws_client):
    """Test exposing a storage collection via websockets."""
    store = storage.Store(hass, 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(hass)

    client = await hass_ws_client(hass)

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