Compare commits
18 Commits
0.1.2
...
separation
| Author | SHA1 | Date | |
|---|---|---|---|
|
5399f044a1
|
|||
|
16fa5143dd
|
|||
|
cff154c247
|
|||
|
038664ec94
|
|||
|
2bbf825cf7
|
|||
|
5e0159047c
|
|||
|
b23b624a86
|
|||
|
9c099e44af
|
|||
|
9c17a73605
|
|||
|
a389edcd87
|
|||
|
17c9bca8d1
|
|||
|
c4fc21d760
|
|||
|
e902d221ea
|
|||
|
e19bffc90c
|
|||
|
5a13183123
|
|||
|
deb26c4945
|
|||
|
c0e3ac1fe0
|
|||
|
370c16eb42
|
@@ -1,9 +1,13 @@
|
|||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
APP:
|
APP:
|
||||||
- ui
|
- ui
|
||||||
- api
|
- api
|
||||||
- abstraction
|
- abstraction
|
||||||
- rules
|
- rules
|
||||||
|
- static
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
build-${APP}:
|
build-${APP}:
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
- predeploy
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
APP:
|
APP:
|
||||||
- ui
|
- ui
|
||||||
- api
|
- api
|
||||||
- abstraction
|
- abstraction
|
||||||
- rules
|
- rules
|
||||||
|
- static
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
deploy-${APP}:
|
deploy-${APP}:
|
||||||
@@ -24,6 +32,3 @@ steps:
|
|||||||
exclude:
|
exclude:
|
||||||
- refs/tags/*-configchange
|
- refs/tags/*-configchange
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- build
|
|
||||||
- predeploy
|
|
||||||
|
|||||||
23
.woodpecker/ingress.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
steps:
|
||||||
|
apply_ingress:
|
||||||
|
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
|
||||||
|
environment:
|
||||||
|
KUBE_CONFIG_CONTENT:
|
||||||
|
from_secret: kube_config
|
||||||
|
NAMESPACE: "homea2"
|
||||||
|
commands:
|
||||||
|
- printf "$KUBE_CONFIG_CONTENT" > /tmp/kubeconfig
|
||||||
|
- export KUBECONFIG=/tmp/kubeconfig
|
||||||
|
- kubectl apply -f deployment/ingress.yaml -n $NAMESPACE
|
||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
ref:
|
||||||
|
exclude:
|
||||||
|
- refs/tags/*-configchange
|
||||||
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
create_namespace:
|
create_namespace:
|
||||||
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
|
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
MQTT_PORT=1883 \
|
MQTT_PORT=1883 \
|
||||||
REDIS_HOST=localhost \
|
REDIS_HOST=localhost \
|
||||||
REDIS_PORT=6379 \
|
REDIS_PORT=6379 \
|
||||||
REDIS_DB=0
|
REDIS_DB=0 \
|
||||||
|
REDIS_CHANNEL=ui:updates
|
||||||
|
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN addgroup -g 10001 -S app && \
|
RUN addgroup -g 10001 -S app && \
|
||||||
|
|||||||
30
apps/homekit/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Environment defaults (can be overridden at runtime)
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
HOMEKIT_NAME="Home Automation Bridge" \
|
||||||
|
HOMEKIT_PIN="031-45-154" \
|
||||||
|
HOMEKIT_PORT="51826" \
|
||||||
|
API_BASE="http://api:8001" \
|
||||||
|
HOMEKIT_API_TOKEN="" \
|
||||||
|
HOMEKIT_PERSIST_FILE="/data/homekit.state"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only requirements first for better build caching
|
||||||
|
COPY apps/homekit/requirements.txt ./apps/homekit/requirements.txt
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& pip install --no-cache-dir -r apps/homekit/requirements.txt
|
||||||
|
|
||||||
|
# Copy full source tree
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Expose HomeKit TCP port (mDNS uses UDP 5353 via host network)
|
||||||
|
EXPOSE 51826/tcp
|
||||||
|
|
||||||
|
# Volume for persistent HomeKit state (pairings etc.)
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# Start the HomeKit bridge
|
||||||
|
CMD ["python", "-m", "apps.homekit.main"]
|
||||||
28
apps/homekit/docker-compose.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
homekit-bridge:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: apps/homekit/Dockerfile
|
||||||
|
container_name: homekit-bridge
|
||||||
|
|
||||||
|
# Required for mDNS/Bonjour to work properly
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- HOMEKIT_NAME=Hottis Home Automation Bridge
|
||||||
|
- HOMEKIT_PIN=031-45-154
|
||||||
|
- HOMEKIT_PORT=51826
|
||||||
|
|
||||||
|
- API_BASE=http://homea2-api-internal.hottis.de
|
||||||
|
- HOMEKIT_API_TOKEN=
|
||||||
|
|
||||||
|
- HOMEKIT_PERSIST_FILE=/data/homekit.state
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- homekit_data:/data
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
homekit_data:
|
||||||
|
driver: local
|
||||||
@@ -6,7 +6,7 @@ FROM python:3.14-alpine
|
|||||||
# Prevent Python from writing .pyc files and enable unbuffered output
|
# Prevent Python from writing .pyc files and enable unbuffered output
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
RULES_CONFIG=config/rules.yaml \
|
RULES_CONFIG=/app/config/rules.yaml \
|
||||||
MQTT_BROKER=172.16.2.16 \
|
MQTT_BROKER=172.16.2.16 \
|
||||||
MQTT_PORT=1883 \
|
MQTT_PORT=1883 \
|
||||||
REDIS_HOST=localhost \
|
REDIS_HOST=localhost \
|
||||||
|
|||||||
15
apps/static/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Static assets Dockerfile (minimal webserver for /static only)
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Remove default nginx content
|
||||||
|
RUN rm -rf ./*
|
||||||
|
|
||||||
|
# Copy only static assets from the UI project
|
||||||
|
COPY apps/static/static/ ./
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Use default nginx config; caller can mount custom config if needed
|
||||||
BIN
apps/static/static/apple-touch-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
apps/static/static/apple-touch-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 639 B |
BIN
apps/static/static/apple-touch-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 827 B |
BIN
apps/static/static/apple-touch-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
apps/static/static/apple-touch-icon-16x16.png
Normal file
|
After Width: | Height: | Size: 153 B |
BIN
apps/static/static/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 1018 B |
BIN
apps/static/static/apple-touch-icon-32x32.png
Normal file
|
After Width: | Height: | Size: 210 B |
BIN
apps/static/static/apple-touch-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 336 B |
BIN
apps/static/static/apple-touch-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 346 B |
BIN
apps/static/static/apple-touch-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 413 B |
BIN
apps/static/static/apple-touch-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
apps/static/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 1018 B |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
BIN
apps/static/static/garage-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 519 B |
BIN
apps/static/static/garage-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 547 B |
BIN
apps/static/static/garage-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 641 B |
BIN
apps/static/static/garage-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
apps/static/static/garage-icon-16x16.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
apps/static/static/garage-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 808 B |
BIN
apps/static/static/garage-icon-32x32.png
Normal file
|
After Width: | Height: | Size: 192 B |
BIN
apps/static/static/garage-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
apps/static/static/garage-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 271 B |
BIN
apps/static/static/garage-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
apps/static/static/garage-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
apps/static/static/garage-icon.png
Normal file
|
After Width: | Height: | Size: 808 B |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
1
apps/static/static/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
empty
|
||||||
@@ -1,49 +1,41 @@
|
|||||||
# UI Service Dockerfile
|
# UI Service Dockerfile (Application only, without static files)
|
||||||
# FastAPI + Jinja2 + HTMX Dashboard
|
|
||||||
|
|
||||||
FROM python:3.14-alpine
|
FROM python:3.14-alpine
|
||||||
|
|
||||||
# Prevent Python from writing .pyc files and enable unbuffered output
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
UI_PORT=8002 \
|
UI_PORT=8002 \
|
||||||
API_BASE=http://api:8001 \
|
API_BASE=http://api:8001 \
|
||||||
BASE_PATH=""
|
BASE_PATH="" \
|
||||||
|
STATIC_BASE=http://static:8080
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN addgroup -g 10001 -S app && \
|
RUN addgroup -g 10001 -S app && \
|
||||||
adduser -u 10001 -S app -G app
|
adduser -u 10001 -S app -G app
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
curl \
|
curl \
|
||||||
gcc \
|
gcc \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
linux-headers
|
linux-headers
|
||||||
|
|
||||||
# Install Python dependencies
|
|
||||||
COPY apps/ui/requirements.txt /app/requirements.txt
|
COPY apps/ui/requirements.txt /app/requirements.txt
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy application code
|
# Copy only Python code and templates, but exclude static assets
|
||||||
COPY apps/__init__.py /app/apps/__init__.py
|
COPY apps/__init__.py /app/apps/__init__.py
|
||||||
COPY apps/ui/ /app/apps/ui/
|
COPY apps/ui/__init__.py /app/apps/ui/__init__.py
|
||||||
|
COPY apps/ui/main.py /app/apps/ui/main.py
|
||||||
|
COPY apps/ui/api_client.py /app/apps/ui/api_client.py
|
||||||
|
COPY apps/ui/templates/ /app/apps/ui/templates/
|
||||||
|
|
||||||
# Change ownership to app user
|
|
||||||
RUN chown -R app:app /app
|
RUN chown -R app:app /app
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost:${UI_PORT}/health || exit 1
|
CMD curl -f http://localhost:${UI_PORT}/health || exit 1
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8002
|
EXPOSE 8002
|
||||||
|
|
||||||
# Run application
|
|
||||||
CMD ["python", "-m", "uvicorn", "apps.ui.main:app", "--host", "0.0.0.0", "--port", "8002"]
|
CMD ["python", "-m", "uvicorn", "apps.ui.main:app", "--host", "0.0.0.0", "--port", "8002"]
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ logger = logging.getLogger(__name__)
|
|||||||
# Read configuration from environment variables
|
# Read configuration from environment variables
|
||||||
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
|
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
|
||||||
BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy
|
BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy
|
||||||
|
STATIC_BASE = os.getenv("STATIC_BASE", "/static")
|
||||||
|
|
||||||
print(f"UI using API_BASE: {API_BASE}")
|
print(f"UI using API_BASE: {API_BASE}")
|
||||||
print(f"UI using BASE_PATH: {BASE_PATH}")
|
print(f"UI using BASE_PATH: {BASE_PATH}")
|
||||||
|
print(f"UI using STATIC_BASE: {STATIC_BASE}")
|
||||||
|
|
||||||
def api_url(path: str) -> str:
|
def api_url(path: str) -> str:
|
||||||
"""Helper function to construct API URLs.
|
"""Helper function to construct API URLs.
|
||||||
@@ -43,6 +45,9 @@ app = FastAPI(
|
|||||||
templates_dir = Path(__file__).parent / "templates"
|
templates_dir = Path(__file__).parent / "templates"
|
||||||
templates = Jinja2Templates(directory=str(templates_dir))
|
templates = Jinja2Templates(directory=str(templates_dir))
|
||||||
|
|
||||||
|
# Make STATIC_BASE available in all templates
|
||||||
|
templates.env.globals["STATIC_BASE"] = STATIC_BASE
|
||||||
|
|
||||||
# Setup static files
|
# Setup static files
|
||||||
static_dir = Path(__file__).parent / "static"
|
static_dir = Path(__file__).parent / "static"
|
||||||
static_dir.mkdir(exist_ok=True)
|
static_dir.mkdir(exist_ok=True)
|
||||||
@@ -98,7 +103,8 @@ async def health() -> JSONResponse:
|
|||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "ui",
|
"service": "ui",
|
||||||
"api_base": API_BASE,
|
"api_base": API_BASE,
|
||||||
"base_path": BASE_PATH
|
"base_path": BASE_PATH,
|
||||||
|
"static_base": STATIC_BASE,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ async def rooms(request: Request) -> HTMLResponse:
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse("rooms.html", {
|
return templates.TemplateResponse("rooms.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"api_base": API_BASE
|
"api_base": API_BASE,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +151,7 @@ async def room_detail(request: Request, room_name: str) -> HTMLResponse:
|
|||||||
return templates.TemplateResponse("room.html", {
|
return templates.TemplateResponse("room.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"api_base": API_BASE,
|
"api_base": API_BASE,
|
||||||
"room_name": room_name
|
"room_name": room_name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
# Home Automation API Client
|
|
||||||
|
|
||||||
Wiederverwendbare JavaScript-API-Client-Bibliothek für das Home Automation UI.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Füge die folgenden Script-Tags in deine HTML-Seiten ein:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="/static/types.js"></script>
|
|
||||||
<script src="/static/api-client.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
Der API-Client nutzt `window.API_BASE`, das vom Backend gesetzt wird:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.API_BASE = '{{ api_base }}'; // Jinja2 template
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verwendung
|
|
||||||
|
|
||||||
### Globale Instanz
|
|
||||||
|
|
||||||
Der API-Client erstellt automatisch eine globale Instanz `window.apiClient`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Layout abrufen
|
|
||||||
const layout = await window.apiClient.getLayout();
|
|
||||||
|
|
||||||
// Geräte abrufen
|
|
||||||
const devices = await window.apiClient.getDevices();
|
|
||||||
|
|
||||||
// Gerätestatus abrufen
|
|
||||||
const state = await window.apiClient.getDeviceState('kitchen_light');
|
|
||||||
|
|
||||||
// Gerätesteuerung
|
|
||||||
await window.apiClient.setDeviceState('kitchen_light', 'light', {
|
|
||||||
power: true,
|
|
||||||
brightness: 80
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verfügbare Methoden
|
|
||||||
|
|
||||||
#### `getLayout(): Promise<Layout>`
|
|
||||||
Lädt die Layout-Daten (Räume und ihre Geräte).
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const layout = await window.apiClient.getLayout();
|
|
||||||
// { rooms: [{name: "Küche", devices: ["kitchen_light", ...]}, ...] }
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getDevices(): Promise<Device[]>`
|
|
||||||
Lädt alle Geräte mit ihren Features.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const devices = await window.apiClient.getDevices();
|
|
||||||
// [{device_id: "...", name: "...", type: "light", features: {...}}, ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getDeviceState(deviceId): Promise<DeviceState>`
|
|
||||||
Lädt den aktuellen Status eines Geräts.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const state = await window.apiClient.getDeviceState('kitchen_light');
|
|
||||||
// {power: true, brightness: 80, ...}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getAllStates(): Promise<Object>`
|
|
||||||
Lädt alle Gerätestatus auf einmal.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const states = await window.apiClient.getAllStates();
|
|
||||||
// {"kitchen_light": {power: true, ...}, "thermostat_1": {...}, ...}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `setDeviceState(deviceId, type, payload): Promise<void>`
|
|
||||||
Sendet einen Befehl an ein Gerät.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Licht einschalten
|
|
||||||
await window.apiClient.setDeviceState('kitchen_light', 'light', {
|
|
||||||
power: true,
|
|
||||||
brightness: 80
|
|
||||||
});
|
|
||||||
|
|
||||||
// Thermostat einstellen
|
|
||||||
await window.apiClient.setDeviceState('thermostat_1', 'thermostat', {
|
|
||||||
target_temp: 22.5
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rollladen steuern
|
|
||||||
await window.apiClient.setDeviceState('cover_1', 'cover', {
|
|
||||||
position: 50
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getDeviceRoom(deviceId): Promise<{room: string}>`
|
|
||||||
Ermittelt den Raum eines Geräts.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { room } = await window.apiClient.getDeviceRoom('kitchen_light');
|
|
||||||
// {room: "Küche"}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getScenes(): Promise<Scene[]>`
|
|
||||||
Lädt alle verfügbaren Szenen.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const scenes = await window.apiClient.getScenes();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `activateScene(sceneId): Promise<void>`
|
|
||||||
Aktiviert eine Szene.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
await window.apiClient.activateScene('evening');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Realtime-Updates (SSE)
|
|
||||||
|
|
||||||
#### `connectRealtime(onEvent, onError): EventSource`
|
|
||||||
Verbindet sich mit dem SSE-Stream für Live-Updates.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.apiClient.connectRealtime(
|
|
||||||
(event) => {
|
|
||||||
console.log('Update:', event.device_id, event.state);
|
|
||||||
// event = {device_id: "...", type: "state", state: {...}}
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.error('Connection error:', error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `onDeviceUpdate(deviceId, callback): Function`
|
|
||||||
Registriert einen Listener für spezifische Geräte-Updates.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Für ein bestimmtes Gerät
|
|
||||||
const unsubscribe = window.apiClient.onDeviceUpdate('kitchen_light', (event) => {
|
|
||||||
console.log('Kitchen light changed:', event.state);
|
|
||||||
updateUI(event.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Für alle Geräte
|
|
||||||
const unsubscribeAll = window.apiClient.onDeviceUpdate(null, (event) => {
|
|
||||||
console.log('Any device changed:', event.device_id, event.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Später: Listener entfernen
|
|
||||||
unsubscribe();
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `disconnectRealtime(): void`
|
|
||||||
Trennt die SSE-Verbindung und entfernt alle Listener.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.apiClient.disconnectRealtime();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Helper-Methoden
|
|
||||||
|
|
||||||
#### `findDevice(devices, deviceId): Device|null`
|
|
||||||
Findet ein Gerät in einem Array.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const devices = await window.apiClient.getDevices();
|
|
||||||
const device = window.apiClient.findDevice(devices, 'kitchen_light');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `findRoom(layout, roomName): Room|null`
|
|
||||||
Findet einen Raum im Layout.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const layout = await window.apiClient.getLayout();
|
|
||||||
const room = window.apiClient.findRoom(layout, 'Küche');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `getDevicesForRoom(layout, devices, roomName): Device[]`
|
|
||||||
Gibt alle Geräte eines Raums zurück.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const layout = await window.apiClient.getLayout();
|
|
||||||
const devices = await window.apiClient.getDevices();
|
|
||||||
const kitchenDevices = window.apiClient.getDevicesForRoom(layout, devices, 'Küche');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `api(path): string`
|
|
||||||
Konstruiert eine vollständige API-URL.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const url = window.apiClient.api('/devices');
|
|
||||||
// "http://172.19.1.11:8001/devices"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backward Compatibility
|
|
||||||
|
|
||||||
Die globale `api()` Funktion ist weiterhin verfügbar:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function api(url) {
|
|
||||||
return window.apiClient.api(url);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Typen (JSDoc)
|
|
||||||
|
|
||||||
Die Datei `types.js` enthält JSDoc-Definitionen für alle API-Typen:
|
|
||||||
|
|
||||||
- `Room` - Raum mit Geräten
|
|
||||||
- `Layout` - Layout-Struktur
|
|
||||||
- `Device` - Gerätedaten
|
|
||||||
- `DeviceFeatures` - Geräte-Features
|
|
||||||
- `DeviceState` - Gerätestatus (Light, Thermostat, Contact, etc.)
|
|
||||||
- `RealtimeEvent` - SSE-Event-Format
|
|
||||||
- `Scene` - Szenen-Definition
|
|
||||||
- `*Payload` - Command-Payloads für verschiedene Gerätetypen
|
|
||||||
|
|
||||||
Diese ermöglichen IDE-Autocomplete und Type-Checking in modernen Editoren (VS Code, WebStorm).
|
|
||||||
|
|
||||||
## Beispiel: Vollständige Seite
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>My Page</title>
|
|
||||||
<script src="/static/types.js"></script>
|
|
||||||
<script src="/static/api-client.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="status"></div>
|
|
||||||
<button id="toggle">Toggle Light</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.API_BASE = 'http://172.19.1.11:8001';
|
|
||||||
const deviceId = 'kitchen_light';
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
// Load initial state
|
|
||||||
const state = await window.apiClient.getDeviceState(deviceId);
|
|
||||||
updateUI(state);
|
|
||||||
|
|
||||||
// Listen for updates
|
|
||||||
window.apiClient.onDeviceUpdate(deviceId, (event) => {
|
|
||||||
updateUI(event.state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to realtime
|
|
||||||
window.apiClient.connectRealtime((event) => {
|
|
||||||
console.log('Event:', event);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle button clicks
|
|
||||||
document.getElementById('toggle').onclick = async () => {
|
|
||||||
const currentState = await window.apiClient.getDeviceState(deviceId);
|
|
||||||
await window.apiClient.setDeviceState(deviceId, 'light', {
|
|
||||||
power: !currentState.power
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI(state) {
|
|
||||||
document.getElementById('status').textContent =
|
|
||||||
state.power ? 'ON' : 'OFF';
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
Alle API-Methoden werfen Exceptions bei Fehlern:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
try {
|
|
||||||
const state = await window.apiClient.getDeviceState('invalid_id');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API error:', error);
|
|
||||||
showErrorMessage(error.message);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-Reconnect
|
|
||||||
|
|
||||||
Der SSE-Client versucht automatisch, nach 5 Sekunden wieder zu verbinden, wenn die Verbindung abbricht.
|
|
||||||
|
|
||||||
## Verwendete Technologien
|
|
||||||
|
|
||||||
- **Fetch API** - Für HTTP-Requests
|
|
||||||
- **EventSource** - Für Server-Sent Events
|
|
||||||
- **JSDoc** - Für Type Definitions
|
|
||||||
- **ES6+** - Modern JavaScript (Class, async/await, etc.)
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 449 B |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 933 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Home Automation",
|
|
||||||
"short_name": "Home",
|
|
||||||
"description": "Smart Home Automation System",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#667eea",
|
|
||||||
"theme_color": "#667eea",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-180x180.png",
|
|
||||||
"sizes": "180x180",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-120x120.png",
|
|
||||||
"sizes": "120x120",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-76x76.png",
|
|
||||||
"sizes": "76x76",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-32x32.png",
|
|
||||||
"sizes": "32x32",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/static/apple-touch-icon-16x16.png",
|
|
||||||
"sizes": "16x16",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,16 @@
|
|||||||
<title>Home Automation</title>
|
<title>Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="{{ STATIC_BASE }}/favicon.svg">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Dashboard">
|
<meta name="apple-mobile-web-app-title" content="Dashboard">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -6,16 +6,15 @@
|
|||||||
<title>Gerät - Home Automation</title>
|
<title>Gerät - Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Gerät">
|
<meta name="apple-mobile-web-app-title" content="Gerät">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -346,8 +345,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Load API client AFTER API_BASE is set -->
|
<!-- Load API client AFTER API_BASE is set -->
|
||||||
<script src="/static/types.js"></script>
|
<script src="{{ STATIC_BASE }}/types.js"></script>
|
||||||
<script src="/static/api-client.js"></script>
|
<script src="{{ STATIC_BASE }}/api-client.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Get device ID from URL
|
// Get device ID from URL
|
||||||
|
|||||||
@@ -6,17 +6,16 @@
|
|||||||
<title>Garage - Home Automation</title>
|
<title>Garage - Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/garage-icon-180x180.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/garage-icon-180x180.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/garage-icon-152x152.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/garage-icon-152x152.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/garage-icon-120x120.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/garage-icon-120x120.png">
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/garage-icon-76x76.png">
|
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/garage-icon-76x76.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/garage-icon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/garage-icon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/garage-icon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/garage-icon-16x16.png">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Garage">
|
<meta name="apple-mobile-web-app-title" content="Garage">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -306,8 +305,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Load API client AFTER API_BASE is set -->
|
<!-- Load API client AFTER API_BASE is set -->
|
||||||
<script src="/static/types.js"></script>
|
<script src="{{ STATIC_BASE }}/types.js"></script>
|
||||||
<script src="/static/api-client.js"></script>
|
<script src="{{ STATIC_BASE }}/api-client.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Device IDs for garage devices
|
// Device IDs for garage devices
|
||||||
|
|||||||
@@ -6,17 +6,16 @@
|
|||||||
<title>Home Automation</title>
|
<title>Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon-180x180.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon-180x180.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon-152x152.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon-152x152.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/apple-touch-icon-120x120.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon-120x120.png">
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/apple-touch-icon-76x76.png">
|
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/apple-touch-icon-76x76.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/apple-touch-icon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/apple-touch-icon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon-16x16.png">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Home Automation">
|
<meta name="apple-mobile-web-app-title" content="Home Automation">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -6,16 +6,15 @@
|
|||||||
<title>{{ room_name }} - Home Automation</title>
|
<title>{{ room_name }} - Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/apple-touch-icon.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon.png">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="{{ room_name }}">
|
<meta name="apple-mobile-web-app-title" content="{{ room_name }}">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -229,8 +228,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Load API client AFTER API_BASE is set -->
|
<!-- Load API client AFTER API_BASE is set -->
|
||||||
<script src="/static/types.js"></script>
|
<script src="{{ STATIC_BASE }}/types.js"></script>
|
||||||
<script src="/static/api-client.js"></script>
|
<script src="{{ STATIC_BASE }}/api-client.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Get room name from URL
|
// Get room name from URL
|
||||||
|
|||||||
@@ -6,17 +6,16 @@
|
|||||||
<title>Räume - Home Automation</title>
|
<title>Räume - Home Automation</title>
|
||||||
|
|
||||||
<!-- Apple Touch Icon -->
|
<!-- Apple Touch Icon -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon-180x180.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/apple-touch-icon-180x180.png">
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon-152x152.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/apple-touch-icon-152x152.png">
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/static/apple-touch-icon-120x120.png">
|
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/apple-touch-icon-120x120.png">
|
||||||
<link rel="apple-touch-icon" sizes="76x76" href="/static/apple-touch-icon-76x76.png">
|
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_BASE }}/apple-touch-icon-76x76.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/apple-touch-icon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_BASE }}/apple-touch-icon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/apple-touch-icon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_BASE }}/apple-touch-icon-16x16.png">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Räume">
|
<meta name="apple-mobile-web-app-title" content="Räume">
|
||||||
<meta name="theme-color" content="#667eea">
|
<meta name="theme-color" content="#667eea">
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -163,8 +162,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Load API client AFTER API_BASE is set -->
|
<!-- Load API client AFTER API_BASE is set -->
|
||||||
<script src="/static/types.js"></script>
|
<script src="{{ STATIC_BASE }}/types.js"></script>
|
||||||
<script src="/static/api-client.js"></script>
|
<script src="{{ STATIC_BASE }}/api-client.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Room icon mapping
|
// Room icon mapping
|
||||||
|
|||||||
118
create_proper_icons.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Script to create proper PNG icons with house and car symbols
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
def create_proper_icons():
|
||||||
|
static_dir = Path("/Users/wn/Workspace/home-automation/apps/ui/static")
|
||||||
|
|
||||||
|
# Create home icon with house symbol
|
||||||
|
def create_home_icon(size):
|
||||||
|
img = Image.new('RGBA', (size, size), color=(102, 126, 234, 255)) # #667EEA
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Calculate proportions
|
||||||
|
margin = size // 10
|
||||||
|
house_size = size - 2 * margin
|
||||||
|
|
||||||
|
# Draw house shape
|
||||||
|
# Base rectangle
|
||||||
|
base_height = house_size // 2
|
||||||
|
base_y = size - margin - base_height
|
||||||
|
draw.rectangle([margin, base_y, size - margin, size - margin], fill='white')
|
||||||
|
|
||||||
|
# Roof triangle
|
||||||
|
roof_height = house_size // 3
|
||||||
|
roof_points = [
|
||||||
|
(size // 2, margin), # top point
|
||||||
|
(margin, base_y), # bottom left
|
||||||
|
(size - margin, base_y) # bottom right
|
||||||
|
]
|
||||||
|
draw.polygon(roof_points, fill='white')
|
||||||
|
|
||||||
|
# Door
|
||||||
|
door_width = house_size // 6
|
||||||
|
door_height = base_height // 2
|
||||||
|
door_x = size // 2 - door_width // 2
|
||||||
|
door_y = size - margin - door_height
|
||||||
|
draw.rectangle([door_x, door_y, door_x + door_width, size - margin], fill=(102, 126, 234, 255))
|
||||||
|
|
||||||
|
# Window
|
||||||
|
window_size = house_size // 8
|
||||||
|
window_x = margin + house_size // 4
|
||||||
|
window_y = base_y + base_height // 4
|
||||||
|
draw.rectangle([window_x, window_y, window_x + window_size, window_y + window_size], fill=(102, 126, 234, 255))
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
# Create car icon with car symbol
|
||||||
|
def create_car_icon(size):
|
||||||
|
img = Image.new('RGBA', (size, size), color=(102, 126, 234, 255)) # #667EEA
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Calculate proportions
|
||||||
|
margin = size // 8
|
||||||
|
car_width = size - 2 * margin
|
||||||
|
car_height = car_width // 2
|
||||||
|
car_y = size // 2 - car_height // 2
|
||||||
|
|
||||||
|
# Draw car body
|
||||||
|
draw.rounded_rectangle([margin, car_y, size - margin, car_y + car_height],
|
||||||
|
radius=size//20, fill='white')
|
||||||
|
|
||||||
|
# Draw car roof
|
||||||
|
roof_margin = car_width // 4
|
||||||
|
roof_height = car_height // 2
|
||||||
|
roof_y = car_y - roof_height // 2
|
||||||
|
draw.rounded_rectangle([margin + roof_margin, roof_y,
|
||||||
|
size - margin - roof_margin, car_y + roof_height // 2],
|
||||||
|
radius=size//30, fill='white')
|
||||||
|
|
||||||
|
# Draw wheels
|
||||||
|
wheel_radius = car_height // 4
|
||||||
|
wheel_y = car_y + car_height - wheel_radius // 2
|
||||||
|
|
||||||
|
# Left wheel
|
||||||
|
left_wheel_x = margin + car_width // 4
|
||||||
|
draw.ellipse([left_wheel_x - wheel_radius, wheel_y - wheel_radius,
|
||||||
|
left_wheel_x + wheel_radius, wheel_y + wheel_radius],
|
||||||
|
fill=(102, 126, 234, 255))
|
||||||
|
|
||||||
|
# Right wheel
|
||||||
|
right_wheel_x = size - margin - car_width // 4
|
||||||
|
draw.ellipse([right_wheel_x - wheel_radius, wheel_y - wheel_radius,
|
||||||
|
right_wheel_x + wheel_radius, wheel_y + wheel_radius],
|
||||||
|
fill=(102, 126, 234, 255))
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
# Sizes to create
|
||||||
|
sizes = [16, 32, 57, 60, 72, 76, 114, 120, 144, 152, 180]
|
||||||
|
|
||||||
|
# Create home icons
|
||||||
|
for size in sizes:
|
||||||
|
home_icon = create_home_icon(size)
|
||||||
|
home_icon.save(static_dir / f"apple-touch-icon-{size}x{size}.png")
|
||||||
|
print(f"Created apple-touch-icon-{size}x{size}.png")
|
||||||
|
|
||||||
|
# Also create the main apple-touch-icon.png
|
||||||
|
main_icon = create_home_icon(180)
|
||||||
|
main_icon.save(static_dir / "apple-touch-icon.png")
|
||||||
|
print("Created apple-touch-icon.png")
|
||||||
|
|
||||||
|
# Create garage icons
|
||||||
|
for size in sizes:
|
||||||
|
car_icon = create_car_icon(size)
|
||||||
|
car_icon.save(static_dir / f"garage-icon-{size}x{size}.png")
|
||||||
|
print(f"Created garage-icon-{size}x{size}.png")
|
||||||
|
|
||||||
|
# Also create the main garage-icon.png
|
||||||
|
main_garage = create_car_icon(180)
|
||||||
|
main_garage.save(static_dir / "garage-icon.png")
|
||||||
|
print("Created garage-icon.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_proper_icons()
|
||||||
@@ -52,11 +52,6 @@ spec:
|
|||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: home-automation-environment
|
name: home-automation-environment
|
||||||
key: SHARED_REDIS_DB
|
key: SHARED_REDIS_DB
|
||||||
- name: REDIS_CHANNEL
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: home-automation-environment
|
|
||||||
key: API_REDIS_CHANNEL
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
mountPath: /app/config
|
mountPath: /app/config
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ data:
|
|||||||
# UI specific environment variables
|
# UI specific environment variables
|
||||||
UI_UI_PORT: "8002"
|
UI_UI_PORT: "8002"
|
||||||
UI_API_BASE: "https://homea2-api.hottis.de"
|
UI_API_BASE: "https://homea2-api.hottis.de"
|
||||||
|
UI_STATIC_BASE: "https://homea2-static.hottis.de"
|
||||||
UI_BASE_PATH: "/"
|
UI_BASE_PATH: "/"
|
||||||
|
|
||||||
# API specific environment variables
|
|
||||||
API_REDIS_CHANNEL: "ui:updates"
|
|
||||||
|
|
||||||
# Rules specific environment variables
|
|
||||||
RULES_RULES_CONFIG: "/app/config/rules.yaml"
|
|
||||||
@@ -11,6 +11,7 @@ spec:
|
|||||||
dnsNames:
|
dnsNames:
|
||||||
- homea2.hottis.de
|
- homea2.hottis.de
|
||||||
- homea2-api.hottis.de
|
- homea2-api.hottis.de
|
||||||
|
- homea2-static.hottis.de
|
||||||
---
|
---
|
||||||
apiVersion: traefik.containo.us/v1alpha1
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
kind: TLSOption
|
kind: TLSOption
|
||||||
@@ -59,4 +60,37 @@ spec:
|
|||||||
services:
|
services:
|
||||||
- name: api
|
- name: api
|
||||||
port: 80
|
port: 80
|
||||||
|
---
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: static
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
secretName: homea2-cert
|
||||||
|
routes:
|
||||||
|
- match: Host(`homea2-static.hottis.de`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: static
|
||||||
|
port: 80
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: api-internal
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik-internal
|
||||||
|
rules:
|
||||||
|
- host: homea2-api-internal.hottis.de
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: api
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ spec:
|
|||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: home-automation-environment
|
name: home-automation-environment
|
||||||
key: SHARED_REDIS_DB
|
key: SHARED_REDIS_DB
|
||||||
- name: RULES_CONFIG
|
|
||||||
valueFrom:
|
|
||||||
configMapKeyRef:
|
|
||||||
name: home-automation-environment
|
|
||||||
key: RULES_RULES_CONFIG
|
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config-volume
|
- name: config-volume
|
||||||
mountPath: /app/config
|
mountPath: /app/config
|
||||||
|
|||||||
61
deployment/static-deployment.yaml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: static
|
||||||
|
namespace: homea2
|
||||||
|
labels:
|
||||||
|
app: static
|
||||||
|
component: home-automation
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: static
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: static
|
||||||
|
component: home-automation
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: static
|
||||||
|
image: %IMAGE%
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: static
|
||||||
|
namespace: homea2
|
||||||
|
labels:
|
||||||
|
app: static
|
||||||
|
component: home-automation
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: static
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
name: http
|
||||||
|
type: ClusterIP
|
||||||
@@ -37,6 +37,11 @@ spec:
|
|||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
name: home-automation-environment
|
name: home-automation-environment
|
||||||
key: UI_API_BASE
|
key: UI_API_BASE
|
||||||
|
- name: STATIC_BASE
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: home-automation-environment
|
||||||
|
key: UI_STATIC_BASE
|
||||||
- name: BASE_PATH
|
- name: BASE_PATH
|
||||||
valueFrom:
|
valueFrom:
|
||||||
configMapKeyRef:
|
configMapKeyRef:
|
||||||
|
|||||||
77
icon-test.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Icon Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.icon-test {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.icon-test img {
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.icon-sizes {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Apple Touch Icon Test</h1>
|
||||||
|
|
||||||
|
<div class="icon-container">
|
||||||
|
<div class="icon-test">
|
||||||
|
<h3>Home Icon</h3>
|
||||||
|
<img src="apps/ui/static/apple-touch-icon.png" alt="Home Icon" width="120" height="120">
|
||||||
|
<p>Haupticon für die Home Automation App</p>
|
||||||
|
<div class="icon-sizes">
|
||||||
|
<img src="apps/ui/static/apple-touch-icon-76x76.png" alt="Home 76px" width="76" height="76">
|
||||||
|
<img src="apps/ui/static/apple-touch-icon-60x60.png" alt="Home 60px" width="60" height="60">
|
||||||
|
<img src="apps/ui/static/apple-touch-icon-32x32.png" alt="Home 32px" width="32" height="32">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="icon-test">
|
||||||
|
<h3>Garage Icon</h3>
|
||||||
|
<img src="apps/ui/static/garage-icon.png" alt="Garage Icon" width="120" height="120">
|
||||||
|
<p>Icon für die Garage-Seite</p>
|
||||||
|
<div class="icon-sizes">
|
||||||
|
<img src="apps/ui/static/garage-icon-76x76.png" alt="Garage 76px" width="76" height="76">
|
||||||
|
<img src="apps/ui/static/garage-icon-60x60.png" alt="Garage 60px" width="60" height="60">
|
||||||
|
<img src="apps/ui/static/garage-icon-32x32.png" alt="Garage 32px" width="32" height="32">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>iPhone Homescreen Test</h2>
|
||||||
|
<p>Um die Icons auf dem iPhone zu testen:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Öffnen Sie Ihre Home Automation App im Safari</li>
|
||||||
|
<li>Tippen Sie auf das Teilen-Symbol</li>
|
||||||
|
<li>Wählen Sie "Zum Home-Bildschirm hinzufügen"</li>
|
||||||
|
<li>Das Icon sollte jetzt als Haus-Symbol erscheinen</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p><strong>Hinweis:</strong> Falls das alte Icon noch angezeigt wird, löschen Sie die bestehende App vom Homescreen und fügen Sie sie neu hinzu.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||