From 3d987c3eb6f2e2920923cf9952b073a82fe94042 Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 1 Jun 2025 12:04:22 +0200 Subject: [PATCH] Initial commit of Simple Home Assistant Addon with web interface --- Dockerfile | 32 +++++++ README.md | 97 +++++++++++++++++++ config.yaml | 32 +++++++ homeassistant-addon/Dockerfile | 21 +++++ homeassistant-addon/README.md | 16 ++++ homeassistant-addon/main.py | 42 +++++++++ homeassistant-addon/requirements.txt | 4 + homeassistant-addon/run.sh | 8 ++ main.py | 111 ++++++++++++++++++++++ web/index.html | 49 ++++++++++ web/script.js | 114 +++++++++++++++++++++++ web/styles.css | 134 +++++++++++++++++++++++++++ 12 files changed, 660 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config.yaml create mode 100644 homeassistant-addon/Dockerfile create mode 100644 homeassistant-addon/README.md create mode 100644 homeassistant-addon/main.py create mode 100644 homeassistant-addon/requirements.txt create mode 100644 homeassistant-addon/run.sh create mode 100644 main.py create mode 100644 web/index.html create mode 100644 web/script.js create mode 100644 web/styles.css diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..138b3fc --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ea7665 --- /dev/null +++ b/README.md @@ -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 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..fff3f2f --- /dev/null +++ b/config.yaml @@ -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" diff --git a/homeassistant-addon/Dockerfile b/homeassistant-addon/Dockerfile new file mode 100644 index 0000000..b3e0447 --- /dev/null +++ b/homeassistant-addon/Dockerfile @@ -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"] diff --git a/homeassistant-addon/README.md b/homeassistant-addon/README.md new file mode 100644 index 0000000..c92ae18 --- /dev/null +++ b/homeassistant-addon/README.md @@ -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. diff --git a/homeassistant-addon/main.py b/homeassistant-addon/main.py new file mode 100644 index 0000000..8d58f71 --- /dev/null +++ b/homeassistant-addon/main.py @@ -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") diff --git a/homeassistant-addon/requirements.txt b/homeassistant-addon/requirements.txt new file mode 100644 index 0000000..7aee0e0 --- /dev/null +++ b/homeassistant-addon/requirements.txt @@ -0,0 +1,4 @@ +requests==2.31.0 +aiohttp==3.8.5 +asyncio==3.4.3 +pyyaml==6.0.1 diff --git a/homeassistant-addon/run.sh b/homeassistant-addon/run.sh new file mode 100644 index 0000000..0b647be --- /dev/null +++ b/homeassistant-addon/run.sh @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..fd5697c --- /dev/null +++ b/main.py @@ -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 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f8c664d --- /dev/null +++ b/web/index.html @@ -0,0 +1,49 @@ + + + + + + Simple Home Assistant Addon + + + +
+
+

Simple Home Assistant Addon

+

A simple interface for controlling your Home Assistant addon

+
+ +
+
+

Addon Status

+
+ + Checking... +
+
+ +
+

Send Message

+
+ + +
+ +
+ +
+

Recent Activity

+
+

No recent activity

+
+
+
+ +
+

© 2025 Simple Home Assistant Addon

+
+
+ + + + diff --git a/web/script.js b/web/script.js new file mode 100644 index 0000000..7adfba5 --- /dev/null +++ b/web/script.js @@ -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); + } +}); diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..62310c2 --- /dev/null +++ b/web/styles.css @@ -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; +}