Skip to content

Commit

Permalink
remove deprecated async_add_job() call, improve error handling and de…
Browse files Browse the repository at this point in the history
…bug logging
  • Loading branch information
c99koder committed Oct 9, 2024
1 parent ae42c4a commit 17d3d45
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 76 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.2.0",
"configurations": [
{
"command": "/usr/local/bin/hass",
"command": "${workspaceFolder}/scripts/develop",
"name": "Run Home Assistant",
"request": "launch",
"type": "node-terminal"
Expand Down Expand Up @@ -37,4 +37,4 @@
]
}
]
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Code template was mainly taken from [@Ludeeus](https://github.com/ludeeus)'s [in
## License
Copyright (C) 2022 Sam Steele. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
Copyright (C) 2024 Sam Steele. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Expand Down
2 changes: 2 additions & 0 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ logger:
default: info
logs:
custom_components.medisafe: debug

debugpy:
77 changes: 39 additions & 38 deletions custom_components/medisafe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022 Sam Steele
# Copyright (C) 2024 Sam Steele
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -10,14 +10,13 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import logging
from datetime import timedelta
from dataclasses import dataclass

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import UpdateFailed
Expand Down Expand Up @@ -45,68 +44,70 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data.setdefault(DOMAIN, {})
_LOGGER.info(STARTUP_MESSAGE)

username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
coordinator = MedisafeDataUpdateCoordinator(hass)

session = async_get_clientsession(hass)
client = MedisafeApiClient(username, password, session)

coordinator = MedisafeDataUpdateCoordinator(hass, client=client)
await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady
entry.runtime_data = MedisafeData(
client=MedisafeApiClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
),
coordinator=coordinator,
)

hass.data[DOMAIN][entry.entry_id] = coordinator
await coordinator.async_config_entry_first_refresh()

for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))

entry.add_update_listener(async_reload_entry)
return True


class MedisafeDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""

def __init__(self, hass: HomeAssistant, client: MedisafeApiClient) -> None:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self.api = client
self.platforms = []

super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)

async def _async_update_data(self):
"""Update data via library."""
try:
return await self.api.async_get_data()
return await self.config_entry.runtime_data.client.async_get_data()
except Exception as exception:
raise UpdateFailed() from exception

def get_medication(self, uuid):
if "medications" not in self.data:
_LOGGER.error("Medisafe has no data yet")
return None

if "medications" in self.data:
for medication in self.data["medications"]:
if medication["uuid"] == uuid:
return medication

_LOGGER.error(f"Medication not found for UUID: {uuid}")

return None


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
if platform in coordinator.platforms
]
)
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)

return unloaded
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)


@dataclass
class MedisafeData:
"""Data for the Medisafe integration."""

client: MedisafeApiClient
coordinator: MedisafeDataUpdateCoordinator
12 changes: 10 additions & 2 deletions custom_components/medisafe/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from datetime import datetime
from datetime import timedelta

from homeassistant.exceptions import ConfigEntryAuthFailed

import aiohttp
import async_timeout

Expand All @@ -39,10 +41,16 @@ async def async_get_data(self) -> dict:
{"username": self._username, "password": self._password},
)
if "error" in auth:
raise Exception(auth["error"]["message"])
if "message" in auth["error"]:
_LOGGER.error(f"Authentication failed: {auth['error']['message']}")
raise ConfigEntryAuthFailed(auth["error"]["message"])
else:
_LOGGER.error(f"Authentication failed: {auth['error']}")
raise ConfigEntryAuthFailed(auth["error"])

if "token" not in auth:
raise Exception("Authentication Failed")
_LOGGER.error(f"No token recieved")
raise ConfigEntryAuthFailed("Authentication Failed")

start = int((datetime.today() - timedelta(days=1)).timestamp() * 1000)
end = int((datetime.today() + timedelta(days=1)).timestamp() * 1000)
Expand Down
4 changes: 2 additions & 2 deletions custom_components/medisafe/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022 Sam Steele
# Copyright (C) 2024 Sam Steele
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -14,7 +14,7 @@
NAME = "Medisafe"
DOMAIN = "medisafe"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.0.2"
VERSION = "0.0.4"

ATTRIBUTION = "Data provided by https://medisafe.com/"
ISSUE_URL = "https://github.com/c99koder/ha-medisafe/issues"
Expand Down
12 changes: 8 additions & 4 deletions custom_components/medisafe/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
"name": "Medisafe",
"documentation": "https://github.com/c99koder/ha-medisafe",
"issue_tracker": "https://github.com/c99koder/ha-medisafe/issues",
"dependencies": ["http"],
"dependencies": [
"http"
],
"config_flow": true,
"codeowners": ["@c99koder"],
"codeowners": [
"@c99koder"
],
"iot_class": "cloud_polling",
"requirements": [],
"version": "0.0.3"
}
"version": "0.0.4"
}
73 changes: 51 additions & 22 deletions custom_components/medisafe/sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022 Sam Steele
# Copyright (C) 2024 Sam Steele
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
Expand All @@ -10,6 +10,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging

from datetime import date

