Python package for Home Assistant API with real-time WebSocket state monitoring
Project description
m-hass-api
A Python package for interacting with Home Assistant API, featuring both REST API client and real-time WebSocket state monitoring, complete with sample code, unit tests, and deployment documentation.
Project Structure
m-hass-api/
├── README.md
├── setup.py
├── requirements.txt
├── .gitignore
├── m_hass_api/
│ ├── __init__.py
│ └── sample.py
└── tests/
├── __init__.py
└── test_sample.py
Installation
From Source (Development)
# Clone the repository
git clone <repository-url>
cd m-hass-api
# Create a virtual environment
python -m venv venv
# Activate the virtual environment
# On macOS/Linux:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# Install the package in development mode
pip install -e .
From PyPI (Production)
pip install m-hass-api
Usage
HassApiClient - REST API Client
Basic Example
from m_hass_api import HassApiClient
# Create a client instance
client = HassApiClient(
base_url="http://homeassistant.local:8123",
api_key="your_long_lived_access_token",
tz="America/New_York"
)
# Get all states as DataFrame
states_df = client.get_states()
print(states_df)
# Get specific entity state
temp = client.get_state_as_numeric("sensor.temperature")
print(f"Temperature: {temp}°C")
# Get entity attribute
next_rising = client.get_state_attribute_as_datetime("sun.sun", "next_rising")
print(f"Sun rises at: {next_rising}")
# Get state history
history_df = client.get_state_history(
["sensor.temperature"],
start_time=datetime.now(tz=timezone.utc) - timedelta(hours=24)
)
print(history_df)
HassStateMonitor - Real-Time WebSocket Monitor
Overview
HassStateMonitor provides real-time monitoring of Home Assistant entity state changes using WebSocket connections. It features automatic reconnection, type-safe state conversion, timezone support, thread-safe operation, and robust error handling.
Features
- Real-time monitoring - Instant notifications when entity states change
- Automatic reconnection - Automatically reconnects on connection loss
- Thread-safe operation - Safe for concurrent access with proper locking
- Type conversion - Automatic conversion to numeric, datetime, boolean, or integer
- Timezone support - Datetime fields automatically converted to your timezone
- Graceful shutdown - Clean shutdown with configurable timeout
- Error isolation - User callback errors don't crash the monitor
- Comprehensive logging - Full logging for debugging and monitoring
Basic Usage
from m_hass_api.hass_state_monitor import HassStateMonitor
from zoneinfo import ZoneInfo
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Define callback for state changes
def on_state_change(event):
print(f"\n=== State Change ====")
print(f"Entity: {event.entity_id}")
print(f"Type: {event.data_type}")
print(f"Old: {event.old_state} -> New: {event.new_state}")
print(f"Last Updated: {event.last_updated}")
# Type-specific handling
if event.data_type == 'numeric':
if event.old_state is not None and event.new_state is not None:
change = event.new_state - event.old_state
print(f"Change: {change:+.2f}")
elif event.data_type == 'datetime':
if event.new_state:
print(f"New value: {event.new_state.strftime('%Y-%m-%d %H:%M:%S')}")
# Create monitor
monitor = HassStateMonitor(
hostname="ws://homeassistant.local:8123/api/websocket",
api_key="your_long_lived_access_token",
entities={
"sensor.temperature": "numeric",
"sensor.humidity": "numeric",
"binary_sensor.door": "bool",
"sensor.last_motion": "datetime",
"input_text.message": "str",
"sensor.counter": "int"
},
callback=on_state_change,
tz="Australia/Sydney"
)
# Start monitoring (non-blocking)
monitor.start()
# Monitor runs in background...
# ... do other work ...
# Stop monitoring with graceful shutdown
monitor.stop(timeout=5.0)
Advanced Usage
Conditional Callbacks
def smart_thermostat_callback(event):
# Only process temperature changes
if event.entity_id != "sensor.thermostat":
return
# Significant temperature change
if event.new_state is not None and event.old_state is not None:
change = event.new_state - event.old_state
if abs(change) > 2.0:
print(f"⚠️ Large temperature change: {change:+.1f}°C")
# Trigger automation...
else:
print(f"📊 Temperature change: {change:+.1f}°C")
monitor = HassStateMonitor(
"ws://homeassistant.local:8123/api/websocket",
"your_token",
{"sensor.thermostat": "numeric"},
smart_thermostat_callback
)
Tracking Multiple Entities
# Monitor multiple sensors and aggregate data
entity_states = {}
def multi_entity_callback(event):
entity_id = event.entity_id
new_state = event.new_state
# Update tracked state
entity_states[entity_id] = new_state
# Check conditions across entities
if entity_states.get("sensor.temperature", 0) > 25:
if entity_states.get("binary_sensor.ac_on", False) is False:
print("🌡️ Temperature high, turning on AC")
# Trigger AC automation...
Error Handling in Callbacks
def robust_callback(event):
try:
# Your callback logic here
process_event(event)
except ValueError as e:
print(f"Invalid value in event: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
# Note: This won't crash the monitor!
# Errors are caught and logged automatically
monitor = HassStateMonitor(..., callback=robust_callback)
API Reference
HassStateMonitor
class HassStateMonitor:
def __init__(
self,
hostname: str,
api_key: str,
entities: Dict[str, str],
callback: Callable[[StateChangeEvent], None],
tz: Union[ZoneInfo, str, None] = None
)
Parameters:
hostname(str): WebSocket server URL (e.g., "ws://localhost:8123/api/websocket")api_key(str): Home Assistant long-lived access tokenentities(Dict[str, str]): Dictionary mapping entity IDs to data typescallback(Callable): Function called on state changestz(Union[ZoneInfo, str, None]): Timezone for datetime conversion
Methods:
start()- Start monitoring (non-blocking)stop(timeout: float = 5.0)- Stop monitoring with graceful shutdown
StateChangeEvent
@dataclass
class StateChangeEvent:
entity_id: str
subscription_id: int
data_type: str
new_state: Any
old_state: Any
new_state_raw: str
old_state_raw: str
new_attributes: Dict[str, Any]
old_attributes: Dict[str, Any]
last_changed: Optional[datetime]
last_updated: Optional[datetime]
for_duration: Optional[str] = None
Data Types
| Type | Aliases | Conversion | Example |
|---|---|---|---|
| Numeric | "numeric" | float(value) |
23.5 |
| String | "str", "string" | str(value) |
"hello" |
| Boolean | "bool", "boolean" | on/true/1 → True, off/false/0 → False | True |
| Integer | "int", "integer" | int(float(value)) |
42 |
| Datetime | "datetime" | ISO 8601 to datetime with timezone | 2024-02-14 10:30:00+11:00 |
Configuration Examples
Different Timezones
from zoneinfo import ZoneInfo
# Using string timezone
monitor = HassStateMonitor(..., tz="UTC")
# Using ZoneInfo object
monitor = HassStateMonitor(..., tz=ZoneInfo("America/Los_Angeles"))
# No timezone conversion (keep original)
monitor = HassStateMonitor(..., tz=None)
WebSocket URLs
# Standard WebSocket (ws://)
monitor = HassStateMonitor(
hostname="ws://192.168.1.100:8123/api/websocket",
...
)
# Secure WebSocket (wss://) - ensure hostname includes wss://
monitor = HassStateMonitor(
hostname="wss://homeassistant.example.com/api/websocket",
...
)
# Hostname only (auto-appends /api/websocket)
monitor = HassStateMonitor(
hostname="homeassistant.local:8123",
...
)
Best Practices
-
Always configure logging to see connection status and errors:
logging.basicConfig(level=logging.INFO)
-
Use graceful shutdown to ensure clean thread termination:
try: monitor.start() # ... monitoring ... finally: monitor.stop(timeout=5.0)
-
Handle exceptions in callbacks even though they're isolated:
def callback(event): try: process(event) except Exception as e: logger.error(f"Callback error: {e}")
-
Use appropriate data types for reliable conversions:
entities = { "sensor.temperature": "numeric", # Numbers "sensor.last_seen": "datetime", # Timestamps "binary_sensor.door": "bool", # on/off states "sensor.count": "int", # Whole numbers "sensor.text": "str" # Text values }
-
Validate API key before starting monitor:
if not api_key or len(api_key) < 10: raise ValueError("Invalid API key")
Running the Sample Script
# Activate your virtual environment first
source venv/bin/activate # On macOS/Linux
# venv\Scripts\activate # On Windows
# Run the sample
python -m m_hass_api.hass_api_client
Testing
Running All Tests
# Activate your virtual environment
source venv/bin/activate # On macOS/Linux
# Install test dependencies
pip install -r requirements.txt
# Run all tests
pytest tests/
# Run with coverage
pytest tests/ --cov=m_hass_api --cov-report=html
# Open coverage report (macOS)
open htmlcov/index.html
Running Specific Test Files
pytest tests/test_hass_api_client.py
Running Specific Tests
# Run a specific test
pytest tests/test_sample.py::test_sample_client_initialization
# Run tests matching a pattern
pytest tests/ -k "test_get_data"
Viewing Test Output
# Verbose output
pytest tests/ -v
# Show print statements
pytest tests/ -v -s
Development
Project Setup
# Install development dependencies
pip install -r requirements.txt
# Install package in editable mode
pip install -e .
Code Quality
# Format code with black
black m_hass_api/ tests/
# Check code style with flake8
flake8 m_hass_api/ tests/
# Type checking with mypy (optional)
mypy m_hass_api/
CI/CD with GitHub Actions
This project uses GitHub Actions for automated testing and deployment to PyPI.
Automated Workflow
The .github/workflows/ci.yml workflow provides:
- Automated Testing: Runs tests on multiple OS (Ubuntu, macOS, Windows) and Python versions (3.8-3.12)
- Code Quality: Automatic linting (flake8) and type checking (mypy)
- Automated Building: Builds Python packages on every push
- Automated Publishing: Deploys to TestPyPI and PyPI based on triggers
Workflow Triggers
| Event | Trigger | Action |
|---|---|---|
Push to main |
✓ | Run tests + build package |
Push to develop |
✓ | Run tests + build + publish to TestPyPI |
| Pull Request | ✓ | Run tests + build |
| GitHub Release | ✓ | Run tests + build + publish to PyPI + create release |
Quick Start
- Set up PyPI account and enable 2FA
- Configure trusted publishing in PyPI settings
- Push to develop to test:
git push origin develop
- Create a release to publish:
gh release create v0.5.0 --generate-notes
For detailed setup instructions, see DEPLOYMENT.md.
Deployment
Building the Package
# Install build tools
pip install build twine
# Build the package
python -m build
# This will create:
# - dist/m_hass_api-0.1.0.tar.gz (source distribution)
# - dist/m_hass_api-0.1.0-py3-none-any.whl (wheel)
Testing the Package Locally
# Upload to TestPyPI
pip install twine
twine upload --repository testpypi dist/*
# Install from TestPyPI
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple m-hass-api
Publishing to PyPI
Prerequisites:
- Create an account at https://pypi.org/account/register/
- Enable 2-Factor Authentication
- Generate an API token at https://pypi.org/manage/account/token/
# Upload to PyPI
twine upload dist/*
# Your package will be available at:
# https://pypi.org/project/m-hass-api/
Deployment Checklist
Before deploying, ensure:
- Update version number in
setup.py - Update
README.mdwith latest changes - All tests pass:
pytest tests/ - Code is formatted:
black m_hass_api/ tests/ - No linting errors:
flake8 m_hass_api/ tests/ - Changelog is updated (if applicable)
- Documentation is complete
Version Management
Update the version in setup.py following semantic versioning:
- MAJOR: Incompatible API changes
- MINOR: Backwards-compatible functionality additions
- PATCH: Backwards-compatible bug fixes
Example: 0.1.0 → 0.1.1 (patch) → 0.2.0 (minor) → 1.0.0 (major)
GitHub Actions CI/CD (Optional)
You can automate testing and deployment with GitHub Actions. Create .github/workflows/ci.yml:
name: CI/CD
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -e .
- name: Run tests
run: pytest tests/ --cov=m_hass_api
deploy:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build dependencies
run: pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
Troubleshooting
Common Issues
Import Error:
# Make sure you've installed the package
pip install -e .
Tests Failing:
# Ensure all dependencies are installed
pip install -r requirements.txt
Build Failing:
# Clean and rebuild
rm -rf build/ dist/ *.egg-info
python -m build
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Ensure all tests pass (
pytest tests/) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - feel free to use this template for your own projects.
Support
For issues, questions, or contributions, please open an issue on GitHub.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file m_hass_api-0.5.2.tar.gz.
File metadata
- Download URL: m_hass_api-0.5.2.tar.gz
- Upload date:
- Size: 24.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d36bd7f79470b456250fd9505f9ef867d4130c7073d5298b4f779e690921a2e7
|
|
| MD5 |
b593a9032981596a3e7d4e0d4f50f88b
|
|
| BLAKE2b-256 |
bd25f7f6049541aee5adfc074604305247020f1a94dda6f09f46cb10c44c6b9d
|
Provenance
The following attestation bundles were made for m_hass_api-0.5.2.tar.gz:
Publisher:
ci.yml on maxim75/m_hass_api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
m_hass_api-0.5.2.tar.gz -
Subject digest:
d36bd7f79470b456250fd9505f9ef867d4130c7073d5298b4f779e690921a2e7 - Sigstore transparency entry: 952530707
- Sigstore integration time:
-
Permalink:
maxim75/m_hass_api@4ab25ec622a67a8dd00cf0e4d20636cb48f30b56 -
Branch / Tag:
refs/tags/v0.5.2a - Owner: https://github.com/maxim75
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@4ab25ec622a67a8dd00cf0e4d20636cb48f30b56 -
Trigger Event:
release
-
Statement type:
File details
Details for the file m_hass_api-0.5.2-py3-none-any.whl.
File metadata
- Download URL: m_hass_api-0.5.2-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66f06ad9599a8e93b10c8dc9c0d73e37b46123d5550c81698d0e327d7745abaa
|
|
| MD5 |
00434e94d0f546c0d709f4eb7a85c3ef
|
|
| BLAKE2b-256 |
79ee7a8961f6fc240a6784978dc5c7dd176b18b8ba740920f185621bd39e43b1
|
Provenance
The following attestation bundles were made for m_hass_api-0.5.2-py3-none-any.whl:
Publisher:
ci.yml on maxim75/m_hass_api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
m_hass_api-0.5.2-py3-none-any.whl -
Subject digest:
66f06ad9599a8e93b10c8dc9c0d73e37b46123d5550c81698d0e327d7745abaa - Sigstore transparency entry: 952530708
- Sigstore integration time:
-
Permalink:
maxim75/m_hass_api@4ab25ec622a67a8dd00cf0e4d20636cb48f30b56 -
Branch / Tag:
refs/tags/v0.5.2a - Owner: https://github.com/maxim75
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@4ab25ec622a67a8dd00cf0e4d20636cb48f30b56 -
Trigger Event:
release
-
Statement type: