Compare commits

..

17 Commits
0.2.0 ... main

Author SHA1 Message Date
1311f7a59b Putzlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:53:17 +01:00
a226fa9268 Putzlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 16:26:38 +01:00
3bd8d293a2 fix licht spuele 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-01 15:46:43 +01:00
be30ad3a3c fix licht spuele 2025-12-01 15:45:12 +01:00
500384b1cd streamline ci 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
2025-12-01 14:56:56 +01:00
6b4c247413 forgotten files
All checks were successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-01 14:53:50 +01:00
04a1807306 streamline ci
Some checks failed
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
ci/woodpecker/tag/build/1 Pipeline failed
2025-12-01 14:52:50 +01:00
db5e4589d0 fix error for devices with missing state 2025-12-01 14:48:32 +01:00
5399f044a1 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
6
2025-12-01 14:24:15 +01:00
16fa5143dd separation of ui and static
All checks were successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
5
2025-12-01 14:15:54 +01:00
cff154c247 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
4
2025-12-01 14:11:25 +01:00
038664ec94 separation of ui and static
All checks were successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
3
2025-12-01 14:06:54 +01:00
2bbf825cf7 separation of ui and static
All checks were successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
2
2025-12-01 14:02:37 +01:00
5e0159047c separation of ui and static
Some checks failed
ci/woodpecker/tag/build/5 Pipeline failed
ci/woodpecker/tag/predeploy Pipeline was successful
ci/woodpecker/tag/deploy/2 unknown status
ci/woodpecker/tag/deploy/1 unknown status
ci/woodpecker/tag/build/2 Pipeline failed
ci/woodpecker/tag/deploy/4 unknown status
ci/woodpecker/tag/deploy/3 unknown status
ci/woodpecker/tag/build/3 Pipeline failed
ci/woodpecker/tag/deploy/5 unknown status
ci/woodpecker/tag/ingress unknown status
ci/woodpecker/tag/build/1 Pipeline failed
ci/woodpecker/tag/build/4 Pipeline failed
2025-12-01 14:00:48 +01:00
b23b624a86 homekit bridge name 2025-12-01 12:45:01 +01:00
9c099e44af drop homekit bridge build script 2025-12-01 11:06:49 +01:00
9c17a73605 build homekit-bridge image 2 2025-12-01 10:54:57 +01:00
55 changed files with 256 additions and 486 deletions

View File

@@ -1,35 +0,0 @@
when:
event: [tag]
steps:
build-homekit-arm64:
image: docker:27.2.0-dind
privileged: true
environment:
REGISTRY:
from_secret: local_registry
USERNAME:
from_secret: local_username
PASSWORD:
from_secret: local_password
commands:
- echo "Logging in to registry $REGISTRY as $USERNAME"
- echo "$PASSWORD" | docker login "$REGISTRY" -u "$USERNAME" --password-stdin
- docker version
- echo "Enabling binfmt for multi-arch builds"
- docker run --privileged --rm tonistiigi/binfmt --install all
- echo "Creating and bootstrapping buildx builder"
- docker buildx create --use --name multiarch-builder || docker buildx use multiarch-builder
- docker buildx inspect --bootstrap
- export IMAGE_TAG="${FORGE_NAME}/${CI_REPO}/homekit:${CI_COMMIT_TAG}"
- echo "Building ARM64 image: $IMAGE_TAG"
- docker buildx build \
--platform linux/arm64 \
-f apps/homekit/Dockerfile \
-t "$IMAGE_TAG" \
--push .
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

View File

@@ -1,5 +1,8 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
matrix:
APP:
@@ -7,6 +10,7 @@ matrix:
- api
- abstraction
- rules
- static
steps:
build-${APP}:
@@ -21,8 +25,3 @@ steps:
repo: ${FORGE_NAME}/${CI_REPO}/${APP}
auto_tag: true
dockerfile: apps/${APP}/Dockerfile
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

View File

@@ -1,23 +1,10 @@
when:
event: [tag]
steps:
create_namespace:
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 create namespace $NAMESPACE || echo "Namespace $NAMESPACE already exists"
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on:
- namespace
steps:
apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4
environment:
@@ -36,6 +23,4 @@ steps:
--namespace=$NAMESPACE
--dry-run=client -o yaml | kubectl apply -f -
- kubectl apply -f deployment/configmap.yaml -n $NAMESPACE
when:
event: [tag]

View File

@@ -1,9 +1,13 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on:
- build
- predeploy
- namespace
- config
matrix:
APP:
@@ -11,6 +15,7 @@ matrix:
- api
- abstraction
- rules
- static
steps:
deploy-${APP}:
@@ -25,9 +30,5 @@ steps:
- export KUBECONFIG=/tmp/kubeconfig
- echo "Deploying application ${APP} ($IMAGE) to namespace $NAMESPACE"
- cat deployment/${APP}-deployment.yaml | sed "s,%IMAGE%,$IMAGE,g" | kubectl apply -n $NAMESPACE -f -
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange

View File

@@ -1,5 +1,8 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
depends_on:
- deploy
@@ -15,9 +18,4 @@ steps:
- 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

15
.woodpecker/namespace.yml Normal file
View File

@@ -0,0 +1,15 @@
when:
event: [tag]
steps:
create_namespace:
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 create namespace $NAMESPACE || echo "Namespace $NAMESPACE already exists"

View File

@@ -9,7 +9,7 @@ services:
network_mode: host
environment:
- HOMEKIT_NAME=Home Automation Bridge
- HOMEKIT_NAME=Hottis Home Automation Bridge
- HOMEKIT_PIN=031-45-154
- HOMEKIT_PORT=51826

15
apps/static/Dockerfile Normal file
View 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

View File

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

View File

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 639 B

View File

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 827 B

View File

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 884 B

View File

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 153 B

View File

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1018 B

View File

Before

Width:  |  Height:  |  Size: 210 B

After

Width:  |  Height:  |  Size: 210 B

View File

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

View File

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 432 B

View File

Before

Width:  |  Height:  |  Size: 1018 B

After

Width:  |  Height:  |  Size: 1018 B

View File

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 244 B

View File

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

View File

Before

Width:  |  Height:  |  Size: 519 B

After

Width:  |  Height:  |  Size: 519 B

View File

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 547 B

View File

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

View File

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 695 B

View File

Before

Width:  |  Height:  |  Size: 126 B

After

Width:  |  Height:  |  Size: 126 B

View File

Before

Width:  |  Height:  |  Size: 808 B

After

Width:  |  Height:  |  Size: 808 B

View File

Before

Width:  |  Height:  |  Size: 192 B

After

Width:  |  Height:  |  Size: 192 B

View File

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 347 B

View File

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 368 B

View File

Before

Width:  |  Height:  |  Size: 808 B

After

Width:  |  Height:  |  Size: 808 B

View File

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1 @@
empty

View File

@@ -1,49 +1,41 @@
# UI Service Dockerfile
# FastAPI + Jinja2 + HTMX Dashboard
# UI Service Dockerfile (Application only, without static files)
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UI_PORT=8002 \
API_BASE=http://api:8001 \
BASE_PATH=""
BASE_PATH="" \
STATIC_BASE=http://static:8080
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
curl \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/ui/requirements.txt /app/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/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
# Switch to non-root user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${UI_PORT}/health || exit 1
# Expose port
EXPOSE 8002
# Run application
CMD ["python", "-m", "uvicorn", "apps.ui.main:app", "--host", "0.0.0.0", "--port", "8002"]

View File