from homeassistant.components.sensor import SensorStateClass
Expand All @@ -19,36 +21,55 @@
from .const import CONF_USERNAME
from .const import DOMAIN

_LOGGER: logging.Logger = logging.getLogger(__package__)


async def async_setup_entry(hass, entry, async_add_devices):
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data.coordinator
await coordinator.async_config_entry_first_refresh()

entities = []

for idx, ent in enumerate(coordinator.data["medications"]):
if ent["treatmentStatus"] == 1:
entities.append(MedisafeMedicationEntity(coordinator, entry, idx))
if coordinator.data is not None and "medications" in coordinator.data:
_LOGGER.info(f"Got {len(coordinator.data['medications'])} medications")
for ent in coordinator.data["medications"]:
if "pillsLeft" not in ent:
_LOGGER.debug(f"Missing pillsLeft: {ent['name']} with UUID {ent['uuid']}")
elif ent["treatmentStatus"] != 1:
_LOGGER.debug(f"Inactive medication: {ent['name']} with UUID {ent['uuid']}")
else:
_LOGGER.debug(f"Adding: {ent['name']} with UUID {ent['uuid']}")
entities.append(MedisafeMedicationEntity(coordinator, entry, ent["uuid"]))

entities.append(MedisafeStatusCountEntity(coordinator, entry, "taken"))
entities.append(MedisafeStatusCountEntity(coordinator, entry, "missed"))
entities.append(MedisafeStatusCountEntity(coordinator, entry, "dismissed"))
entities.append(MedisafeStatusCountEntity(coordinator, entry, "pending"))

_LOGGER.info(f"Setting up {len(entities)} entities")
async_add_devices(entities)


class MedisafeStatusCountEntity(CoordinatorEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_unit_of_measurement = "doses"
_attr_icon = "mdi:pill"
_attr_suggested_display_precision = 0
_attr_has_entity_name = True

def __init__(self, coordinator, config_entry, status):
super().__init__(coordinator)
self.status = status
self.config_entry = config_entry
self._attr_unique_id = f"medication_{self.config_entry.entry_id}_{status}"
self._attr_name = f"medication {status}".title()

@property
def name(self):
return f"medication {self.status}".title()

@property
def available(self):
return "items" in self.coordinator.data

@property
def state(self):
Expand All @@ -72,39 +93,46 @@ def extra_state_attributes(self):

class MedisafeMedicationEntity(CoordinatorEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_device_class = "medication"
_attr_unit_of_measurement = "pills"
_attr_icon = "mdi:pill"
_attr_suggested_display_precision = 0
_attr_has_entity_name = True

def __init__(self, coordinator, config_entry, idx):
def __init__(self, coordinator, config_entry, uuid):
super().__init__(coordinator)
self.idx = idx
self.uuid = uuid
self.config_entry = config_entry
self._attr_unique_id = f"medication_{self.config_entry.entry_id}_{self.coordinator.data['medications'][self.idx]['uuid']}"
self._attr_name = self.coordinator.data["medications"][self.idx]["name"]
self._attr_unique_id = f"medication_{self.config_entry.entry_id}_{uuid}"

@property
def name(self):
return self.coordinator.get_medication(self.uuid)["name"]

@property
def state(self):
if "pillsLeft" in self.coordinator.data["medications"][self.idx]:
return self.coordinator.data["medications"][self.idx]["pillsLeft"]
medication = self.coordinator.get_medication(self.uuid)

if "pillsLeft" in medication:
return medication["pillsLeft"]
else:
return None

@property
def available(self):
return (
"medications" in self.coordinator.data
and "pillsLeft" in self.coordinator.data["medications"][self.idx]
)
medication = self.coordinator.get_medication(self.uuid)

return (medication is not None and "pillsLeft" in medication)

@property
def extra_state_attributes(self):
if "dose" in self.coordinator.data["medications"][self.idx]:
medication = self.coordinator.get_medication(self.uuid)

if "dose" in medication:
return {
"account": self.config_entry.data.get(CONF_USERNAME),
"attribution": ATTRIBUTION,
"integration": DOMAIN,
"dose": self.coordinator.data["medications"][self.idx]["dose"],
"dose": medication["dose"],
}
else:
return {
Expand All @@ -115,8 +143,9 @@ def extra_state_attributes(self):

@property
def entity_picture(self):
med = self.coordinator.data["medications"][self.idx]
if med is None or med["shape"] == "capsule":
medication = self.coordinator.get_medication(self.uuid)

if medication is None or medication["shape"] == "capsule":
return None
else:
return f"https://web.medisafe.com/medication-icons/pill_{med['shape']}_{med['color']}.png"
return f"https://web.medisafe.com/medication-icons/pill_{medication['shape']}_{medication['color']}.png"
4 changes: 2 additions & 2 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "Medisafe",
"hacs": "1.6.0",
"homeassistant": "2022.2.1"
}
"homeassistant": "2024.8.0"
}
4 changes: 1 addition & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
colorlog==6.8.2
homeassistant==2024.10.1
pip>=21.3.1
ruff==0.6.7
homeassistant==2024.10.0

0 comments on commit 17d3d45

Please sign in to comment.