Initial commit of Simple Home Assistant Addon with web interface

This commit is contained in:
Manu
2025-06-01 12:04:22 +02:00
commit 3d987c3eb6
12 changed files with 660 additions and 0 deletions

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM homeassistant/amd64-base:latest
# Install necessary packages
RUN apk add --no-cache \
python3 \
python3-dev \
build-base \
git
# Set working directory
WORKDIR /app
# Copy addon files
COPY . /app
# Install Python dependencies
RUN pip3 install --no-cache-dir \
homeassistant \
aiohttp \
aiohttp_cors
# Create web directory if it doesn't exist
RUN mkdir -p /app/web
# Create startup script
RUN echo "#!/bin/sh\npython3 /app/main.py" > /startup.sh && chmod +x /startup.sh
# Expose the web interface port
EXPOSE 8099
# Start the addon
CMD ["/startup.sh"]

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# Simple Home Assistant Addon
This is a simple Home Assistant addon that demonstrates the basic structure and functionality of a Home Assistant addon with a web interface.
## Features
- Basic addon structure
- Service registration
- Logging capabilities
- Python-based implementation
- Web interface for interaction
- API endpoints for status and messaging
## Installation
### Method 1: Local Development
1. Build the Docker image:
```bash
docker build -t simple-addon .
```
2. Push the image to your container registry
3. Add the addon to your Home Assistant instance through the Supervisor interface
### Method 2: Home Assistant Add-on Store
1. Add this repository URL to your Home Assistant add-on store:
- Navigate to Settings → Add-ons → Add-on Store
- Click the three dots in the top right corner
- Select "Repositories"
- Add the URL of this repository
2. Find the "Simple Addon" in the add-on store and click install
## Configuration
The addon can be configured through the Home Assistant Supervisor interface. Available options:
- `message`: Default message to display (default: "Hello from Home Assistant!")
## Usage
### Web Interface
The addon provides a web interface accessible through:
- Home Assistant sidebar (the addon adds an icon to the sidebar)
- Direct URL: `http://your-home-assistant:8099/addon/`
The web interface allows you to:
- Check the addon status
- Send messages to Home Assistant
- View activity logs
### Service Calls
You can also use the addon's services through Home Assistant's service calls:
```yaml
service: simple_addon.hello
data:
message: "Hello from Home Assistant!"
```
### API Endpoints
The addon exposes the following API endpoints:
- `GET /api/status` - Get the current status of the addon
- `POST /api/message` - Send a message to the addon
## Development
To develop this addon, you'll need:
- Docker
- Python 3.8+
- Home Assistant development environment
### Project Structure
```
/
├── config.yaml # Addon configuration
├── Dockerfile # Docker build instructions
├── main.py # Main Python code
├── README.md # Documentation
└── web/ # Web interface files
├── index.html # HTML interface
├── styles.css # CSS styling
└── script.js # JavaScript for interactivity
```
## License
MIT License

32
config.yaml Normal file
View File

@ -0,0 +1,32 @@
name: Simple Addon
description: A simple Home Assistant addon example with web interface
version: "1.0.0"
slug: "simple-addon"
arch:
- aarch64
- amd64
- armhf
- armv7
- i386
- ppc64le
- riscv64
- s390x
webui: "http://[HOST]:[PORT:8099]/addon/"
ports:
8099/tcp: 8099
port_map:
8099/tcp: 8099
ingress: true
ingress_port: 8099
panel_icon: mdi:application
panel_title: Simple Addon
init: false
hassio_api: true
homeassistant_api: true
host_network: false
map:
- config:rw
options:
message: "Hello from Home Assistant!"
schema:
message: "str"

View File

@ -0,0 +1,21 @@
FROM homeassistant/amd64-base:latest
# Install basic dependencies
RUN apt-get update && \
apt-get install -y \
python3-pip \
python3-dev \
build-essential \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Copy addon files
COPY requirements.txt /
COPY run.sh /
COPY config.yaml /
# Set permissions
RUN chmod +x /run.sh
# Start the addon
CMD ["/run.sh"]

View File

@ -0,0 +1,16 @@
# Simple Home Assistant Addon
A basic Home Assistant addon template.
## Installation
1. Download this addon from the Home Assistant Addon Store
2. Install it through the Home Assistant Supervisor interface
## Configuration
No configuration is required for this basic addon.
## Usage
This addon provides a simple example of how to create a Home Assistant addon.

View File

@ -0,0 +1,42 @@
import asyncio
import logging
import sys
import time
from datetime import datetime
import aiohttp
import yaml
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
_LOGGER = logging.getLogger(__name__)
async def main():
_LOGGER.info("Starting Simple Addon")
# Example of Home Assistant API usage
async with aiohttp.ClientSession() as session:
try:
# Get Home Assistant info
async with session.get('http://supervisor/core/info') as response:
if response.status == 200:
data = await response.json()
_LOGGER.info(f"Home Assistant version: {data.get('version')}")
except Exception as e:
_LOGGER.error(f"Error connecting to Home Assistant: {str(e)}")
# Keep the addon running
while True:
await asyncio.sleep(60)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
_LOGGER.info("Addon stopped")

View File

@ -0,0 +1,4 @@
requests==2.31.0
aiohttp==3.8.5
asyncio==3.4.3
pyyaml==6.0.1

View File

@ -0,0 +1,8 @@
#!/bin/bash
# Set environment variables
export PYTHONPATH=/usr/local/lib/python3.9/site-packages
# Start the addon
python3 -m pip install -r /requirements.txt
python3 /app/main.py

111
main.py Normal file
View File

@ -0,0 +1,111 @@
import logging
import asyncio
import os
import json
from aiohttp import web
import aiohttp_cors
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__)
# Define the web server port
WEB_PORT = 8099
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the simple addon component."""
# Log that the addon is starting
_LOGGER.info("Simple Addon is starting")
# Get the directory of this file
current_dir = os.path.dirname(os.path.realpath(__file__))
web_dir = os.path.join(current_dir, 'web')
# Create a simple service
async def handle_hello(call):
"""Handle the service call."""
message = call.data.get('message', 'No message provided')
_LOGGER.info(f"Hello service called with message: {message}")
# Store the message in the state
hass.states.async_set('simple_addon.last_message', message)
return {"success": True, "message": message}
# Register our service
hass.services.async_register(
'simple_addon',
'hello',
handle_hello
)
# Set up the web server
app = web.Application()
# Configure CORS
cors = aiohttp_cors.setup(app, defaults={
"*": aiohttp_cors.ResourceOptions(
allow_credentials=True,
expose_headers="*",
allow_headers="*",
)
})
# API endpoint for sending messages
class MessageView(HomeAssistantView):
url = "/api/message"
name = "api:message"
async def post(self, request):
data = await request.json()
message = data.get('message', '')
# Call the hello service
await hass.services.async_call(
'simple_addon', 'hello',
{"message": message}
)
return web.json_response({"success": True})
# API endpoint for getting status
class StatusView(HomeAssistantView):
url = "/api/status"
name = "api:status"
async def get(self, request):
return web.json_response({
"status": "online",
"last_message": hass.states.get('simple_addon.last_message').state
if hass.states.get('simple_addon.last_message') else None
})
# Register the API endpoints
hass.http.register_view(MessageView)
hass.http.register_view(StatusView)
# Serve static files
app.router.add_static('/addon/', web_dir)
# Start the web server
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, '0.0.0.0', WEB_PORT)
await site.start()
_LOGGER.info(f"Web interface started on port {WEB_PORT}")
# Set the webui URL in the addon configuration
hass.states.async_set('simple_addon.webui', f"http://localhost:{WEB_PORT}/addon/")
# Return boolean to indicate that initialization was successfully
return True
async def async_setup_entry(hass: HomeAssistant, entry):
"""Set up the addon from a config entry."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True

