Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Tests

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements_test.txt
- run: python -m pip install -r requirements_test.txt
- run: ruff check .
- run: ruff format --check .
- run: pytest
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Benutzerdefinierte Home-Assistant-Integration für die dokumentierte externe ShipShow-API.

Version `1.0.1` ist mit Home Assistant `2026.8` kompatibel und gegen `2026.8.1`
getestet. Bestehende Entity-IDs werden bei der erforderlichen Geräte- und
Unique-ID-Migration beibehalten.

## Funktionen

- Einrichtung über den Home-Assistant-Konfigurationsdialog
Expand Down
36 changes: 31 additions & 5 deletions custom_components/shipshow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .api import ShipShowClient
Expand All @@ -24,9 +26,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ShipShowConfigEntry) ->
)
coordinator = ShipShowDataUpdateCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
await _async_migrate_registry_entries(hass, entry)

entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_async_register_services(hass)
return True
Expand All @@ -37,9 +39,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShipShowConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def _async_update_listener(hass: HomeAssistant, entry: ShipShowConfigEntry) -> None:
"""Reload entry when options change."""
await hass.config_entries.async_reload(entry.entry_id)
async def _async_migrate_registry_entries(hass: HomeAssistant, entry: ShipShowConfigEntry) -> None:
"""Scope legacy package entities and devices to their config entry."""
unique_id_prefix = f"{entry.entry_id}_"