@@ -16,9 +16,11 @@ logger = logging.getLogger(__name__)
# Read configuration from environment variables
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
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 BASE_PATH: {BASE_PATH}")
print(f"UI using STATIC_BASE: {STATIC_BASE}")
def api_url(path: str) -> str:
"""Helper function to construct API URLs.
@@ -43,6 +45,9 @@ app = FastAPI(
templates_dir = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_dir))
# Make STATIC_BASE available in all templates
templates.env.globals["STATIC_BASE"] = STATIC_BASE
# Setup static files
static_dir = Path(__file__).parent / "static"
static_dir.mkdir(exist_ok=True)
@@ -98,7 +103,8 @@ async def health() -> JSONResponse:
"status": "ok",
"service": "ui",
"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", {
"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", {
"request": request,
"api_base": API_BASE,
"room_name": room_name
"room_name": room_name,
})

View File

@@ -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.)

View File

@@ -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"
}
]
}

View File

@@ -6,17 +6,16 @@
<title>Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/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="16x16" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/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_BASE }}/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_BASE }}/apple-touch-icon.png">
<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-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Dashboard">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/static/manifest.json">
<style>
* {
margin: 0;

View File

@@ -6,16 +6,15 @@
<title>Gerät - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/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="16x16" 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_BASE }}/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_BASE }}/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-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Gerät">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/static/manifest.json">
<style>
* {
margin: 0;
@@ -346,8 +345,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Get device ID from URL
@@ -359,6 +358,7 @@
let deviceData = null;
let deviceState = {};
let roomName = '';
let deviceStateUnknown = false;
// Device type icons
const deviceIcons = {
@@ -381,8 +381,19 @@
// NEW: Use new endpoints for device info and layout
deviceData = await window.apiClient.getDevice(deviceId);
console.log("Loaded device data:", deviceData);
try {
deviceState = await window.apiClient.getDeviceState(deviceId);
console.log("Loaded device state:", deviceState);
if (!deviceState || Object.keys(deviceState).length === 0) {
deviceStateUnknown = true;
deviceState = {};
}
} catch (stateError) {
console.warn('No state for device, using unknown state:', stateError);
deviceStateUnknown = true;
deviceState = {};
}
const layoutInfo = await window.apiClient.getDeviceLayout(deviceId);
console.log("Loaded layout info:", layoutInfo);
roomName = layoutInfo.room;
@@ -518,6 +529,14 @@
}, 0);
}
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}
@@ -553,6 +572,14 @@
`;
card.appendChild(sliderGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
setTimeout(() => {
@@ -581,6 +608,14 @@
powerGroup.appendChild(powerButton);
card.appendChild(powerGroup);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}
@@ -599,6 +634,14 @@
`;
card.appendChild(statusDiv);
if (deviceStateUnknown) {
const hint = document.createElement('div');
hint.className = 'device-meta';
hint.style.marginTop = '12px';
hint.textContent = 'Status unbekannt';
card.appendChild(hint);
}
container.appendChild(card);
}

View File

@@ -6,17 +6,16 @@
<title>Garage - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/garage-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/garage-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/garage-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/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="16x16" href="/static/garage-icon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_BASE }}/garage-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_BASE }}/garage-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_BASE }}/garage-icon-120x120.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_BASE }}/garage-icon-32x32.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-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Garage">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/manifest.json">
<style>
* {
margin: 0;
@@ -306,8 +305,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Device IDs for garage devices

View File

@@ -6,17 +6,16 @@
<title>Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/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="120x120" href="/static/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/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="16x16" href="/static/apple-touch-icon-16x16.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_BASE }}/apple-touch-icon-152x152.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_BASE }}/apple-touch-icon-76x76.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_BASE }}/apple-touch-icon-16x16.png">
<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-title" content="Home Automation">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/static/manifest.json">
<style>
* {
margin: 0;

View File

@@ -6,16 +6,15 @@
<title>{{ room_name }} - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/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="16x16" 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_BASE }}/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_BASE }}/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-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="{{ room_name }}">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/static/manifest.json">
<style>
* {
margin: 0;
@@ -229,8 +228,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Get room name from URL

View File

@@ -6,17 +6,16 @@
<title>Räume - Home Automation</title>
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/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="120x120" href="/static/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/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="16x16" href="/static/apple-touch-icon-16x16.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_BASE }}/apple-touch-icon-152x152.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_BASE }}/apple-touch-icon-76x76.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_BASE }}/apple-touch-icon-16x16.png">
<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-title" content="Räume">
<meta name="theme-color" content="#667eea">
<link rel="manifest" href="/manifest.json">
<style>
* {
margin: 0;
@@ -163,8 +162,8 @@
</script>
<!-- Load API client AFTER API_BASE is set -->
<script src="/static/types.js"></script>
<script src="/static/api-client.js"></script>
<script src="{{ STATIC_BASE }}/types.js"></script>
<script src="{{ STATIC_BASE }}/api-client.js"></script>
<script>
// Room icon mapping

View File

@@ -730,8 +730,19 @@ devices:
features:
power: true
topics:
set: "shellies/LightKitchenSink/relay/0/command"
state: "shellies/LightKitchenSink/relay/0"
set: "shellies/shellyplug-s-DED4E4/relay/0/command"
state: "shellies/shellyplug-s-DED4E4/relay/0"
- device_id: putzlicht_kueche
name: Putzlicht
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xa4c138563834406c"
set: "zigbee2mqtt/0xa4c138563834406c/set"
- device_id: licht_schrank_esszimmer
name: Schrank
type: relay

View File

@@ -123,6 +123,10 @@ rooms:
title: Küche Spüle
icon: 💡
rank: 142
- device_id: putzlicht_kueche
title: Küche Putzlicht
icon: 💡
rank: 143
- device_id: thermostat_kueche
title: Kueche
icon: 🌡️

View File

@@ -14,6 +14,7 @@ data:
# UI specific environment variables
UI_UI_PORT: "8002"
UI_API_BASE: "https://homea2-api.hottis.de"
UI_STATIC_BASE: "https://homea2-static.hottis.de"
UI_BASE_PATH: "/"

View File

@@ -11,6 +11,7 @@ spec:
dnsNames:
- homea2.hottis.de
- homea2-api.hottis.de
- homea2-static.hottis.de
---
apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
@@ -60,6 +61,22 @@ spec:
- name: api
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:

View 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

View File

@@ -37,6 +37,11 @@ spec:
configMapKeyRef:
name: home-automation-environment
key: UI_API_BASE
- name: STATIC_BASE
valueFrom:
configMapKeyRef:
name: home-automation-environment
key: UI_STATIC_BASE
- name: BASE_PATH
valueFrom:
configMapKeyRef: