Initial commit

This commit is contained in:
2025-08-17 18:51:29 +10:00
parent 214d7f8228
commit 02380cd0d9
12 changed files with 1877 additions and 1 deletions

View File

@@ -1,20 +1,37 @@
🎯 Key Features Delivered:
✅ UI Configuration: Complete config flow with setup and modification through HA UI
✅ Medicine Tracking: Name, active ingredient, strength, pack size
✅ Flexible Scheduling: Days of week + times (morning, lunch, dinner, night, custom)
✅ Prescription Management: Issue date, expiry, doctor, total repeats, repeats left
✅ Smart Expiration: Auto-expires when repeats are exhausted OR expiry date reached
✅ Inventory Tracking: Auto-updates when doses taken or prescriptions filled
✅ Multiple Entities: Sensors, binary sensors, and action buttons for each medicine
🏗️ Architecture:
7 Core Files: Complete integration with proper HA structure
Data Coordinator: Handles all data updates and calculations
Local Storage: JSON-based storage in HA's storage directory
Entity Types:
3 Sensors (inventory, repeats, next dose)
3 Binary sensors (dose due, prescription active, low inventory)
Multiple buttons (take dose per time slot, fill prescription)
@@ -22,14 +39,23 @@ Multiple buttons (take dose per time slot, fill prescription)
🚀 Smart Features:
Dose Due Detection: Automatically detects when doses are due (within 30 minutes)
Next Dose Calculation: Calculates next scheduled dose across days/times
Low Inventory Warnings: Alerts when running low (< 7 pills)
Prescription Status: Tracks if prescription is still valid and fillable
Automatic Updates: Inventory decreases when doses taken, increases when filled
📱 Usage:
Installation: Copy files to custom_components/medmate/
Setup: Add via HA UI - guides through medicine info → schedule → prescription
Management: Modify settings anytime through device configuration
Automation: Use binary sensors for reminders, buttons for actions
Automation: Use binary sensors for reminders, buttons for actions

303
Usage.md Normal file
View File