49
web/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Home Assistant Addon</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>Simple Home Assistant Addon</h1>
<p>A simple interface for controlling your Home Assistant addon</p>
</header>
<main>
<div class="card">
<h2>Addon Status</h2>
<div class="status-indicator">
<span id="status-dot" class="status-dot"></span>
<span id="status-text">Checking...</span>
</div>
</div>
<div class="card">
<h2>Send Message</h2>
<div class="form-group">
<label for="message">Message:</label>
<input type="text" id="message" placeholder="Enter your message">
</div>
<button id="send-btn" class="btn">Send</button>
</div>
<div class="card">
<h2>Recent Activity</h2>
<div id="activity-log" class="activity-log">
<p>No recent activity</p>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Simple Home Assistant Addon</p>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>

114
web/script.js Normal file
View File

@ -0,0 +1,114 @@
document.addEventListener('DOMContentLoaded', function() {
// Elements
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const messageInput = document.getElementById('message');
const sendBtn = document.getElementById('send-btn');
const activityLog = document.getElementById('activity-log');
// API endpoints
const API_BASE = window.location.origin;
const STATUS_API = `${API_BASE}/api/status`;
const MESSAGE_API = `${API_BASE}/api/message`;
// Set initial status and check periodically
checkStatus();
setInterval(checkStatus, 30000); // Check every 30 seconds
// Event listeners
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// Check addon status
function checkStatus() {
logActivity('Checking addon status...');
fetch(STATUS_API)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
setStatus(data.status === 'online');
if (data.last_message) {
logActivity(`Last message: "${data.last_message}"`);
}
})
.catch(error => {
console.error('Error checking status:', error);
setStatus(false);
logActivity(`Error checking status: ${error.message}`);
});
}
// Set status indicator
function setStatus(isOnline) {
if (isOnline) {
statusDot.classList.add('online');
statusDot.classList.remove('offline');
statusText.textContent = 'Online';
} else {
statusDot.classList.add('offline');
statusDot.classList.remove('online');
statusText.textContent = 'Offline';
}
}
// Send message to Home Assistant
function sendMessage() {
const message = messageInput.value.trim();
if (!message) {
alert('Please enter a message');
return;
}
logActivity(`Sending message: "${message}"`);
fetch(MESSAGE_API, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message }),
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
logActivity(`Message sent successfully: "${message}"`);
messageInput.value = '';
} else {
logActivity(`Failed to send message: ${data.error || 'Unknown error'}`);
}
})
.catch(error => {
console.error('Error sending message:', error);
logActivity(`Error sending message: ${error.message}`);
});
}
// Log activity
function logActivity(text) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('p');
logEntry.textContent = `[${timestamp}] ${text}`;
// Check if "No recent activity" message exists
if (activityLog.firstChild && activityLog.firstChild.textContent === 'No recent activity') {
activityLog.innerHTML = '';
}
activityLog.insertBefore(logEntry, activityLog.firstChild);
}
});

134
web/styles.css Normal file
View File

@ -0,0 +1,134 @@
:root {
--primary-color: #03a9f4;
--secondary-color: #2196f3;
--background-color: #f5f5f5;
--card-color: #ffffff;
--text-color: #333333;
--border-radius: 8px;
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
}
header h1 {
color: var(--primary-color);
margin-bottom: 10px;
}
.card {
background-color: var(--card-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 20px;
margin-bottom: 20px;
}
.card h2 {
color: var(--secondary-color);
margin-bottom: 15px;
font-size: 1.5rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: #ccc;
}
.status-dot.online {
background-color: #4CAF50;
}
.status-dot.offline {
background-color: #F44336;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: var(--secondary-color);
}
.activity-log {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
background-color: #f9f9f9;
}
.activity-log p {
margin-bottom: 5px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.activity-log p:last-child {
margin-bottom: 0;
border-bottom: none;
}
footer {
text-align: center;
margin-top: 30px;
color: #777;
font-size: 14px;
}