From 7827131c5dbf8310a74c6f664c4a828cd6b30f6e Mon Sep 17 00:00:00 2001 From: TheOriginalCER06 <69149072+TheOriginalCER06@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:14:50 +0100 Subject: [PATCH 1/3] Add Lovelace examples, automation, and unit test; update README --- README.md | 100 ++++++++++++++++++ .../automations/track17_remove_notify.yaml | 17 +++ examples/lovelace/track17.yaml | 72 +++++++++++++ examples/lovelace/track17_full.yaml | 87 +++++++++++++++ tests/test_coordinator_remove.py | 54 ++++++++++ 5 files changed, 330 insertions(+) create mode 100644 examples/automations/track17_remove_notify.yaml create mode 100644 examples/lovelace/track17.yaml create mode 100644 examples/lovelace/track17_full.yaml create mode 100644 tests/test_coordinator_remove.py diff --git a/README.md b/README.md index feddcad..2f04d58 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,14 @@ cards: # and adds the package. This avoids the common Lovelace issue where the # client sends the literal template string instead of its evaluated value. service: track17.add_package_from_helper + - show_name: true + show_icon: true + type: button + name: Remove package + tap_action: + action: call-service + # Remove the package named in the helper using the server-side service. + service: track17.remove_package_from_helper - type: markdown title: 17TRACK Overview content: > @@ -117,6 +125,79 @@ cards: {% endif %} ``` +Per-package remove buttons (two approaches) +------------------------------------------ + +If you'd like a per-package remove control in your dashboard (a remove +button next to each tracked package), here are two options: a simple script +approach (no custom cards) and a dynamic approach using HACS custom cards. + +1) Simple script + manual rows + + - Add a small script that accepts a `tracking_number` variable and calls + the integration's removal service. Put this in `scripts.yaml` or create + it in the Scripts UI: + + ```yaml + track17_remove_package: + alias: "17TRACK: Remove package" + sequence: + - service: track17.remove_package + data: + tracking_number: "{{ tracking_number }}" + ``` + + - Then in Lovelace, add an `entities` card and for each package row add a + manual `entity` entry with a `tap_action` that calls the script and + passes the package string. This is easy for a small or static set of + tracked packages. + +2) Dynamic list with per-item buttons (recommended for many packages) + + - Install the HACS custom cards `auto-entities` and `button-card`. + + - The following Lovelace snippet uses `auto-entities` to discover + `sensor.track17_*` sensors and renders each as a `button-card` with a + remove action. The `button-card` JS template extracts the tracked + number from the sensor attributes and passes it to the `remove_package` + service. + + ```yaml + type: 'custom:auto-entities' + card: + type: grid + columns: 1 + filter: + include: + - domain: sensor + entity_id: sensor.track17_* + sort: + method: name + card: + type: 'custom:button-card' + entity: '${entity}' + name: '[[[ return entity.attributes.tracking_number || entity.entity_id ]]]' + show_state: true + show_icon: false + tap_action: + action: call-service + service: track17.remove_package + service_data: + tracking_number: "[[[ return entity.attributes.tracking_number ]]]" + ``` + + Notes: + - `auto-entities` automatically builds one child card per matching entity. + The `card:` block controls how each entity is rendered. + - `button-card` uses JavaScript templating (the `[[[ ... ]]]` syntax) to + access the `entity` object and its attributes. We call the integration + service and pass `entity.attributes.tracking_number` as the argument. + - This approach requires HACS to install the custom cards but provides a + fully dynamic UI that updates when packages are added or removed. + +Pick the approach that fits your setup — the dynamic `auto-entities` + +`button-card` approach works best for larger or frequently-changing lists. + Add this helper script to your Home Assistant `scripts.yaml` (or create it from the Scripts UI). The script templates the `input_text` value server-side and calls the integration's `add_package` service correctly: @@ -193,6 +274,25 @@ automation: - mon action: - service: track17.remove_delivered_packages + +Included example files +---------------------- + +This repository includes a couple of ready-made examples you can copy into +your Home Assistant config: + +- `examples/lovelace/track17.yaml` — a small Lovelace view with the helper + input and Add / Remove buttons. It also contains a commented dynamic + `auto-entities` + `button-card` example (requires HACS) for per-item remove + buttons. +- `examples/automations/track17_remove_notify.yaml` — an example automation + that creates a persistent notification when the `track17.remove_package` + service is called (useful as a demo confirmation of remove actions). +- `examples/lovelace/track17_full.yaml` — a fully fleshed-out Lovelace view + with tiles, icons and a layout you can copy into a Raw Lovelace view. + +Copy the files into the corresponding folders in your Home Assistant config +or open them and paste into the UI editors. ``` diff --git a/examples/automations/track17_remove_notify.yaml b/examples/automations/track17_remove_notify.yaml new file mode 100644 index 0000000..717c8c8 --- /dev/null +++ b/examples/automations/track17_remove_notify.yaml @@ -0,0 +1,17 @@ +# Example automation: show a persistent notification when a package is removed +# Place this in your automations folder or import via the UI. + +alias: "17TRACK: Notify on package removal" +trigger: + - platform: event + event_type: call_service + event_data: + domain: track17 + service: remove_package +condition: [] +action: + - service: persistent_notification.create + data: + title: "17TRACK — Package removed" + message: "Removed package: {{ trigger.event.data.service_data.tracking_number }}" +mode: single diff --git a/examples/lovelace/track17.yaml b/examples/lovelace/track17.yaml new file mode 100644 index 0000000..2fb3db5 --- /dev/null +++ b/examples/lovelace/track17.yaml @@ -0,0 +1,72 @@ +# Example Lovelace view for 17TRACK integration +# Copy/paste this into a Lovelace Raw config or use as a view file. +# Requires the integration to be installed and configured. + +title: 17TRACK +path: track17 +cards: + - type: entities + title: Add / Remove Package + entities: + - entity: input_text.track17_new_package + name: Tracking number + - type: call-service + name: Add package (from helper) + action_name: Add + service: track17.add_package_from_helper + - type: call-service + name: Remove package (from helper) + action_name: Remove + service: track17.remove_package_from_helper + + - type: markdown + title: 17TRACK Overview (simple) + content: | + {% set packages = state_attr('sensor.track17_packages', 'packages') %} + {% if packages %} + | Tracking Number | Carrier | Country | Last Event | Delivered At | Link | + |-----------------|---------|---------|------------|--------------|------| + {% for pkg in packages %} + {% set attr = state_attr('sensor.track17_' ~ pkg, 'attributes') %} + {% if attr is not none %} + | {{ attr.tracking_number }} | {{ attr.carrier }} | {{ attr.country }} | {{ attr.last_event }} | {{ attr.delivered_at }} | [Link]({{ attr.url }}) | + {% else %} + | {{ pkg }} | (no data) | | | | | + {% endif %} + {% endfor %} + {% else %} + No packages tracked. + {% endif %} + + # Dynamic list (requires HACS custom cards: auto-entities + button-card) + # Uncomment this section if you have installed the required custom cards. + + #- type: 'custom:auto-entities' + # card: + # type: grid + # columns: 1 + # filter: + # include: + # - domain: sensor + # entity_id: sensor.track17_* + # sort: + # method: name + # card: + # type: 'custom:button-card' + # entity: '${entity}' + # name: '[[[ return entity.attributes.tracking_number || entity.entity_id ]]]' + # show_state: true + # show_icon: false + # tap_action: + # action: call-service + # service: track17.remove_package + # service_data: + # tracking_number: "[[[ return entity.attributes.tracking_number ]]]" + + - type: entities + title: Controls + entities: + - entity: button.17track_refresh + name: Refresh all + - entity: sensor.track17_packages + name: Tracked packages diff --git a/examples/lovelace/track17_full.yaml b/examples/lovelace/track17_full.yaml new file mode 100644 index 0000000..839b5e0 --- /dev/null +++ b/examples/lovelace/track17_full.yaml @@ -0,0 +1,87 @@ +title: 17TRACK Full +path: track17-full +cards: + - type: vertical-stack + cards: + - type: entities + title: Add / Remove Package + entities: + - entity: input_text.track17_new_package + name: Tracking number + - type: call-service + name: Add package + action_name: Add + service: track17.add_package_from_helper + - type: call-service + name: Remove package + action_name: Remove + service: track17.remove_package_from_helper + + - type: horizontal-stack + cards: + - type: entities + title: Summary + entities: + - entity: sensor.track17_packages + name: Tracked packages + - entity: button.17track_refresh + name: Refresh all + + - type: markdown + title: Quick Links + content: | + - [17TRACK web](https://t.17track.net) + + - type: grid + title: Package tiles + columns: 3 + square: false + cards: [] + + - type: 'custom:auto-entities' + card: + type: grid + columns: 3 + square: false + filter: + include: + - domain: sensor + entity_id: sensor.track17_* + sort: + method: name + card: + type: 'custom:button-card' + entity: '${entity}' + name: '[[[ return entity.attributes.tracking_number || entity.entity_id ]]]' + show_state: true + show_icon: true + icon: mdi:truck-delivery + color_type: icon + styles: + card: + - padding: 10px + - border-radius: 8px + name: + - font-weight: 600 + grid: + - grid-template-areas: '"i n" "s s"' + - grid-template-columns: 28px 1fr + - align-items: center + state: + - value: 'Delivered' + color: '#43A047' + - operator: default + color: '#1E88E5' + tap_action: + action: call-service + service: track17.remove_package + service_data: + tracking_number: "[[[ return entity.attributes.tracking_number ]]]" + + - type: markdown + title: Notes + content: | + - Use the Add / Remove helper to manage packages quickly. + - For a dynamic per-item remove button, install HACS and the + `auto-entities` + `button-card` custom cards and use the snippet in + `examples/lovelace/track17.yaml`. diff --git a/tests/test_coordinator_remove.py b/tests/test_coordinator_remove.py new file mode 100644 index 0000000..6eaef04 --- /dev/null +++ b/tests/test_coordinator_remove.py @@ -0,0 +1,54 @@ +import pytest +import asyncio +from unittest.mock import MagicMock + +from custom_components.track17 import coordinator + + +@pytest.mark.asyncio +async def test_async_remove_package_removes_entity_and_saves(monkeypatch): + # Minimal fake hass and entry + hass = MagicMock() + hass.bus = MagicMock() + + entry = MagicMock() + entry.data = {"api_key": "abc"} + entry.options = {} + entry.entry_id = "test" + + # Dummy Store that records saved data + class DummyStore: + def __init__(self, hass_arg, version, key): + self._data = [] + + async def async_load(self): + return [] + + async def async_save(self, data): + self._data = list(data) + + monkeypatch.setattr(coordinator, "Store", DummyStore) + + # Instantiate coordinator + coord = coordinator.Track17Coordinator(hass, entry) + + # Replace network/API with a dummy + coord.api = MagicMock() + + # Seed tracking numbers + coord.tracking_numbers = ["LP1"] + + # Fake entity registry + registry = MagicMock() + entity = MagicMock() + entity.entity_id = "sensor.track17_LP1" + registry.async_get.return_value = entity + + monkeypatch.setattr(coordinator.er, "async_get", lambda hass_arg: registry) + + # Call removal + result = await coord.async_remove_package("LP1") + + assert result is True + assert "LP1" not in coord.tracking_numbers + registry.async_remove.assert_called_with("sensor.track17_LP1") From 6fe8c82d8c7b38ec0fc2b4bdefcd0cdf563df20e Mon Sep 17 00:00:00 2001 From: TheOriginalCER06 <69149072+TheOriginalCER06@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:15:51 +0100 Subject: [PATCH 2/3] CI: add GitHub Actions workflow to run pytest --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..26f1c7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.11] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install test dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-asyncio + + - name: Run tests + run: | + pytest -q From 9b1fcef1a00e11051d3f55ccaf559f32a57202ec Mon Sep 17 00:00:00 2001 From: TheOriginalCER06 <69149072+TheOriginalCER06@users.noreply.github.com> Date: Sun, 18 Jan 2026 19:16:47 +0100 Subject: [PATCH 3/3] 1.1.6 --- custom_components/track17/__init__.py | 17 +++++++++++++++++ custom_components/track17/const.py | 4 ++-- custom_components/track17/manifest.json | 2 +- custom_components/track17/services.yaml | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/custom_components/track17/__init__.py b/custom_components/track17/__init__.py index ffa1793..8c10ea3 100644 --- a/custom_components/track17/__init__.py +++ b/custom_components/track17/__init__.py @@ -71,12 +71,28 @@ async def handle_add_from_helper(call): if added: await hass.config_entries.async_reload(entry.entry_id) + async def handle_remove_from_helper(call): + """Read the input_text helper and remove that package. + + This mirrors `add_package_from_helper` but removes the package named + in the helper. Useful for Lovelace buttons that shouldn't send + client-side templates. + """ + state = hass.states.get(DEFAULT_HELPER_ENTITY) + if not state: + return + number = state.state + removed = await coordinator.async_remove_package(number) + if removed: + await hass.config_entries.async_reload(entry.entry_id) + # Register services hass.services.async_register(DOMAIN, "add_package", handle_add_package) hass.services.async_register(DOMAIN, "remove_package", handle_remove_package) hass.services.async_register(DOMAIN, "refresh_package", handle_refresh_package) hass.services.async_register(DOMAIN, "refresh_all_packages", handle_refresh_all) hass.services.async_register(DOMAIN, "add_package_from_helper", handle_add_from_helper) + hass.services.async_register(DOMAIN, "remove_package_from_helper", handle_remove_from_helper) # Forward platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -105,5 +121,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, "refresh_package") hass.services.async_remove(DOMAIN, "refresh_all_packages") hass.services.async_remove(DOMAIN, "add_package_from_helper") + hass.services.async_remove(DOMAIN, "remove_package_from_helper") return unload_ok diff --git a/custom_components/track17/const.py b/custom_components/track17/const.py index adbb4a3..d376c4e 100644 --- a/custom_components/track17/const.py +++ b/custom_components/track17/const.py @@ -7,10 +7,10 @@ EVENT_DELIVERED = "track17_delivered" -VERSION = "1.1.5" +VERSION = "1.1.6" API_URL = "https://api.17track.net/track/v2.4/" DEFAULT_HEADERS = { "Content-Type": "application/json", - "User-Agent": "HomeAssistant-Track17-Integration/1.1.5" + "User-Agent": "HomeAssistant-Track17-Integration/1.1.6" } \ No newline at end of file diff --git a/custom_components/track17/manifest.json b/custom_components/track17/manifest.json index d08bb74..6231e3b 100644 --- a/custom_components/track17/manifest.json +++ b/custom_components/track17/manifest.json @@ -1,7 +1,7 @@ { "domain": "track17", "name": "17TRACK Package Tracking", - "version": "1.1.5", + "version": "1.1.6", "documentation": "https://api.17track.net", "config_flow": true, "iot_class": "cloud_polling" diff --git a/custom_components/track17/services.yaml b/custom_components/track17/services.yaml index 87135b6..918e0a0 100644 --- a/custom_components/track17/services.yaml +++ b/custom_components/track17/services.yaml @@ -30,3 +30,8 @@ add_package_from_helper: name: Add Package From Helper description: "Read the configured input_text helper and add the package value" fields: {} + +remove_package_from_helper: + name: Remove Package From Helper + description: "Read the configured input_text helper and remove the package value" + fields: {}