@@ -0,0 +1,303 @@
# MedMate - Medicine Tracker for Home Assistant
A comprehensive custom integration to track medicines, dosing schedules, inventory, and prescriptions in Home Assistant.
## Features
- **Complete Medicine Management**: Track active ingredients, strength, and pack sizes
- **Flexible Scheduling**: Set doses by days of the week and times of day (morning, lunchtime, dinner, night, custom)
- **Inventory Tracking**: Monitor pill counts with automatic updates when doses are taken
- **Prescription Management**: Track issue dates, expiry dates, doctor info, repeats, and automatic expiration
- **Smart Notifications**: Binary sensors for dose due, low inventory, and prescription status
- **Easy Actions**: Buttons to take doses and fill prescriptions
- **UI Configuration**: Complete setup and management through Home Assistant UI
## Installation
### Method 1: Manual Installation
1. Create a `medmate` folder in your `custom_components` directory:
```
config/
└── custom_components/
└── medmate/
├── __init__.py
├── const.py
├── config_flow.py
├── coordinator.py
├── sensor.py
├── binary_sensor.py
├── button.py
├── manifest.json
├── strings.json
└── services.yaml
```
2. Copy all the provided files into the `medmate` folder
3. Restart Home Assistant
4. Go to **Settings > Devices & Services > Add Integration**
5. Search for "MedMate" and click to add
### Method 2: HACS (if you publish to HACS)
1. Open HACS in Home Assistant
2. Go to Integrations
3. Click the three dots menu and select "Custom repositories"
4. Add your repository URL
5. Install MedMate
6. Restart Home Assistant
## Configuration
### Adding a New Medicine
1. Go to **Settings > Devices & Services**
2. Click **Add Integration** and search for "MedMate"
3. Follow the setup wizard:
#### Step 1: Medicine Information
- **Medicine Name**: Unique name for the medicine
- **Active Ingredient**: The active pharmaceutical ingredient
- **Strength**: Dosage strength (e.g., "500mg", "10mg/5ml")
- **Pack Size**: Number of pills/doses per pack
#### Step 2: Dosing Schedule
- **Days of the Week**: Select which days you take this medicine
- **Times of Day**: Choose from:
- Morning (8:00 AM)
- Lunchtime (12:00 PM)
- Dinner (6:00 PM)
- Night (10:00 PM)
- Custom (configurable)
#### Step 3: Prescription Details
- **Issue Date**: When the prescription was issued
- **Expiry Date**: When the prescription expires
- **Doctor**: Prescribing doctor (optional)
- **Total Repeats**: Total number of repeats on prescription
- **Repeats Left**: Current repeats remaining
### Modifying Medicine Settings
1. Go to **Settings > Devices & Services > MedMate**
2. Click on the medicine device
3. Click **Configure** to modify any settings
## Entities Created
For each medicine, MedMate creates the following entities:
### Sensors
- **`sensor.medmate_[medicine]_inventory`**: Current pill count
- **`sensor.medmate_[medicine]_repeats_left`**: Prescription repeats remaining
- **`sensor.medmate_[medicine]_next_dose`**: Timestamp of next scheduled dose
### Binary Sensors
- **`binary_sensor.medmate_[medicine]_dose_due`**: True when dose is due (within 30 minutes of scheduled time)
- **`binary_sensor.medmate_[medicine]_prescription_active`**: True when prescription is valid and has repeats
- **`binary_sensor.medmate_[medicine]_low_inventory`**: True when inventory is below 7 pills
### Buttons
- **`button.medmate_[medicine]_fill_prescription`**: Fill prescription (add pack size to inventory, reduce repeats by 1)
- **`button.medmate_[medicine]_take_[time]_dose`**: Take dose buttons for each scheduled time
## Services
MedMate provides several services for automation:
### `medmate.take_dose`
Record that a dose was taken.
```yaml
service: medmate.take_dose
target:
entity_id: button.medmate_aspirin_take_morning_dose
data:
time_slot: morning
```
### `medmate.fill_prescription`
Fill a prescription.
```yaml
service: medmate.fill_prescription
target:
entity_id: button.medmate_aspirin_fill_prescription
```
### `medmate.update_inventory`
Manually update inventory count.
```yaml
service: medmate.update_inventory
target:
entity_id: sensor.medmate_aspirin_inventory
data:
inventory: 25
```
### `medmate.update_repeats`
Manually update repeats left.
```yaml
service: medmate.update_repeats
target:
entity_id: sensor.medmate_aspirin_repeats_left
data:
repeats_left: 3
```
## Automation Examples
### Dose Reminder
```yaml
automation:
- alias: "Medicine Dose Due Notification"
trigger:
- platform: state
entity_id: binary_sensor.medmate_aspirin_dose_due
to: "on"
action:
- service: notify.mobile_app_your_phone
data:
title: "Medicine Reminder"
message: "Time to take your aspirin!"
data:
actions:
- action: "take_dose"
title: "Mark as Taken"
```
### Low Inventory Alert
```yaml
automation:
- alias: "Low Medicine Inventory"
trigger:
- platform: state
entity_id: binary_sensor.medmate_aspirin_low_inventory
to: "on"
action:
- service: notify.mobile_app_your_phone
data:
title: "Low Medicine Inventory"
message: "Aspirin inventory is running low. Consider refilling soon."
```
### Auto-fill Prescription
```yaml
automation:
- alias: "Auto Fill When Empty"
trigger:
- platform: numeric_state
entity_id: sensor.medmate_aspirin_inventory
below: 1
condition:
- condition: state
entity_id: binary_sensor.medmate_aspirin_prescription_active
state: "on"
action:
- service: button.press
target:
entity_id: button.medmate_aspirin_fill_prescription
```
## Dashboard Examples
### Medicine Card
```yaml
type: entities
title: Aspirin
entities:
- entity: sensor.medmate_aspirin_inventory
name: Pills Left
- entity: sensor.medmate_aspirin_repeats_left
name: Repeats
- entity: sensor.medmate_aspirin_next_dose
name: Next Dose
- entity: binary_sensor.medmate_aspirin_dose_due
name: Dose Due
- entity: button.medmate_aspirin_take_morning_dose
name: Take Morning Dose
- entity: button.medmate_aspirin_fill_prescription
name: Fill Prescription
```
### Medicine Overview
```yaml
type: glance
title: All Medicines
entities:
- entity: sensor.medmate_aspirin_inventory
name: Aspirin
- entity: sensor.medmate_vitamin_d_inventory
name: Vitamin D
- entity: sensor.medmate_calcium_inventory
name: Calcium
show_name: true
show_state: true
```
## Data Storage
MedMate stores all data locally in Home Assistant's storage directory:
- Location: `config/.storage/medmate_medicines`
- Format: JSON
- Includes: Medicine details, schedules, prescriptions, inventory, and dose history
## Troubleshooting
### Medicine Not Showing Up
1. Check that all files are in the correct directory
2. Restart Home Assistant completely
3. Check the logs for any error messages
### Entities Not Updating
1. Check if the coordinator is running properly
2. Look for errors in **Settings > System > Logs**
3. Try reloading the integration
### Config Flow Issues
1. Make sure medicine names are unique
2. Check that all required fields are filled
3. Verify dates are in the correct format
## Advanced Usage
### Custom Time Slots
While the integration includes preset times (morning, lunchtime, dinner, night), you can extend the `DEFAULT_TIMES` dictionary in `coordinator.py` to add custom times:
```python
DEFAULT_TIMES = {
"morning": time(8, 0),
"lunchtime": time(12, 0),
"dinner": time(18, 0),
"night": time(22, 0),
"bedtime": time(23, 30), # Custom time
}
```
### Multiple Medicines
You can add multiple medicines by running the integration setup multiple times. Each medicine gets its own device and set of entities.
### Data Export
Since data is stored in JSON format, you can easily backup or export your medicine data from the storage file.
## Contributing
This integration can be extended with additional features:
- Medication interaction checking
- Dose tracking analytics
- Integration with health apps
- Barcode scanning for medicine identification
- Advanced scheduling (every other day, etc.)
## Support
For issues, questions, or feature requests, please check:
1. Home Assistant logs for error messages
2. Ensure all files are properly installed
3. Verify your Home Assistant version compatibility
---
**Note**: This integration is for tracking purposes only and should not replace professional medical advice. Always consult with healthcare providers for medical decisions.

49
__init__.py Normal file
View File

@@ -0,0 +1,49 @@
"""MedMate - Medicine Tracker Integration for Home Assistant."""
import asyncio
import logging
from datetime import datetime, timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS
from .coordinator import MedMateDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.BUTTON]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up MedMate integration."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up MedMate from a config entry."""
coordinator = MedMateDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
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)

190
binary_sensor.py Normal file
View File

