Skip to content
Closed
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
18 changes: 6 additions & 12 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,19 @@ name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

permissions:
contents: read
packages: read
permissions: {}

jobs:
validate-hassfest:
name: Hassfest Validation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
uses: actions/checkout@v4

- name: Run Hassfest validation
uses: home-assistant/actions/hassfest@master
Expand All @@ -31,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4

- name: Run HACS validation
uses: hacs/action@main
Expand Down
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg-info/
*.egg
dist/
build/
venv/
.venv/

# IDE - PyCharm / JetBrains
.idea/

# macOS
.DS_Store
.AppleDouble
.LSOverride
._*

# Home Assistant
config/
.storage/

# Environment
.env
.env.*

# Testing
.pytest_cache/
.coverage
htmlcov/
204 changes: 43 additions & 161 deletions custom_components/petlibro/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import logging

from datetime import timedelta # For managing the update interval
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed # For coordinator and update handling
from .devices import Device
from .devices.feeders.feeder import Feeder
from .devices.feeders.air_smart_feeder import AirSmartFeeder
Expand All @@ -20,134 +18,42 @@
from .devices.fountains.dockstream_2_smart_fountain import Dockstream2SmartFountain
from .devices.litterboxes.litter_box import LitterBox
from .devices.litterboxes.luma_smart_litter_box import LumaSmartLitterBox
from .const import DOMAIN, CONF_EMAIL, CONF_PASSWORD, PLATFORMS, UPDATE_INTERVAL_SECONDS # Assuming UPDATE_INTERVAL_SECONDS is defined in const
from .const import DOMAIN, CONF_EMAIL, CONF_PASSWORD, PLATFORMS
from .hub import PetLibroHub

_LOGGER = logging.getLogger(__name__)

type PetLibroConfigEntry = ConfigEntry[PetLibroHub]


# All device types currently support the same set of platforms
DEFAULT_PLATFORMS = (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE,
)

# Define the platforms for each device type
PLATFORMS_BY_TYPE = {
Feeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
AirSmartFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
GranarySmartFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
GranarySmartCameraFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
OneRFIDSmartFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
PolarWetFoodFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
SpaceSmartFeeder: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
DockstreamSmartFountain: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
DockstreamSmartRFIDFountain: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
Dockstream2SmartCordlessFountain: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
Dockstream2SmartFountain: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
LumaSmartLitterBox: (
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
Platform.BUTTON,
Platform.NUMBER,
Platform.SELECT,
Platform.TEXT,
Platform.UPDATE
),
cls: DEFAULT_PLATFORMS
for cls in [
Feeder,
AirSmartFeeder,
GranarySmartFeeder,
GranarySmartCameraFeeder,
OneRFIDSmartFeeder,
PolarWetFoodFeeder,
SpaceSmartFeeder,
DockstreamSmartFountain,
DockstreamSmartRFIDFountain,
Dockstream2SmartCordlessFountain,
Dockstream2SmartFountain,
LumaSmartLitterBox,
]
}


Expand All @@ -162,76 +68,52 @@ def get_platforms_for_devices(devices: list[Device]) -> set[Platform]:
}


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: PetLibroConfigEntry) -> bool:
"""Set up platform from a ConfigEntry."""
email = entry.data.get(CONF_EMAIL)
password = entry.data.get(CONF_PASSWORD)

# Ensure email and password exist
if not email or not password:
_LOGGER.error("Email or password is missing in the configuration entry. Cannot proceed.")
_LOGGER.error("Email or password is missing in the configuration entry.")
return False

# Initialize PetLibroHub
try:
hub = PetLibroHub(hass, entry)
entry.runtime_data = hub

# Store the hub in hass.data
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub

# Load member only once here
await hub.load_member()

# Load devices only once here
await hub.load_devices()

# Initialize Helpers
await hub._initialize_helpers()

# Start the coordinator for periodic updates
await hub.coordinator.async_config_entry_first_refresh()

# Forward entry setups for each platform
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

_LOGGER.info(f"Successfully set up PetLibro integration for {email}")
_LOGGER.info("Successfully set up PetLibro integration for %s", email)
return True

except Exception as err:
_LOGGER.error(f"Failed to set up PetLibro integration: {err}", exc_info=True)
_LOGGER.error("Failed to set up PetLibro integration: %s", err, exc_info=True)
return False


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: PetLibroConfigEntry) -> bool:
"""Unload a config entry."""
# Get the hub from Home Assistant's domain data
hub = hass.data[DOMAIN].pop(entry.entry_id, None)

if hub is None:
_LOGGER.warning(f"PetLibro hub for entry {entry.entry_id} not found.")
return False

# Unload platforms associated with the entry
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
_LOGGER.info(f"Successfully unloaded PetLibro entry for {entry.data.get(CONF_EMAIL)}")
await hub.async_unload() # If you have any cleanup to do in the hub
await entry.runtime_data.async_unload()
_LOGGER.info("Successfully unloaded PetLibro entry for %s", entry.data.get(CONF_EMAIL))
else:
_LOGGER.error(f"Failed to unload PetLibro entry for {entry.data.get(CONF_EMAIL)}")
_LOGGER.error("Failed to unload PetLibro entry for %s", entry.data.get(CONF_EMAIL))

return unload_ok


async def async_remove_config_entry_device(hass: HomeAssistant, entry: ConfigEntry, device_entry: DeviceEntry) -> bool:
async def async_remove_config_entry_device(
hass: HomeAssistant, entry: PetLibroConfigEntry, device_entry: DeviceEntry
) -> bool:
"""Remove a config entry from a device."""
hub = hass.data[DOMAIN].get(entry.entry_id)

if not hub:
_LOGGER.warning("No hub found for this entry during device removal.")
return False
hub = entry.runtime_data

# Match the serial number with devices in the hub
return not any(
identifier
for identifier in device_entry.identifiers
Expand Down
Loading