@callback
def migrate_entity(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
if entity_entry.unique_id.startswith(unique_id_prefix):
return None
return {"new_unique_id": f"{unique_id_prefix}{entity_entry.unique_id}"}

await er.async_migrate_entries(hass, entry.entry_id, migrate_entity)

device_registry = dr.async_get(hass)
for device_entry in dr.async_entries_for_config_entry(device_registry, entry.entry_id):
identifiers = {
(domain, f"{unique_id_prefix}{identifier}")
if domain == DOMAIN
and identifier != entry.entry_id
and not identifier.startswith(unique_id_prefix)
else (domain, identifier)
for domain, identifier in device_entry.identifiers
}
if identifiers != device_entry.identifiers:
device_registry.async_update_device(
device_entry.id,
new_identifiers=identifiers,
)


def _async_register_services(hass: HomeAssistant) -> None:
Expand Down
4 changes: 2 additions & 2 deletions custom_components/shipshow/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from __future__ import annotations

from asyncio import timeout as asyncio_timeout
from dataclasses import dataclass, field
from typing import Any

import aiohttp
import async_timeout

from .const import DEFAULT_API_BASE_URL, DEFAULT_LIMIT

Expand Down Expand Up @@ -82,7 +82,7 @@ async def async_get_trackings(self, query: ShipShowQuery) -> ShipShowPage:
"""Fetch one page of trackings."""
url = f"{self._api_base_url}getTrackings"
try:
async with async_timeout.timeout(self._timeout):
async with asyncio_timeout(self._timeout):
response = await self._session.get(
url,
params=query.as_params(self._api_key),
Expand Down
13 changes: 8 additions & 5 deletions custom_components/shipshow/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Callable
from typing import Any

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import ShipShowConfigEntry
from .coordinator import ShipShowDataUpdateCoordinator
Expand Down Expand Up @@ -62,7 +63,7 @@ class ShipShowBinarySensorDescription(BinarySensorEntityDescription):
async def async_setup_entry(
hass: HomeAssistant,
entry: ShipShowConfigEntry,
async_add_entities: AddEntitiesCallback,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ShipShow binary sensors."""
coordinator = entry.runtime_data
Expand All @@ -77,7 +78,7 @@ class ShipShowBinarySensorManager:
def __init__(
self,
coordinator: ShipShowDataUpdateCoordinator,
async_add_entities: AddEntitiesCallback,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
self.coordinator = coordinator
self.async_add_entities = async_add_entities
Expand Down Expand Up @@ -112,7 +113,9 @@ def __init__(
) -> None:
super().__init__(coordinator, tracking_id)
self.entity_description = description
self._attr_unique_id = f"{tracking_id}_{description.key}"
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}_{tracking_id}_{description.key}"
)
self.role = BINARY_SENSOR_ROLES.get(description.key, description.key)
self._attr_suggested_object_id = self.tracking_object_id(self.role)

Expand Down
10 changes: 6 additions & 4 deletions custom_components/shipshow/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util

from . import ShipShowConfigEntry
Expand All @@ -18,7 +18,7 @@
async def async_setup_entry(
hass: HomeAssistant,
entry: ShipShowConfigEntry,
async_add_entities: AddEntitiesCallback,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ShipShow calendars."""
coordinator = entry.runtime_data
Expand All @@ -33,7 +33,7 @@ class ShipShowCalendarManager:
def __init__(
self,
coordinator: ShipShowDataUpdateCoordinator,
async_add_entities: AddEntitiesCallback,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
self.coordinator = coordinator
self.async_add_entities = async_add_entities
Expand Down Expand Up @@ -65,7 +65,9 @@ def __init__(
tracking_id: str,
) -> None:
super().__init__(coordinator, tracking_id)
self._attr_unique_id = f"{tracking_id}_delivery_calendar"
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}_{tracking_id}_delivery_calendar"
)

@property
def name(self) -> str:
Expand Down
3 changes: 1 addition & 2 deletions custom_components/shipshow/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers.aiohttp_client import async_get_clientsession
Expand Down Expand Up @@ -94,7 +93,7 @@ def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> ShipShow
return ShipShowOptionsFlow(config_entry)


class ShipShowOptionsFlow(config_entries.OptionsFlow):
class ShipShowOptionsFlow(config_entries.OptionsFlowWithReload):
"""Handle ShipShow options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
Expand Down
8 changes: 7 additions & 1 deletion custom_components/shipshow/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from datetime import timedelta

from homeassistant.const import Platform

DOMAIN = "shipshow"

EVENT_DELIVERY_OUT_FOR_DELIVERY = "shipshow_lieferung_in_zustellung"
Expand Down Expand Up @@ -53,6 +55,10 @@
"undelivered",
]

PLATFORMS = ["sensor", "binary_sensor", "calendar"]
PLATFORMS: list[Platform] = [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.CALENDAR,
]

RECOMMENDED_SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
17 changes: 5 additions & 12 deletions custom_components/shipshow/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from __future__ import annotations

import logging
import re
from collections import Counter
from dataclasses import dataclass
from datetime import timedelta
import logging
import re
from typing import Any

from homeassistant.config_entries import ConfigEntry
Expand Down Expand Up @@ -104,13 +104,10 @@ async def _async_update_data(self) -> ShipShowData:
trackings[unique_id] = tracking

categories = {
str(category["id"]): category
for category in page.categories
if category.get("id")
str(category["id"]): category for category in page.categories if category.get("id")
}
status_counts = Counter(
str(tracking.get("last_status") or "unknown")
for tracking in trackings.values()
str(tracking.get("last_status") or "unknown") for tracking in trackings.values()
)

data = ShipShowData(
Expand Down Expand Up @@ -145,11 +142,7 @@ def _async_fire_delivery_events(self, data: ShipShowData) -> None:
),
)

if (
stops is not None
and previous_stops is not None
and stops < previous_stops
):
if stops is not None and previous_stops is not None and stops < previous_stops:
self.hass.bus.async_fire(
EVENT_STOPS_DECREASED,
_delivery_event_data(
Expand Down
5 changes: 3 additions & 2 deletions custom_components/shipshow/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from __future__ import annotations

from datetime import date, datetime
import re
from datetime import date, datetime
from typing import Any

from homeassistant.helpers.device_registry import DeviceInfo
Expand Down Expand Up @@ -67,8 +67,9 @@ def device_info(self) -> DeviceInfo:
tracking = self.tracking
carrier = tracking.get("carrier") or {}
carrier_name = carrier.get("name") or carrier.get("id")
entry_id = self.coordinator.config_entry.entry_id
return DeviceInfo(
identifiers={(DOMAIN, self.tracking_id)},
identifiers={(DOMAIN, f"{entry_id}_{self.tracking_id}")},
name=tracking_title(tracking),
manufacturer=carrier_name,
model=tracking.get("trackingnumber"),
Expand Down
10 changes: 5 additions & 5 deletions custom_components/shipshow/manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"domain": "shipshow",
"name": "ShipShow",
"codeowners": ["@drapple"],
"codeowners": ["@dr-apple"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/drapple/ha-shipshow",
"documentation": "https://github.com/dr-apple/ha-shipshow",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/drapple/ha-shipshow/issues",
"requirements": ["async-timeout>=4.0.3"],
"version": "1.0.0"
"issue_tracker": "https://github.com/dr-apple/ha-shipshow/issues",
"requirements": [],
"version": "1.0.1"
}
Loading
Loading