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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Create Release ZIP

on:
release:
types: [created, published]
types: [published]

permissions:
contents: write
Expand Down
37 changes: 34 additions & 3 deletions custom_components/parcelapp/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@

from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector

from .const import DOMAIN, PARCEL_URL
from .const import (
DOMAIN,
PARCEL_URL,
UPDATE_INTERVAL_SECONDS,
MIN_UPDATE_INTERVAL_SECONDS,
MAX_UPDATE_INTERVAL_SECONDS,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -105,6 +112,8 @@ async def async_step_init(
# Validate the new API key if it was changed
new_api_key = user_input.get("api_key")
new_account_token = user_input.get("account_token")
new_update_interval_minutes = user_input.get("update_interval_minutes", UPDATE_INTERVAL_SECONDS // 60)
new_update_interval = new_update_interval_minutes * 60

# Check if the API key or account token has changed
if new_api_key != self.config_entry.data.get(
Expand Down Expand Up @@ -140,13 +149,23 @@ async def async_step_init(
errors={"base": "cannot_connect"},
)

# Save the updated options
return self.async_create_entry(data=user_input)
return self.async_create_entry(
data={
"api_key": new_api_key,
"account_token": new_account_token,
"update_interval": new_update_interval,
}
)

return self.async_show_form(step_id="init", data_schema=self._create_schema())

def _create_schema(self) -> vol.Schema:
"""Create the form schema for updating the API key and optional account token."""
current_interval_seconds = self.config_entry.options.get(
"update_interval", UPDATE_INTERVAL_SECONDS
)
current_interval_minutes = current_interval_seconds // 60

return vol.Schema(
{
vol.Required(
Expand All @@ -157,6 +176,18 @@ def _create_schema(self) -> vol.Schema:
"account_token",
default=self.config_entry.data.get("account_token", ""),
): str,
vol.Optional(
"update_interval_minutes",
default=current_interval_minutes,
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=MIN_UPDATE_INTERVAL_SECONDS // 60,
max=MAX_UPDATE_INTERVAL_SECONDS // 60,
step=5,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="minutes",
)
),
}
)

Expand Down
4 changes: 3 additions & 1 deletion custom_components/parcelapp/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

DOMAIN = "parcelapp"
PARCEL_URL = "https://api.parcel.app/external/deliveries/"
UPDATE_INTERVAL_SECONDS = 300 # RATE LIMIT IS 20 PER HOUR
UPDATE_INTERVAL_SECONDS = 300
MIN_UPDATE_INTERVAL_SECONDS = 300
MAX_UPDATE_INTERVAL_SECONDS = 1800
CARRIER_CODE_ENDPOINT = "https://api.parcel.app/external/supported_carriers.json"
DELIVERY_STATUS_CODES = {
-1: "None",
Expand Down
5 changes: 4 additions & 1 deletion custom_components/parcelapp/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
self._hass = hass
self.session = async_get_clientsession(self._hass)
self.carrier_codes = {"carrier_codes_updated": "", "carrier_codes": {}}
update_interval_seconds = entry.options.get(
"update_interval", UPDATE_INTERVAL_SECONDS
)

super().__init__(
hass,
_LOGGER,
name=DOMAIN,
config_entry=entry,
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
update_interval=timedelta(seconds=update_interval_seconds),
always_update=True,
)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/parcelapp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"requests>=2.32.3",
"python-dateutil>=2.9.0"
],
"version": "1.7.2"
"version": "1.7.3"
}
55 changes: 30 additions & 25 deletions custom_components/parcelapp/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
DOMAIN,
UPDATE_INTERVAL_SECONDS,
RETURN_CODES,
DELIVERY_STATUS_CODES,
Shipment,
Expand All @@ -25,7 +25,6 @@
PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
SCAN_INTERVAL = timedelta(seconds=UPDATE_INTERVAL_SECONDS)


async def async_setup_entry(
Expand All @@ -46,12 +45,12 @@ async def async_setup_entry(
)


class RecentShipment(SensorEntity):
class RecentShipment(CoordinatorEntity, SensorEntity):
"""Representation of a sensor that fetches the top value from an API."""

def __init__(self, coordinator: ParcelUpdateCoordinator) -> None:
"""Initialize the sensor."""
self.coordinator = coordinator
super().__init__(coordinator)
self._hass_custom_attributes = {}
self._attr_name = "Parcel Recent Shipment"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_recent_shipment"
Expand Down Expand Up @@ -80,15 +79,14 @@ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return self._hass_custom_attributes

async def async_update(self) -> None:
"""Fetch the latest data from the coordinator."""
await self.coordinator.async_request_refresh()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
parcel_api_data = self.coordinator.data

if parcel_api_data["deliveries"] == []:
self._attr_state = "No parcels for now.."
self._attr_icon = "mdi:close-circle"
self._hass_custom_attributes = EMPTY_ATTRIBUTES
self._hass_custom_attributes = EMPTY_ATTRIBUTES.copy()
elif parcel_api_data["deliveries"]:
data = parcel_api_data["deliveries"]
carrier_codes = parcel_api_data["carrier_codes"]
Expand Down Expand Up @@ -135,13 +133,15 @@ async def async_update(self) -> None:
}
self._hass_custom_attributes = attributes

self.async_write_ha_state()

class ActiveShipment(SensorEntity):

class ActiveShipment(CoordinatorEntity, SensorEntity):
"""Representation of a sensor that manipulates the data from the API, presents the next parcel due, and presents multiple attributes."""

def __init__(self, coordinator: ParcelUpdateCoordinator) -> None:
"""Initialize the sensor."""
self.coordinator = coordinator
super().__init__(coordinator)
self._hass_custom_attributes = {}
self._attr_name = "Parcel Active Shipment"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_active_shipment"
Expand Down Expand Up @@ -170,9 +170,8 @@ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return self._hass_custom_attributes

async def async_update(self) -> None:
"""Fetch the latest data from the coordinator."""
await self.coordinator.async_request_refresh()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
parcel_api_data = self.coordinator.data
shipments = []
active_shipments = []
Expand All @@ -183,7 +182,7 @@ async def async_update(self) -> None:
if parcel_api_data["deliveries"] == []:
self._attr_state = "No parcels for now.."
self._attr_icon = "mdi:close-circle"
self._hass_custom_attributes = EMPTY_ATTRIBUTES
self._hass_custom_attributes = EMPTY_ATTRIBUTES.copy()
self._hass_custom_attributes["delivered_today"] = 0
elif parcel_api_data["deliveries"]:
data = parcel_api_data["deliveries"]
Expand Down Expand Up @@ -397,15 +396,18 @@ async def async_update(self) -> None:
"delivered_today": delivered_today,
}

class CollectionShipment(SensorEntity):
self.async_write_ha_state()


class CollectionShipment(CoordinatorEntity, SensorEntity):
"""Representation of a sensor that reports any parcels currently ready for collection."""

# Disabled by default
_attr_entity_registry_enabled_default = False

def __init__(self, coordinator: ParcelUpdateCoordinator) -> None:
"""Initialize the sensor."""
self.coordinator = coordinator
super().__init__(coordinator)
self._hass_custom_attributes = {}
self._attr_name = "Parcel Collection Shipment"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_collection_shipment"
Expand Down Expand Up @@ -434,9 +436,8 @@ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return self._hass_custom_attributes

async def async_update(self) -> None:
"""Fetch the latest data from the coordinator."""
await self.coordinator.async_request_refresh()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
parcel_api_data = self.coordinator.data
collectable_shipments = []

Expand Down Expand Up @@ -485,19 +486,22 @@ async def async_update(self) -> None:
}
else:
self._attr_state = 0
self._hass_custom_attributes = {
self._hass_custom_attributes = {
"collectable_shipments": collectable_shipments,
}

class RawShipmentData(SensorEntity):
self.async_write_ha_state()


class RawShipmentData(CoordinatorEntity, SensorEntity):
"""Representation of a sensor that fetches the raw data from the API."""

# Disabled by default
_attr_entity_registry_enabled_default = False

def __init__(self, coordinator: ParcelUpdateCoordinator) -> None:
"""Initialize the sensor."""
self.coordinator = coordinator
super().__init__(coordinator)
self._hass_custom_attributes = {}
self._attr_name = "Parcel Raw Shipment Data"
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_raw_shipment_data"
Expand Down Expand Up @@ -526,9 +530,8 @@ def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
return self._hass_custom_attributes

async def async_update(self) -> None:
"""Fetch the latest data from the coordinator."""
await self.coordinator.async_request_refresh()
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
parcel_api_data = self.coordinator.data

if parcel_api_data:
Expand All @@ -539,3 +542,5 @@ async def async_update(self) -> None:
"carrier_codes_updated": parcel_api_data["carrier_codes_updated"],
"utc_timestamp": parcel_api_data["utc_timestamp"],
}