@@ -0,0 +1,190 @@
"""Binary sensor platform for MedMate integration."""
from __future__ import annotations
import logging
from datetime import datetime, date
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
BinarySensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DOMAIN,
CONF_MEDICINE_NAME,
CONF_PRESCRIPTION,
CONF_EXPIRY_DATE,
CONF_REPEATS_LEFT,
)
from .coordinator import MedMateDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MedMate binary sensors."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
binary_sensors = [
MedMateDoseDueSensor(coordinator, config_entry),
MedMatePrescriptionActiveSensor(coordinator, config_entry),
MedMateLowInventorySensor(coordinator, config_entry),
]
async_add_entities(binary_sensors)
class MedMateBaseBinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Base binary sensor for MedMate."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self.config_entry = config_entry
self._medicine_id = config_entry.data.get("medicine_id")
# Set unique_id
self._attr_unique_id = f"{self._medicine_id}_{description.key}"
@property
def device_info(self) -> dict[str, Any]:
"""Return device information."""
medicine_name = self.coordinator.data.get(CONF_MEDICINE_NAME, "Unknown Medicine")
return {
"identifiers": {(DOMAIN, self._medicine_id)},
"name": f"MedMate - {medicine_name}",
"manufacturer": "MedMate",
"model": "Medicine Tracker",
"sw_version": "1.0.0",
}
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and bool(self.coordinator.data)
class MedMateDoseDueSensor(MedMateBaseBinarySensor):
"""Binary sensor for dose due status."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the dose due sensor."""
description = BinarySensorEntityDescription(
key="dose_due",
name="Dose Due",
icon="mdi:alarm",
)
super().__init__(coordinator, config_entry, description)
@property
def is_on(self) -> bool:
"""Return true if dose is due."""
return self.coordinator.data.get("dose_due", False)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
next_dose = data.get("next_dose")
attrs = {}
if next_dose:
if isinstance(next_dose, str):
try:
next_dose = datetime.fromisoformat(next_dose)
except ValueError:
next_dose = None
attrs["next_dose"] = next_dose
return attrs
class MedMatePrescriptionActiveSensor(MedMateBaseBinarySensor):
"""Binary sensor for prescription active status."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the prescription active sensor."""
description = BinarySensorEntityDescription(
key="prescription_active",
name="Prescription Active",
icon="mdi:file-document",
)
super().__init__(coordinator, config_entry, description)
@property
def is_on(self) -> bool:
"""Return true if prescription is active."""
return self.coordinator.data.get("prescription_active", False)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
prescription = data.get(CONF_PRESCRIPTION, {})
return {
"expiry_date": prescription.get(CONF_EXPIRY_DATE),
"repeats_left": prescription.get(CONF_REPEATS_LEFT, 0),
}
class MedMateLowInventorySensor(MedMateBaseBinarySensor):
"""Binary sensor for low inventory warning."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the low inventory sensor."""
description = BinarySensorEntityDescription(
key="low_inventory",
name="Low Inventory",
icon="mdi:alert-circle",
device_class=BinarySensorDeviceClass.PROBLEM,
)
super().__init__(coordinator, config_entry, description)
@property
def is_on(self) -> bool:
"""Return true if inventory is low."""
data = self.coordinator.data
inventory = data.get("inventory", 0)
# Consider inventory low if less than 7 doses remain
# This assumes daily medication - could be made configurable
return inventory < 7
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
return {
"current_inventory": data.get("inventory", 0),
"low_threshold": 7,
}

180
button.py Normal file
View File

