From 6e56684c9dc75bf068e0e83e0838e0cb05b5b350 Mon Sep 17 00:00:00 2001 From: Michael Shaffer Date: Mon, 21 Sep 2026 14:52:08 +0000 Subject: [PATCH 1/2] fix: keep Learn IR visible when no remotes are configured After the infrared platform release, many setups have emitters without remotes. Learn IR was hidden in that case, leaving iTach users with no pinhole capture path. Always show the option and create a remote during learn when none exist. Docs: README Learn IR subsection, CONTEXT menu/translation notes. Signed-off-by: Michael Shaffer --- CONTEXT.md | 6 +- README.md | 17 +- .../globalcache_itach/config_flow.py | 262 ++++++++++++------ .../globalcache_itach/options_menu.py | 43 +++ .../globalcache_itach/strings.json | 7 +- .../globalcache_itach/translations/en.json | 7 +- tests/test_options_menu.py | 36 +++ 7 files changed, 276 insertions(+), 102 deletions(-) create mode 100644 custom_components/globalcache_itach/options_menu.py create mode 100644 tests/test_options_menu.py diff --git a/CONTEXT.md b/CONTEXT.md index 4c42de9..93df7f5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -70,7 +70,7 @@ Matching uses **unique_id** patterns (`{entry_id}_relay_*`, `{entry_id}_serial_* 2. **Options flow menus** — some steps use **`vol.In({...})`** instead of **`SelectSelector`** for broader HA compatibility. -3. **Translations** — `options.step.remote_commands` needs **`description`** + **`{hint}`** placeholder. **`options.step.init.data.next`** in `strings.json`. IR/serial command JSON uses **`TextSelector(multiline=True)`** (textarea), not a single-line field. Learn IR uses **`learn_ir`** / **`learn_ir_capture`** with placeholders `{timeout}`, `{command}`, `{detail}`. +3. **Translations** — `options.step.remote_commands` needs **`description`** + **`{hint}`** placeholder. **`options.step.init.data.next`** in `strings.json`. IR/serial command JSON uses **`TextSelector(multiline=True)`** (textarea), not a single-line field. Learn IR uses **`learn_ir`** / **`learn_ir_capture`** with placeholders `{timeout}`, `{command}`, `{detail}`, and **`{ir_hint}`** on `learn_ir` (always pass a non-empty placeholder so HA does not show the raw token). When creating a remote mid-learn, the form key is **`remote_name`** (not `name`) because `CONF_REMOTE_NAME` and `CONF_CMD_NAME` both serialize as `"name"`. 4. **Do not use `listen` as a config-flow field key** — Home Assistant does not apply `options.step.*.data.listen` labels; UI shows raw `listen`. Use **`monitor_incoming`** with label *Monitor incoming data (persistent connection)* (plain **`bool`**, same as `append_cr`). @@ -92,7 +92,7 @@ Matching uses **unique_id** patterns (`{entry_id}_relay_*`, `{entry_id}_serial_* 13. **Git / releases** — semver bumps via [`.github/workflows/release.yml`](.github/workflows/release.yml) (workflow_dispatch). Each release attaches **`globalcache_itach.zip`** (integration files at zip root) for HACS `zip_release` download counting; [`hacs.json`](hacs.json) sets `zip_release` + `filename`. `WORKFLOW_TRIGGER_TOKEN` enables automated manifest-bump PRs; without it, open the compare URL from the workflow summary. **Do not** add `Co-authored-by: Cursor` to commits; history was rewritten (2026-06-05) to remove it from `main` and retag `v1.0.0`. -14. **Pinhole Learn IR** — Options **Learn IR command (pinhole)** calls `get_IRL`, waits for one `sendir` line, `stop_IRL`, then stores `full_sendir` on the chosen remote via `rewrite_sendir_connector` (learner always reports `1:1`). +14. **Pinhole Learn IR** — Options menu is built by [`options_menu.build_options_init_menu`](custom_components/globalcache_itach/options_menu.py). **Learn IR command (pinhole)** is **always** listed (even with zero remotes); edit/remove remote/relay/serial entries are omitted when their lists are empty. Flow: `get_IRL` → wait for one `sendir` → `stop_IRL` → store `full_sendir` via `rewrite_sendir_connector` (learner always reports `1:1`). With no remotes, `async_step_learn_ir` collects remote name + module/port and creates the remote on successful capture. 15. **Infrared timing padding** — `infrared-protocols` NEC (and similar) frames end on a mark with no trailing space. `us_timings_to_gc_pairs` pads with a **~40 ms** off pulse (not the ~80 µs GC minimum), otherwise LG Infrared / other consumers fail to decode on the TV. ## Documentation @@ -149,4 +149,4 @@ Install via HACS (default feed) preferred; custom-repository install is no longe --- -*Last updated: 2026-09-20 — bare getversion / IP2CC config validation (#17).* +*Last updated: 2026-09-21 — Learn IR always visible in Configure; create remote during learn when none exist.* diff --git a/README.md b/README.md index 4bae190..c0cc743 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,22 @@ Open **Configure** on the integration card (gear icon on older layouts): | **IR defaults** | Carrier frequency, repeat, offset, sendir ID policy (auto-increment vs fixed). | | **Timeouts** | Connect and command timeouts. | | **Add remote** | Name, module/port (e.g. module `1`, port `2` → connector **1:2**), repeat multiplier, **JSON command list**. | -| **Learn IR command (pinhole)** | Pick a remote and command name → Submit → aim the handheld at the iTach **pinhole** within the timeout. Saves a **`full_sendir`** button (rewrites connector to the remote’s module:port). Same name replaces an existing command. | -| **Edit remote** / **Remove remote** | Change or delete a configured remote (entity IDs stay stable on edit). | +| **Learn IR command (pinhole)** | Always available in Configure. Pick an existing remote (or enter a name + IR connector to create one) and a command name → Submit → aim the handheld at the iTach **pinhole** within the timeout. Saves a **`full_sendir`** button (rewrites connector to the remote’s module:port). Same name replaces an existing command. | +| **Edit remote** / **Remove remote** | Change or delete a configured remote (entity IDs stay stable on edit). Shown only when at least one remote exists. | | **Add relay** / **Edit relay** / **Remove relay** | Relay `switch` entities via `setstate` / `getstate`. | | **Add serial port** / **Edit serial** / **Remove serial** | Serial `text` entity, optional preset **buttons**, **Last received** sensor. | +#### Learn IR (pinhole) + +iTach / GC-100 / Flex do **not** expose room IR receive (`RECEIVER`). Use **Configure → Learn IR command (pinhole)** (`get_IRL` / `stop_IRL`) instead: + +1. Open **Configure** on the hub (Learn IR is always in the Action list, even with no remotes yet). +2. If you already have remotes: pick one, enter a command name and timeout. +3. If you have none: enter a new remote name, IR module/port, command name, and timeout — the first successful capture creates that remote. +4. Submit on the capture step, then hold the handheld remote 1–2 inches from the iTach **pinhole** and press one button within the timeout. + +The learned line is stored as a **`full_sendir`** command button (connector rewritten to the remote’s module:port). Automations can also use services `ir_learner_start` / `ir_learner_stop` and the bus event `globalcache_itach_ir_learned`. + ### Devices and entities Each **gateway** is one hub device with diagnostic sensors (**TCP connected**, **Last gateway poll**, **Configured remotes**, optional **Gateway diagnostics**), plus one **`infrared` emitter** per IR connector discovered via `getdevices` (and, on **Global Connect** only, an optional **receiver** when a jack is in `RECEIVER` mode). Each configured **remote** appears as a child device with one **button** per JSON command. @@ -178,7 +189,7 @@ Protocol reference: [iTach API (PDF)](https://www.globalcache.com/files/docs/API - One **serialized** TCP client per config entry with **connect retries** and **EOF recovery** so the next command opens a new session. Multiple Home Assistant instances or other controllers talking to the same iTach can still contend on port **4998**. - **Relay** and **serial** connectors are configured in **integration options** (like remotes). Serial payloads use the Unified TCP data socket (**control port + module**, e.g. 4999 for module 1 when control is 4998). Confirm module/port wiring on your SKU (IP2CC relays are often module **3**; **GC-100-12** relays are module **3**, IR emitters modules **4** and **5** — run `get_devices` or check diagnostics). - **GC-100** allows only **one** TCP client on port **4998** at a time; avoid iHelp/other tools holding that port while Home Assistant is connected. -- **IR learner** — use **Configure → Learn IR command (pinhole)** to capture a `sendir` line and append it to a remote as a **button** (`full_sendir`). Services `ir_learner_start` / `ir_learner_stop` and bus events remain for automations; Global Caché’s **iLearn** utility is still useful for advanced editing. +- **IR learner** — use **Configure → Learn IR command (pinhole)** (always listed, even with no remotes yet) to capture a `sendir` line and append it to a remote as a **button** (`full_sendir`). With no remotes, the same flow creates one. Services `ir_learner_start` / `ir_learner_stop` and bus events remain for automations; Global Caché’s **iLearn** utility is still useful for advanced editing. - **`infrared` receivers** / `set_IR RECEIVER` / `receiveIR` are **Global Connect only** (Unified TCP API). On iTach/GC-100/Flex the integration does not probe for RECEIVER or create receiver entities; use emitters plus the **pinhole learner** (`get_IRL`). Raw `set_ir` / `receive_ir` services remain for advanced use but `RECEIVER` is invalid on non–Global Connect hardware. - Removing a remote, relay, or serial port from options **deletes** its entities from the registry on reload (they are not left as orphaned grey entities). diff --git a/custom_components/globalcache_itach/config_flow.py b/custom_components/globalcache_itach/config_flow.py index afd0fe3..9b8da53 100644 --- a/custom_components/globalcache_itach/config_flow.py +++ b/custom_components/globalcache_itach/config_flow.py @@ -35,6 +35,7 @@ async_scan_beacons, normalize_unique_id, ) +from .options_menu import build_options_init_menu from .const import ( _LEGACY_SERIAL_LISTEN, CONF_CMD_DATA, @@ -507,6 +508,18 @@ def _default_options() -> dict[str, Any]: } +def _learn_timeout_selector() -> selector.NumberSelector: + return selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + min=5, + max=120, + step=1, + unit_of_measurement="s", + ) + ) + + class GlobalCacheItachOptionsFlow(OptionsFlow): """Options and remote/command editor.""" @@ -565,30 +578,10 @@ async def async_step_init( opts = self._opts() remotes: list[dict[str, Any]] = list(opts.get(CONF_REMOTES, [])) relays: list[dict[str, Any]] = list(opts.get(CONF_RELAYS, [])) - menu = { - "ir_defaults": "IR defaults", - "timeouts": "Timeouts", - "add_remote": "Add remote", - "edit_remote": "Edit remote", - "remove_remote": "Remove remote", - "learn_ir": "Learn IR command (pinhole)", - "add_relay": "Add relay", - "edit_relay": "Edit relay", - "remove_relay": "Remove relay", - "add_serial": "Add serial port", - "edit_serial": "Edit serial port", - "remove_serial": "Remove serial port", - } - if not remotes: - menu.pop("edit_remote", None) - menu.pop("learn_ir", None) - if not relays: - menu.pop("edit_relay", None) - menu.pop("remove_relay", None) serials: list[dict[str, Any]] = list(opts.get(CONF_SERIAL_PORTS, [])) - if not serials: - menu.pop("edit_serial", None) - menu.pop("remove_serial", None) + menu = build_options_init_menu( + remotes=remotes, relays=relays, serials=serials + ) return self.async_show_form( step_id="init", data_schema=vol.Schema({vol.Required("next"): vol.In(menu)}), @@ -865,60 +858,110 @@ async def async_step_remote_commands( async def async_step_learn_ir( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Pick remote and command name, then capture from the pinhole learner.""" + """Pick remote (or create one) and command name, then capture from pinhole.""" opts = self._opts() remotes: list[dict[str, Any]] = list(opts.get(CONF_REMOTES, [])) + entry = self._options_entry() + modules = list(entry.data.get(CONF_DEVICE_MODULES, [])) + default_module = default_ir_module(modules) if modules else 1 + if not remotes: - return self.async_abort(reason="no_remotes") + return await self._async_step_learn_ir_new_remote( + user_input, default_module=default_module, modules=modules + ) + choices = { str(r[CONF_REMOTE_ID]): str(r.get(CONF_REMOTE_NAME, r[CONF_REMOTE_ID])) for r in remotes if r.get(CONF_REMOTE_ID) } + schema = vol.Schema( + { + vol.Required(CONF_REMOTE_ID): vol.In(choices), + vol.Required(CONF_CMD_NAME, default="power"): str, + vol.Required("timeout", default=30): _learn_timeout_selector(), + } + ) + placeholders = {"ir_hint": ir_connectors_hint(modules) or " "} if user_input is not None: + cmd_name = str(user_input[CONF_CMD_NAME]).strip() self._learn_draft = { CONF_REMOTE_ID: str(user_input[CONF_REMOTE_ID]), - CONF_CMD_NAME: str(user_input[CONF_CMD_NAME]).strip(), + CONF_CMD_NAME: cmd_name, "timeout": float(user_input.get("timeout", 30)), } - if not self._learn_draft[CONF_CMD_NAME]: + if not cmd_name: return self.async_show_form( step_id="learn_ir", - data_schema=vol.Schema( - { - vol.Required(CONF_REMOTE_ID): vol.In(choices), - vol.Required(CONF_CMD_NAME): str, - vol.Required("timeout", default=30): selector.NumberSelector( - selector.NumberSelectorConfig( - mode=selector.NumberSelectorMode.BOX, - min=5, - max=120, - step=1, - unit_of_measurement="s", - ) - ), - } - ), + data_schema=schema, errors={"base": "command_name_required"}, + description_placeholders=placeholders, ) return await self.async_step_learn_ir_capture() return self.async_show_form( step_id="learn_ir", - data_schema=vol.Schema( - { - vol.Required(CONF_REMOTE_ID): vol.In(choices), - vol.Required(CONF_CMD_NAME, default="power"): str, - vol.Required("timeout", default=30): selector.NumberSelector( - selector.NumberSelectorConfig( - mode=selector.NumberSelectorMode.BOX, - min=5, - max=120, - step=1, - unit_of_measurement="s", - ) - ), - } - ), + data_schema=schema, + description_placeholders=placeholders, + ) + + async def _async_step_learn_ir_new_remote( + self, + user_input: dict[str, Any] | None, + *, + default_module: int, + modules: list[Any], + ) -> FlowResult: + """Learn IR when no remotes exist: create remote + command in one flow.""" + # CONF_REMOTE_NAME and CONF_CMD_NAME are both "name" in options storage; + # use a distinct form key for the remote label on this combined step. + schema = vol.Schema( + { + vol.Required("remote_name", default="Remote"): str, + vol.Required( + CONF_MODULE, default=default_module + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + min=1, + max=5, + ) + ), + vol.Required(CONF_CONN_PORT, default=1): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + min=1, + max=6, + ) + ), + vol.Required(CONF_CMD_NAME, default="power"): str, + vol.Required("timeout", default=30): _learn_timeout_selector(), + } + ) + placeholders = {"ir_hint": ir_connectors_hint(modules) or " "} + if user_input is not None: + remote_name = str(user_input.get("remote_name", "")).strip() or "Remote" + cmd_name = str(user_input[CONF_CMD_NAME]).strip() + self._learn_draft = { + "create_remote": True, + CONF_REMOTE_NAME: remote_name, + CONF_MODULE: int(user_input[CONF_MODULE]), + CONF_CONN_PORT: int(user_input[CONF_CONN_PORT]), + CONF_IR_COUNT: 1, + CONF_CMD_NAME: cmd_name, + "timeout": float(user_input.get("timeout", 30)), + } + if not cmd_name: + return self.async_show_form( + step_id="learn_ir", + data_schema=schema, + errors={"base": "command_name_required"}, + description_placeholders=placeholders, + ) + return await self.async_step_learn_ir_capture() + return self.async_show_form( + step_id="learn_ir", + data_schema=schema, + description_placeholders=placeholders, ) async def async_step_learn_ir_capture( @@ -960,17 +1003,10 @@ async def async_step_learn_ir_capture( if raw_line is not None and not errors: opts = self._opts() remotes = list(opts.get(CONF_REMOTES, [])) - rid = str(draft[CONF_REMOTE_ID]) name = command - updated: list[dict[str, Any]] = [] - found = False - for remote in remotes: - if str(remote.get(CONF_REMOTE_ID)) != rid: - updated.append(remote) - continue - found = True - module = int(remote[CONF_MODULE]) - port = int(remote[CONF_CONN_PORT]) + if draft.get("create_remote"): + module = int(draft[CONF_MODULE]) + port = int(draft[CONF_CONN_PORT]) try: sendir = rewrite_sendir_connector( raw_line, module, port @@ -978,31 +1014,73 @@ async def async_step_learn_ir_capture( except ValueError as err: errors["base"] = "learn_failed" draft["error_detail"] = str(err) - updated.append(remote) - continue - commands = [ - c - for c in list(remote.get(CONF_COMMANDS, [])) - if str(c.get(CONF_CMD_NAME, "")).strip().lower() - != name.lower() - ] - commands.append( - { - CONF_CMD_NAME: name, - CONF_CMD_FORMAT: "full_sendir", - CONF_CMD_DATA: sendir, - } - ) - updated.append({**remote, CONF_COMMANDS: commands}) - if not errors and not found: - errors["base"] = "learn_failed" - draft["error_detail"] = "Remote not found" - elif not errors: - self._learn_draft = None - return self.async_create_entry( - title="", - data={**opts, CONF_REMOTES: updated}, - ) + else: + remotes.append( + { + CONF_REMOTE_ID: str(uuid.uuid4()), + CONF_REMOTE_NAME: str( + draft.get(CONF_REMOTE_NAME, "Remote") + ), + CONF_MODULE: module, + CONF_CONN_PORT: port, + CONF_IR_COUNT: int(draft.get(CONF_IR_COUNT, 1)), + CONF_COMMANDS: [ + { + CONF_CMD_NAME: name, + CONF_CMD_FORMAT: "full_sendir", + CONF_CMD_DATA: sendir, + } + ], + } + ) + self._learn_draft = None + return self.async_create_entry( + title="", + data={**opts, CONF_REMOTES: remotes}, + ) + else: + rid = str(draft[CONF_REMOTE_ID]) + updated: list[dict[str, Any]] = [] + found = False + for remote in remotes: + if str(remote.get(CONF_REMOTE_ID)) != rid: + updated.append(remote) + continue + found = True + module = int(remote[CONF_MODULE]) + port = int(remote[CONF_CONN_PORT]) + try: + sendir = rewrite_sendir_connector( + raw_line, module, port + ) + except ValueError as err: + errors["base"] = "learn_failed" + draft["error_detail"] = str(err) + updated.append(remote) + continue + commands = [ + c + for c in list(remote.get(CONF_COMMANDS, [])) + if str(c.get(CONF_CMD_NAME, "")).strip().lower() + != name.lower() + ] + commands.append( + { + CONF_CMD_NAME: name, + CONF_CMD_FORMAT: "full_sendir", + CONF_CMD_DATA: sendir, + } + ) + updated.append({**remote, CONF_COMMANDS: commands}) + if not errors and not found: + errors["base"] = "learn_failed" + draft["error_detail"] = "Remote not found" + elif not errors: + self._learn_draft = None + return self.async_create_entry( + title="", + data={**opts, CONF_REMOTES: updated}, + ) detail = str(draft.get("error_detail") or "") return self.async_show_form( diff --git a/custom_components/globalcache_itach/options_menu.py b/custom_components/globalcache_itach/options_menu.py new file mode 100644 index 0000000..01e5b94 --- /dev/null +++ b/custom_components/globalcache_itach/options_menu.py @@ -0,0 +1,43 @@ +"""Configure (options) menu labels for Global Caché iTach.""" + +from __future__ import annotations + +from typing import Any + + +def build_options_init_menu( + *, + remotes: list[Any], + relays: list[Any], + serials: list[Any], +) -> dict[str, str]: + """Build the Configure menu; Learn IR stays visible even with no remotes. + + Edit/remove entries are omitted when the matching list is empty. Learn IR + must remain available so iTach users (no infrared receiver) can still + capture codes via the pinhole learner without first adding a remote. + """ + menu = { + "ir_defaults": "IR defaults", + "timeouts": "Timeouts", + "add_remote": "Add remote", + "edit_remote": "Edit remote", + "remove_remote": "Remove remote", + "learn_ir": "Learn IR command (pinhole)", + "add_relay": "Add relay", + "edit_relay": "Edit relay", + "remove_relay": "Remove relay", + "add_serial": "Add serial port", + "edit_serial": "Edit serial port", + "remove_serial": "Remove serial port", + } + if not remotes: + menu.pop("edit_remote", None) + menu.pop("remove_remote", None) + if not relays: + menu.pop("edit_relay", None) + menu.pop("remove_relay", None) + if not serials: + menu.pop("edit_serial", None) + menu.pop("remove_serial", None) + return menu diff --git a/custom_components/globalcache_itach/strings.json b/custom_components/globalcache_itach/strings.json index a7c3317..a781a48 100644 --- a/custom_components/globalcache_itach/strings.json +++ b/custom_components/globalcache_itach/strings.json @@ -82,11 +82,14 @@ }, "learn_ir": { "title": "Learn IR command", - "description": "Capture a code with the iTach pinhole learner and add it to a remote as a button.", + "description": "Capture a code with the iTach pinhole learner and add it to a remote as a button. If you have no remotes yet, enter a name and IR connector here to create one. {ir_hint}", "data": { "remote_id": "Remote", + "remote_name": "New remote name", "name": "Command name", - "timeout": "Listen timeout" + "timeout": "Listen timeout", + "module": "Module address", + "port": "Port (1–3 on IP2IR)" } }, "learn_ir_capture": { diff --git a/custom_components/globalcache_itach/translations/en.json b/custom_components/globalcache_itach/translations/en.json index 8a38b40..df531ba 100644 --- a/custom_components/globalcache_itach/translations/en.json +++ b/custom_components/globalcache_itach/translations/en.json @@ -150,11 +150,14 @@ }, "learn_ir": { "title": "Learn IR command", - "description": "Capture a code with the iTach pinhole learner and add it to a remote as a button.", + "description": "Capture a code with the iTach pinhole learner and add it to a remote as a button. If you have no remotes yet, enter a name and IR connector here to create one. {ir_hint}", "data": { "remote_id": "Remote", + "remote_name": "New remote name", "name": "Command name", - "timeout": "Listen timeout" + "timeout": "Listen timeout", + "module": "Module address", + "port": "Port (1–3 on IP2IR)" } }, "learn_ir_capture": { diff --git a/tests/test_options_menu.py b/tests/test_options_menu.py new file mode 100644 index 0000000..1261de3 --- /dev/null +++ b/tests/test_options_menu.py @@ -0,0 +1,36 @@ +"""Options Configure menu visibility (Learn IR must stay available).""" + +from custom_components.globalcache_itach.options_menu import build_options_init_menu + + +def test_learn_ir_visible_without_remotes() -> None: + menu = build_options_init_menu(remotes=[], relays=[], serials=[]) + assert "learn_ir" in menu + assert menu["learn_ir"] == "Learn IR command (pinhole)" + assert "add_remote" in menu + assert "edit_remote" not in menu + assert "remove_remote" not in menu + + +def test_learn_ir_visible_with_remotes() -> None: + menu = build_options_init_menu( + remotes=[{"remote_id": "r1"}], + relays=[], + serials=[], + ) + assert "learn_ir" in menu + assert "edit_remote" in menu + assert "remove_remote" in menu + + +def test_relay_serial_edit_gated_on_empty() -> None: + menu = build_options_init_menu( + remotes=[], + relays=[{"relay_id": "x"}], + serials=[], + ) + assert "edit_relay" in menu + assert "remove_relay" in menu + assert "edit_serial" not in menu + assert "remove_serial" not in menu + assert "learn_ir" in menu From b2ce5a4301886d8a19c4c16ff09fc0c49f5b2a62 Mon Sep 17 00:00:00 2001 From: Michael Shaffer Date: Mon, 21 Sep 2026 15:00:36 +0000 Subject: [PATCH 2/2] docs: point CONTEXT production snapshot at v1.1.3 Fold in the curated release summary from the closed docs PR so the handoff file matches the published GitHub release notes. Signed-off-by: Michael Shaffer --- CONTEXT.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 93df7f5..1201279 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -134,19 +134,25 @@ cannot import name 'DhcpServiceInfo' from 'homeassistant.components.dhcp' Config entry shows **`setup_error` / Import error**. Deploy manifest **1.0.2+** (or cherry-pick the one-line import change in `config_flow.py`), then restart HA or reload the integration. -## Production / deployment snapshot (2026-07-31) +## Production / deployment snapshot (2026-09-21) | Environment | Integration version | Status | |-------------|---------------------|--------| | **HACS default** | Search **Global Caché iTach** / **GlobalCache** | Added via [hacs/default#8063](https://github.com/hacs/default/pull/8063) | -| **Latest release** | **v1.0.3** | https://github.com/Shaffer-Softworks/GlobalCache/releases | +| **Latest release** | **v1.1.3** | https://github.com/Shaffer-Softworks/GlobalCache/releases/tag/v1.1.3 | Install via HACS (default feed) preferred; custom-repository install is no longer needed. +### v1.1.3 release notes (summary) + +- **IP2CC config validation** — accept bare `getversion` firmware replies ([#17](https://github.com/Shaffer-Softworks/GlobalCache/issues/17)) +- **iTach RECEIVER probe** — skip on non–Global Connect so IP2IR / WF2IR stop logging spurious reload warnings ([#18](https://github.com/Shaffer-Softworks/GlobalCache/issues/18)) +- **`via_device_id`** — replace deprecated `via_device` for HA 2027.8 ([#19](https://github.com/Shaffer-Softworks/GlobalCache/issues/19)) + ## Optional follow-ups (not implemented) - YAML import from core `itach` integration if applicable --- -*Last updated: 2026-09-21 — Learn IR always visible in Configure; create remote during learn when none exist.* +*Last updated: 2026-09-21 — Learn IR always visible in Configure; CONTEXT production snapshot → v1.1.3.*