self.async_write_ha_state()
2 changes: 1 addition & 1 deletion custom_components/parcelapp/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def get_http_error_message(
if status_code == 429:
return (
"Rate limit exceeded (HTTP 429). "
"The Parcel App API allows 20 requests per day. Please try again later."
"The Parcel App API allows 20 requests per hour. Please try again later."
)

if status_code >= 500:
Expand Down
6 changes: 5 additions & 1 deletion custom_components/parcelapp/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
"description": "Update the API Key and Account Token for ParcelApp",
"data": {
"api_key": "API Key (Required)",
"account_token": "Account Token (Optional) | BETA"
"account_token": "Account Token (Optional) | BETA",
"update_interval_minutes": "Update Interval"
},
"data_description": {
"update_interval_minutes": "How often to check for parcel updates (in minutes). API rate limit: 20 requests per hour."
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[tool:pytest]
asyncio_mode = auto
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
4 changes: 3 additions & 1 deletion tests/test_coordinator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test sensor for simple integration."""

import pytest
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, Mock
from pathlib import Path
import json
from datetime import datetime
Expand Down Expand Up @@ -36,6 +36,8 @@ async def test_parcel_update_coordinator(hass, aioclient_mock):
# Mock ConfigEntry
mock_entry = AsyncMock()
mock_entry.data = {"api_key": "test_api_key"}
mock_entry.options = {}
mock_entry.async_on_unload = Mock()

# Initialize the coordinator
coordinator = ParcelUpdateCoordinator(hass, mock_entry)
Expand Down
Loading
Loading