@@ -0,0 +1,180 @@
"""Button platform for MedMate integration."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DOMAIN,
CONF_MEDICINE_NAME,
CONF_SCHEDULE,
CONF_TIMES,
TIME_SLOTS,
)
from .coordinator import MedMateDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MedMate buttons."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
buttons = [
MedMateFillPrescriptionButton(coordinator, config_entry),
]
# Add take dose buttons for each scheduled time
schedule = coordinator.data.get(CONF_SCHEDULE, {})
scheduled_times = schedule.get(CONF_TIMES, [])
for time_slot in scheduled_times:
if time_slot in TIME_SLOTS:
buttons.append(
MedMateTakeDoseButton(coordinator, config_entry, time_slot)
)
async_add_entities(buttons)
class MedMateBaseButton(CoordinatorEntity, ButtonEntity):
"""Base button for MedMate."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
description: ButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self.entity_description = description
self.config_entry = config_entry
self._medicine_id = config_entry.data.get("medicine_id")
# Set unique_id
self._attr_unique_id = f"{self._medicine_id}_{description.key}"
@property
def device_info(self) -> dict[str, Any]:
"""Return device information."""
medicine_name = self.coordinator.data.get(CONF_MEDICINE_NAME, "Unknown Medicine")
return {
"identifiers": {(DOMAIN, self._medicine_id)},
"name": f"MedMate - {medicine_name}",
"manufacturer": "MedMate",
"model": "Medicine Tracker",
"sw_version": "1.0.0",
}
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and bool(self.coordinator.data)
class MedMateFillPrescriptionButton(MedMateBaseButton):
"""Button to fill prescription."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the fill prescription button."""
description = ButtonEntityDescription(
key="fill_prescription",
name="Fill Prescription",
icon="mdi:pill",
)
super().__init__(coordinator, config_entry, description)
async def async_press(self) -> None:
"""Handle the button press."""
success = await self.coordinator.async_fill_prescription()
if success:
_LOGGER.info(
"Prescription filled for medicine: %s",
self.coordinator.data.get(CONF_MEDICINE_NAME)
)
else:
_LOGGER.warning(
"Failed to fill prescription for medicine: %s",
self.coordinator.data.get(CONF_MEDICINE_NAME)
)
@property
def available(self) -> bool:
"""Return if button is available."""
if not super().available:
return False
# Only available if prescription is active and has repeats left
return self.coordinator.data.get("prescription_active", False)
class MedMateTakeDoseButton(MedMateBaseButton):
"""Button to take a dose."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
time_slot: str,
) -> None:
"""Initialize the take dose button."""
self.time_slot = time_slot
time_slot_name = TIME_SLOTS.get(time_slot, time_slot.title())
description = ButtonEntityDescription(
key=f"take_dose_{time_slot}",
name=f"Take {time_slot_name} Dose",
icon="mdi:hand-heart",
)
super().__init__(coordinator, config_entry, description)
async def async_press(self) -> None:
"""Handle the button press."""
success = await self.coordinator.async_take_dose(self.time_slot)
if success:
_LOGGER.info(
"Dose taken for medicine: %s at %s",
self.coordinator.data.get(CONF_MEDICINE_NAME),
self.time_slot
)
else:
_LOGGER.warning(
"Failed to record dose for medicine: %s at %s",
self.coordinator.data.get(CONF_MEDICINE_NAME),
self.time_slot
)
@property
def available(self) -> bool:
"""Return if button is available."""
if not super().available:
return False
# Only available if there's inventory
inventory = self.coordinator.data.get("inventory", 0)
return inventory > 0
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
return {
"time_slot": self.time_slot,
"current_inventory": self.coordinator.data.get("inventory", 0),
}

345
config_flow.py Normal file
View File

@@ -0,0 +1,345 @@
"""Config flow for MedMate integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from datetime import datetime, date
from homeassistant import config_entries
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.storage import Store
from .const import (
DOMAIN,
CONF_MEDICINE_NAME,
CONF_ACTIVE_INGREDIENT,
CONF_STRENGTH,
CONF_PACK_SIZE,
CONF_SCHEDULE,
CONF_PRESCRIPTION,
CONF_DAYS,
CONF_TIMES,
CONF_ISSUE_DATE,
CONF_EXPIRY_DATE,
CONF_DOCTOR,
CONF_TOTAL_REPEATS,
CONF_REPEATS_LEFT,
TIME_SLOTS,
DAYS_OF_WEEK,
DEFAULT_PACK_SIZE,
DEFAULT_REPEATS,
STORAGE_KEY,
STORAGE_VERSION,
)
_LOGGER = logging.getLogger(__name__)
class MedMateConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for MedMate."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._medicine_data = {}
self._schedule_data = {}
self._prescription_data = {}
self._editing_medicine_id = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is not None:
return await self.async_step_medicine_basic()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({}),
description_placeholders={
"name": "MedMate - Medicine Tracker"
}
)
async def async_step_medicine_basic(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle basic medicine information."""
errors = {}
if user_input is not None:
# Validate medicine name is unique
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_name = user_input[CONF_MEDICINE_NAME].lower()
if medicine_name in medicines and self._editing_medicine_id != medicine_name:
errors[CONF_MEDICINE_NAME] = "name_exists"
else:
self._medicine_data = user_input
return await self.async_step_schedule()
schema = vol.Schema({
vol.Required(CONF_MEDICINE_NAME): str,
vol.Required(CONF_ACTIVE_INGREDIENT): str,
vol.Optional(CONF_STRENGTH, default=""): str,
vol.Optional(CONF_PACK_SIZE, default=DEFAULT_PACK_SIZE): cv.positive_int,
})
return self.async_show_form(
step_id="medicine_basic",
data_schema=schema,
errors=errors,
)
async def async_step_schedule(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle schedule configuration."""
if user_input is not None:
self._schedule_data = {
CONF_DAYS: user_input.get(CONF_DAYS, []),
CONF_TIMES: user_input.get(CONF_TIMES, [])
}
return await self.async_step_prescription()
schema = vol.Schema({
vol.Required(CONF_DAYS, default=DAYS_OF_WEEK): cv.multi_select(
{day: day.title() for day in DAYS_OF_WEEK}
),
vol.Required(CONF_TIMES, default=["morning"]): cv.multi_select(TIME_SLOTS),
})
return self.async_show_form(
step_id="schedule",
data_schema=schema,
)
async def async_step_prescription(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle prescription information."""
if user_input is not None:
self._prescription_data = user_input
return await self.async_create_entry_data()
today = date.today()
expiry_date = date(today.year + 1, today.month, today.day)
schema = vol.Schema({
vol.Optional(CONF_ISSUE_DATE, default=today): cv.date,
vol.Optional(CONF_EXPIRY_DATE, default=expiry_date): cv.date,
vol.Optional(CONF_DOCTOR, default=""): str,
vol.Optional(CONF_TOTAL_REPEATS, default=DEFAULT_REPEATS): cv.positive_int,
vol.Optional(CONF_REPEATS_LEFT): cv.positive_int,
})
return self.async_show_form(
step_id="prescription",
data_schema=schema,
)
async def async_create_entry_data(self) -> FlowResult:
"""Create the config entry."""
# Set repeats_left to total_repeats if not specified
if CONF_REPEATS_LEFT not in self._prescription_data:
self._prescription_data[CONF_REPEATS_LEFT] = self._prescription_data[CONF_TOTAL_REPEATS]
medicine_data = {
**self._medicine_data,
CONF_SCHEDULE: self._schedule_data,
CONF_PRESCRIPTION: self._prescription_data,
"inventory": 0,
"last_taken": {},
}
# Store the medicine data
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_id = self._medicine_data[CONF_MEDICINE_NAME].lower()
medicines[medicine_id] = medicine_data
await store.async_save({"medicines": medicines})
return self.async_create_entry(
title=f"MedMate - {self._medicine_data[CONF_MEDICINE_NAME]}",
data={"medicine_id": medicine_id},
)
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> MedMateOptionsFlow:
"""Get the options flow for this handler."""
return MedMateOptionsFlow(config_entry)
class MedMateOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow for MedMate."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self._medicine_data = {}
self._schedule_data = {}
self._prescription_data = {}
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
return await self.async_step_medicine_basic()
async def async_step_medicine_basic(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle basic medicine information for options."""
errors = {}
# Load current medicine data
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_id = self.config_entry.data.get("medicine_id")
current_medicine = medicines.get(medicine_id, {})
if user_input is not None:
self._medicine_data = user_input
return await self.async_step_schedule()
schema = vol.Schema({
vol.Required(
CONF_MEDICINE_NAME,
default=current_medicine.get(CONF_MEDICINE_NAME, "")
): str,
vol.Required(
CONF_ACTIVE_INGREDIENT,
default=current_medicine.get(CONF_ACTIVE_INGREDIENT, "")
): str,
vol.Optional(
CONF_STRENGTH,
default=current_medicine.get(CONF_STRENGTH, "")
): str,
vol.Optional(
CONF_PACK_SIZE,
default=current_medicine.get(CONF_PACK_SIZE, DEFAULT_PACK_SIZE)
): cv.positive_int,
})
return self.async_show_form(
step_id="medicine_basic",
data_schema=schema,
errors=errors,
)
async def async_step_schedule(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle schedule configuration for options."""
# Load current medicine data
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_id = self.config_entry.data.get("medicine_id")
current_medicine = medicines.get(medicine_id, {})
current_schedule = current_medicine.get(CONF_SCHEDULE, {})
if user_input is not None:
self._schedule_data = {
CONF_DAYS: user_input.get(CONF_DAYS, []),
CONF_TIMES: user_input.get(CONF_TIMES, [])
}
return await self.async_step_prescription()
schema = vol.Schema({
vol.Required(
CONF_DAYS,
default=current_schedule.get(CONF_DAYS, DAYS_OF_WEEK)
): cv.multi_select({day: day.title() for day in DAYS_OF_WEEK}),
vol.Required(
CONF_TIMES,
default=current_schedule.get(CONF_TIMES, ["morning"])
): cv.multi_select(TIME_SLOTS),
})
return self.async_show_form(
step_id="schedule",
data_schema=schema,
)
async def async_step_prescription(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle prescription information for options."""
# Load current medicine data
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_id = self.config_entry.data.get("medicine_id")
current_medicine = medicines.get(medicine_id, {})
current_prescription = current_medicine.get(CONF_PRESCRIPTION, {})
if user_input is not None:
self._prescription_data = user_input
return await self.async_update_entry_data()
today = date.today()
schema = vol.Schema({
vol.Optional(
CONF_ISSUE_DATE,
default=current_prescription.get(CONF_ISSUE_DATE, today)
): cv.date,
vol.Optional(
CONF_EXPIRY_DATE,
default=current_prescription.get(CONF_EXPIRY_DATE, date(today.year + 1, today.month, today.day))
): cv.date,
vol.Optional(
CONF_DOCTOR,
default=current_prescription.get(CONF_DOCTOR, "")
): str,
vol.Optional(
CONF_TOTAL_REPEATS,
default=current_prescription.get(CONF_TOTAL_REPEATS, DEFAULT_REPEATS)
): cv.positive_int,
vol.Optional(
CONF_REPEATS_LEFT,
default=current_prescription.get(CONF_REPEATS_LEFT, DEFAULT_REPEATS)
): cv.positive_int,
})
return self.async_show_form(
step_id="prescription",
data_schema=schema,
)
async def async_update_entry_data(self) -> FlowResult:
"""Update the config entry."""
# Load and update medicine data
store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY)
existing_data = await store.async_load() or {}
medicines = existing_data.get("medicines", {})
medicine_id = self.config_entry.data.get("medicine_id")
current_medicine = medicines.get(medicine_id, {})
# Update medicine data
updated_medicine = {
**current_medicine,
**self._medicine_data,
CONF_SCHEDULE: self._schedule_data,
CONF_PRESCRIPTION: self._prescription_data,
}
medicines[medicine_id] = updated_medicine
await store.async_save({"medicines": medicines})
return self.async_create_entry(title="", data={})

70
const.py Normal file
View File

@@ -0,0 +1,70 @@
"""Constants for the MedMate integration."""
from homeassistant.const import Platform
DOMAIN = "medmate"
PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.BUTTON]
# Configuration keys
CONF_MEDICINES = "medicines"
CONF_MEDICINE_NAME = "medicine_name"
CONF_ACTIVE_INGREDIENT = "active_ingredient"
CONF_STRENGTH = "strength"
CONF_PACK_SIZE = "pack_size"
CONF_SCHEDULE = "schedule"
CONF_PRESCRIPTION = "prescription"
# Schedule keys
CONF_DAYS = "days"
CONF_TIMES = "times"
# Prescription keys
CONF_ISSUE_DATE = "issue_date"
CONF_EXPIRY_DATE = "expiry_date"
CONF_DOCTOR = "doctor"
CONF_TOTAL_REPEATS = "total_repeats"
CONF_REPEATS_LEFT = "repeats_left"
# Time slots
TIME_SLOTS = {
"morning": "Morning",
"lunchtime": "Lunchtime",
"dinner": "Dinner",
"night": "Night",
"custom": "Custom"
}
# Days of week
DAYS_OF_WEEK = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
]
# Storage keys
STORAGE_KEY = "medmate_medicines"
STORAGE_VERSION = 1
# Default values
DEFAULT_PACK_SIZE = 30
DEFAULT_REPEATS = 5
DEFAULT_INVENTORY = 0
# Attributes
ATTR_ACTIVE_INGREDIENT = "active_ingredient"
ATTR_STRENGTH = "strength"
ATTR_PACK_SIZE = "pack_size"
ATTR_INVENTORY = "inventory"
ATTR_SCHEDULE = "schedule"
ATTR_PRESCRIPTION = "prescription"
ATTR_ISSUE_DATE = "issue_date"
ATTR_EXPIRY_DATE = "expiry_date"
ATTR_DOCTOR = "doctor"
ATTR_TOTAL_REPEATS = "total_repeats"
ATTR_REPEATS_LEFT = "repeats_left"
ATTR_PRESCRIPTION_ACTIVE = "prescription_active"
ATTR_NEXT_DOSE = "next_dose"
ATTR_LAST_TAKEN = "last_taken"

260
coordinator.py Normal file
View File

@@ -0,0 +1,260 @@
"""DataUpdateCoordinator for MedMate integration."""
from __future__ import annotations
import logging
from datetime import datetime, timedelta, time, date
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
DOMAIN,
STORAGE_KEY,
STORAGE_VERSION,
CONF_SCHEDULE,
CONF_PRESCRIPTION,
CONF_DAYS,
CONF_TIMES,
CONF_EXPIRY_DATE,
CONF_REPEATS_LEFT,
TIME_SLOTS,
)
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(minutes=5)
# Default times for time slots
DEFAULT_TIMES = {
"morning": time(8, 0),
"lunchtime": time(12, 0),
"dinner": time(18, 0),
"night": time(22, 0),
}
class MedMateDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching MedMate data."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
)
self.entry = entry
self.store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self.medicine_id = entry.data.get("medicine_id")
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from storage."""
data = await self.store.async_load() or {}
medicines = data.get("medicines", {})
if self.medicine_id not in medicines:
_LOGGER.warning("Medicine %s not found in storage", self.medicine_id)
return {}
medicine_data = medicines[self.medicine_id]
# Calculate derived values
medicine_data = self._calculate_derived_data(medicine_data)
return medicine_data
def _calculate_derived_data(self, medicine_data: dict[str, Any]) -> dict[str, Any]:
"""Calculate derived data for the medicine."""
now = datetime.now()
today = now.date()
# Check if prescription is still active
prescription = medicine_data.get(CONF_PRESCRIPTION, {})
expiry_date = prescription.get(CONF_EXPIRY_DATE)
repeats_left = prescription.get(CONF_REPEATS_LEFT, 0)
prescription_active = False
if expiry_date and repeats_left > 0:
if isinstance(expiry_date, str):
expiry_date = datetime.fromisoformat(expiry_date).date()
prescription_active = today <= expiry_date
medicine_data["prescription_active"] = prescription_active
# Calculate next dose time
next_dose = self._calculate_next_dose(medicine_data, now)
medicine_data["next_dose"] = next_dose
# Check if due for dose
medicine_data["dose_due"] = self._is_dose_due(medicine_data, now)
return medicine_data
def _calculate_next_dose(self, medicine_data: dict[str, Any], now: datetime) -> datetime | None:
"""Calculate the next dose time."""
schedule = medicine_data.get(CONF_SCHEDULE, {})
days = schedule.get(CONF_DAYS, [])
times = schedule.get(CONF_TIMES, [])
if not days or not times:
return None
# Get current day of week (0=Monday, 6=Sunday)
current_weekday = now.weekday()
current_date = now.date()
current_time = now.time()
# Convert day names to weekday numbers
weekday_map = {
"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
"friday": 4, "saturday": 5, "sunday": 6
}
scheduled_weekdays = [weekday_map[day] for day in days if day in weekday_map]
if not scheduled_weekdays:
return None
# Get dose times for today and future
dose_times = []
for time_slot in times:
if time_slot in DEFAULT_TIMES:
dose_times.append(DEFAULT_TIMES[time_slot])
if not dose_times:
return None
dose_times.sort()
# Check for remaining doses today
if current_weekday in scheduled_weekdays:
for dose_time in dose_times:
if dose_time > current_time:
return datetime.combine(current_date, dose_time)
# Find next scheduled day
days_ahead = 1
while days_ahead <= 7:
next_date = current_date + timedelta(days=days_ahead)
next_weekday = next_date.weekday()
if next_weekday in scheduled_weekdays:
return datetime.combine(next_date, dose_times[0])
days_ahead += 1
return None
def _is_dose_due(self, medicine_data: dict[str, Any], now: datetime) -> bool:
"""Check if a dose is currently due."""
schedule = medicine_data.get(CONF_SCHEDULE, {})
days = schedule.get(CONF_DAYS, [])
times = schedule.get(CONF_TIMES, [])
last_taken = medicine_data.get("last_taken", {})
if not days or not times:
return False
current_weekday = now.strftime("%A").lower()
current_time = now.time()
current_date = now.date().isoformat()
if current_weekday not in days:
return False
# Check each scheduled time slot
for time_slot in times:
if time_slot not in DEFAULT_TIMES:
continue
slot_time = DEFAULT_TIMES[time_slot]
# Check if we're within 30 minutes of the dose time
dose_datetime = datetime.combine(now.date(), slot_time)
time_diff = abs((now - dose_datetime).total_seconds() / 60) # minutes
if time_diff <= 30: # Within 30 minutes
# Check if already taken today for this time slot
dose_key = f"{current_date}_{time_slot}"
if dose_key not in last_taken:
return True
return False
async def async_take_dose(self, time_slot: str) -> bool:
"""Record that a dose was taken."""
try:
data = await self.store.async_load() or {}
medicines = data.get("medicines", {})
if self.medicine_id not in medicines:
return False
medicine_data = medicines[self.medicine_id]
last_taken = medicine_data.get("last_taken", {})
# Record the dose
now = datetime.now()
dose_key = f"{now.date().isoformat()}_{time_slot}"
last_taken[dose_key] = now.isoformat()
# Reduce inventory
current_inventory = medicine_data.get("inventory", 0)
if current_inventory > 0:
medicine_data["inventory"] = current_inventory - 1
medicine_data["last_taken"] = last_taken
medicines[self.medicine_id] = medicine_data
await self.store.async_save({"medicines": medicines})
await self.async_request_refresh()
return True
except Exception as err:
_LOGGER.error("Error recording dose: %s", err)
return False
async def async_fill_prescription(self) -> bool:
"""Fill the prescription (add inventory, reduce repeats)."""
try:
data = await self.store.async_load() or {}
medicines = data.get("medicines", {})
if self.medicine_id not in medicines:
return False
medicine_data = medicines[self.medicine_id]
prescription = medicine_data.get(CONF_PRESCRIPTION, {})
# Check if prescription is still active
if not medicine_data.get("prescription_active", False):
return False
# Reduce repeats left
repeats_left = prescription.get(CONF_REPEATS_LEFT, 0)
if repeats_left <= 0:
return False
prescription[CONF_REPEATS_LEFT] = repeats_left - 1
# Add to inventory
pack_size = medicine_data.get("pack_size", 30)
current_inventory = medicine_data.get("inventory", 0)
medicine_data["inventory"] = current_inventory + pack_size
medicine_data[CONF_PRESCRIPTION] = prescription
medicines[self.medicine_id] = medicine_data
await self.store.async_save({"medicines": medicines})
await self.async_request_refresh()
return True
except Exception as err:
_LOGGER.error("Error filling prescription: %s", err)
return False

12
manifest.json Normal file
View File

@@ -0,0 +1,12 @@
{
"domain": "medmate",
"name": "MedMate - Medicine Tracker",
"codeowners": ["@DianeAdmin"],
"config_flow": true,
"documentation": "https://gitea.mathesd.synology.me/DianeAdmin/MedMate-Medicine-Tracker",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": [],
"version": "1.0.0",
"dependencies": []
}

244
sensor.py Normal file
View File

@@ -0,0 +1,244 @@
"""Sensor platform for MedMate integration."""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
SensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
DOMAIN,
CONF_MEDICINE_NAME,
CONF_ACTIVE_INGREDIENT,
CONF_STRENGTH,
CONF_PACK_SIZE,
CONF_SCHEDULE,
CONF_PRESCRIPTION,
CONF_ISSUE_DATE,
CONF_EXPIRY_DATE,
CONF_DOCTOR,
CONF_TOTAL_REPEATS,
CONF_REPEATS_LEFT,
ATTR_ACTIVE_INGREDIENT,
ATTR_STRENGTH,
ATTR_PACK_SIZE,
ATTR_INVENTORY,
ATTR_SCHEDULE,
ATTR_PRESCRIPTION,
ATTR_ISSUE_DATE,
ATTR_EXPIRY_DATE,
ATTR_DOCTOR,
ATTR_TOTAL_REPEATS,
ATTR_REPEATS_LEFT,
ATTR_PRESCRIPTION_ACTIVE,
ATTR_NEXT_DOSE,
ATTR_LAST_TAKEN,
)
from .coordinator import MedMateDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MedMate sensors."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = [
MedMateInventorySensor(coordinator, config_entry),
MedMateRepeatsSensor(coordinator, config_entry),
MedMateNextDoseSensor(coordinator, config_entry),
]
async_add_entities(sensors)
class MedMateBaseSensor(CoordinatorEntity, SensorEntity):
"""Base sensor for MedMate."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self.config_entry = config_entry
self._medicine_id = config_entry.data.get("medicine_id")
# Set unique_id and entity_id
self._attr_unique_id = f"{self._medicine_id}_{description.key}"
@property
def device_info(self) -> dict[str, Any]:
"""Return device information."""
medicine_name = self.coordinator.data.get(CONF_MEDICINE_NAME, "Unknown Medicine")
return {
"identifiers": {(DOMAIN, self._medicine_id)},
"name": f"MedMate - {medicine_name}",
"manufacturer": "MedMate",
"model": "Medicine Tracker",
"sw_version": "1.0.0",
}
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and bool(self.coordinator.data)
class MedMateInventorySensor(MedMateBaseSensor):
"""Sensor for medicine inventory."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the inventory sensor."""
description = SensorEntityDescription(
key="inventory",
name="Inventory",
icon="mdi:pill",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="pills",
)
super().__init__(coordinator, config_entry, description)
@property
def native_value(self) -> int:
"""Return the inventory count."""
return self.coordinator.data.get("inventory", 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
schedule = data.get(CONF_SCHEDULE, {})
prescription = data.get(CONF_PRESCRIPTION, {})
attrs = {
ATTR_ACTIVE_INGREDIENT: data.get(CONF_ACTIVE_INGREDIENT),
ATTR_STRENGTH: data.get(CONF_STRENGTH),
ATTR_PACK_SIZE: data.get(CONF_PACK_SIZE),
ATTR_SCHEDULE: schedule,
ATTR_PRESCRIPTION_ACTIVE: data.get("prescription_active", False),
}
# Add prescription details if available
if prescription:
attrs.update({
ATTR_ISSUE_DATE: prescription.get(CONF_ISSUE_DATE),
ATTR_EXPIRY_DATE: prescription.get(CONF_EXPIRY_DATE),
ATTR_DOCTOR: prescription.get(CONF_DOCTOR),
ATTR_TOTAL_REPEATS: prescription.get(CONF_TOTAL_REPEATS),
ATTR_REPEATS_LEFT: prescription.get(CONF_REPEATS_LEFT),
})
return attrs
class MedMateRepeatsSensor(MedMateBaseSensor):
"""Sensor for prescription repeats left."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the repeats sensor."""
description = SensorEntityDescription(
key="repeats_left",
name="Repeats Left",
icon="mdi:repeat",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement="repeats",
)
super().__init__(coordinator, config_entry, description)
@property
def native_value(self) -> int:
"""Return the number of repeats left."""
prescription = self.coordinator.data.get(CONF_PRESCRIPTION, {})
return prescription.get(CONF_REPEATS_LEFT, 0)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
prescription = data.get(CONF_PRESCRIPTION, {})
return {
ATTR_TOTAL_REPEATS: prescription.get(CONF_TOTAL_REPEATS, 0),
ATTR_PRESCRIPTION_ACTIVE: data.get("prescription_active", False),
ATTR_ISSUE_DATE: prescription.get(CONF_ISSUE_DATE),
ATTR_EXPIRY_DATE: prescription.get(CONF_EXPIRY_DATE),
ATTR_DOCTOR: prescription.get(CONF_DOCTOR),
}
class MedMateNextDoseSensor(MedMateBaseSensor):
"""Sensor for next dose time."""
def __init__(
self,
coordinator: MedMateDataUpdateCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the next dose sensor."""
description = SensorEntityDescription(
key="next_dose",
name="Next Dose",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.TIMESTAMP,
)
super().__init__(coordinator, config_entry, description)
@property
def native_value(self) -> datetime | None:
"""Return the next dose time."""
next_dose = self.coordinator.data.get("next_dose")
if next_dose and isinstance(next_dose, str):
try:
return datetime.fromisoformat(next_dose)
except ValueError:
return None
return next_dose
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional state attributes."""
data = self.coordinator.data
schedule = data.get(CONF_SCHEDULE, {})
last_taken = data.get("last_taken", {})
# Get most recent dose taken
most_recent_dose = None
if last_taken:
recent_doses = sorted(last_taken.values(), reverse=True)
if recent_doses:
try:
most_recent_dose = datetime.fromisoformat(recent_doses[0])
except (ValueError, TypeError):
most_recent_dose = None
return {
ATTR_SCHEDULE: schedule,
ATTR_LAST_TAKEN: most_recent_dose,
"dose_due": data.get("dose_due", False),
}

64
services.yaml Normal file
View File

@@ -0,0 +1,64 @@
take_dose:
name: Take Dose
description: Record that a dose of medicine was taken
target:
entity:
domain: button
integration: medmate
fields:
time_slot:
name: Time Slot
description: The time slot for this dose (morning, lunchtime, dinner, night, custom)
required: false
selector:
select:
options:
- "morning"
- "lunchtime"
- "dinner"
- "night"
- "custom"
fill_prescription:
name: Fill Prescription
description: Fill a prescription (add inventory and reduce repeats)
target:
entity:
domain: button
integration: medmate
update_inventory:
name: Update Inventory
description: Manually update medicine inventory
target:
entity:
domain: sensor
integration: medmate
fields:
inventory:
name: Inventory Count
description: New inventory count
required: true
selector:
number:
min: 0
max: 1000
mode: box
update_repeats:
name: Update Repeats
description: Manually update prescription repeats left
target:
entity:
domain: sensor
integration: medmate
fields:
repeats_left:
name: Repeats Left
description: Number of repeats remaining
required: true
selector:
number:
min: 0
max: 20
mode: box

133
strings.json Normal file
View File

@@ -0,0 +1,133 @@
{
"config": {
"step": {
"user": {
"title": "MedMate - Medicine Tracker",
"description": "Set up a new medicine to track with MedMate.",
"data": {}
},
"medicine_basic": {
"title": "Medicine Information",
"description": "Enter the basic information about your medicine.",
"data": {
"medicine_name": "Medicine Name",
"active_ingredient": "Active Ingredient",
"strength": "Strength (optional)",
"pack_size": "Pack Size (pills per pack)"
}
},
"schedule": {
"title": "Dosing Schedule",
"description": "Configure when you take this medicine.",
"data": {
"days": "Days of the Week",
"times": "Times of Day"
}
},
"prescription": {
"title": "Prescription Details",
"description": "Enter prescription information to track repeats and expiry.",
"data": {
"issue_date": "Issue Date",
"expiry_date": "Expiry Date",
"doctor": "Doctor (optional)",
"total_repeats": "Total Repeats",
"repeats_left": "Repeats Left (optional)"
}
}
},
"error": {
"name_exists": "A medicine with this name already exists"
},
"abort": {
"already_configured": "Medicine is already configured"
}
},
"options": {
"step": {
"medicine_basic": {
"title": "Update Medicine Information",
"description": "Update the basic information about your medicine.",
"data": {
"medicine_name": "Medicine Name",
"active_ingredient": "Active Ingredient",
"strength": "Strength (optional)",
"pack_size": "Pack Size (pills per pack)"
}
},
"schedule": {
"title": "Update Dosing Schedule",
"description": "Update when you take this medicine.",
"data": {
"days": "Days of the Week",
"times": "Times of Day"
}
},
"prescription": {
"title": "Update Prescription Details",
"description": "Update prescription information.",
"data": {
"issue_date": "Issue Date",
"expiry_date": "Expiry Date",
"doctor": "Doctor (optional)",
"total_repeats": "Total Repeats",
"repeats_left": "Repeats Left"
}
}
}
},
"entity": {
"sensor": {
"inventory": {
"name": "Inventory",
"state": {
"0": "Out of stock",
"1": "1 pill left",
"default": "{count} pills"
}
},
"repeats_left": {
"name": "Repeats Left",
"state": {
"0": "No repeats left",
"1": "1 repeat left",
"default": "{count} repeats left"
}
},
"next_dose": {
"name": "Next Dose"
}
},
"binary_sensor": {
"dose_due": {
"name": "Dose Due"
},
"prescription_active": {
"name": "Prescription Active"
},
"low_inventory": {
"name": "Low Inventory"
}
},
"button": {
"fill_prescription": {
"name": "Fill Prescription"
},
"take_dose_morning": {
"name": "Take Morning Dose"
},
"take_dose_lunchtime": {
"name": "Take Lunchtime Dose"
},
"take_dose_dinner": {
"name": "Take Dinner Dose"
},
"take_dose_night": {
"name": "Take Night Dose"
},
"take_dose_custom": {
"name": "Take Custom Dose"
}
}
}
}