Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76d88dd77f |
@@ -105,7 +105,6 @@ _CONTEXT_DOMAINS = frozenset({
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"person",
|
||||
"sun",
|
||||
"weather",
|
||||
@@ -123,29 +122,6 @@ _NUMERIC_STATE_CLASSES = frozenset({"measurement", "total", "total_increasing"})
|
||||
|
||||
|
||||
def classify_entity(entity: HaEntitySummary) -> DiscoveredEntity:
|
||||
if entity.domain in _ACTUATOR_DOMAINS:
|
||||
return _result(
|
||||
entity,
|
||||
EntityRole.ACTUATOR,
|
||||
category=_actuator_category(entity),
|
||||
learnable=False,
|
||||
reason="Aktor ist ein mögliches Automationsziel, aber kein Trainingssensor.",
|
||||
)
|
||||
|
||||
if entity.domain in _CONTEXT_DOMAINS:
|
||||
learnable = entity.domain in _LEARNABLE_CONTEXT_DOMAINS
|
||||
return _result(
|
||||
entity,
|
||||
EntityRole.CONTEXT,
|
||||
category=_context_category(entity),
|
||||
learnable=learnable,
|
||||
reason=(
|
||||
"Kontextquelle für Training und Erklärungen."
|
||||
if learnable
|
||||
else "Kontextquelle ohne direkte Trainingsfreigabe."
|
||||
),
|
||||
)
|
||||
|
||||
if entity.domain == "sensor" and (
|
||||
entity.state_class in _NUMERIC_STATE_CLASSES
|
||||
or entity.device_class in _MEASUREMENT_CLASSES
|
||||
@@ -168,6 +144,29 @@ def classify_entity(entity: HaEntitySummary) -> DiscoveredEntity:
|
||||
reason="Binärer Kontextsensor für Zustands- und Anwesenheitsmuster.",
|
||||
)
|
||||
|
||||
if entity.domain in _CONTEXT_DOMAINS:
|
||||
learnable = entity.domain in _LEARNABLE_CONTEXT_DOMAINS
|
||||
return _result(
|
||||
entity,
|
||||
EntityRole.CONTEXT,
|
||||
category=_context_category(entity),
|
||||
learnable=learnable,
|
||||
reason=(
|
||||
"Kontextquelle für Training und Erklärungen."
|
||||
if learnable
|
||||
else "Kontextquelle ohne direkte Trainingsfreigabe."
|
||||
),
|
||||
)
|
||||
|
||||
if entity.domain in _ACTUATOR_DOMAINS:
|
||||
return _result(
|
||||
entity,
|
||||
EntityRole.ACTUATOR,
|
||||
category=_actuator_category(entity),
|
||||
learnable=False,
|
||||
reason="Aktor ist ein mögliches Automationsziel, aber kein Trainingssensor.",
|
||||
)
|
||||
|
||||
return _result(
|
||||
entity,
|
||||
EntityRole.UNSUPPORTED,
|
||||
@@ -227,33 +226,26 @@ def _actuator_category(entity: HaEntitySummary) -> str:
|
||||
if entity.domain == "lock":
|
||||
return "lock"
|
||||
if entity.domain == "fan":
|
||||
return "ventilation"
|
||||
if entity.domain == "humidifier":
|
||||
return "climate"
|
||||
return "fan"
|
||||
if entity.domain in {"media_player", "remote"}:
|
||||
return "media_tv"
|
||||
if entity.domain == "input_boolean" or entity.domain == "number" or entity.domain == "input_button":
|
||||
if entity.domain in {"input_boolean", "number"}:
|
||||
return "helper"
|
||||
if entity.domain == "sensor" and (entity.device_class or entity.unit_of_measurement):
|
||||
return "measurement"
|
||||
return entity.domain
|
||||
|
||||
|
||||
def _measurement_category(entity: HaEntitySummary) -> str:
|
||||
device_class = entity.device_class or ""
|
||||
unit = (entity.unit_of_measurement or "").lower()
|
||||
if device_class == "illuminance" or unit == "lx":
|
||||
if device_class == "illuminance":
|
||||
return "brightness"
|
||||
if device_class == "temperature" or unit in {"°c", "°f", "k"}:
|
||||
if device_class == "temperature":
|
||||
return "temperature"
|
||||
if device_class in {"humidity", "moisture"} or unit in {"%", "rh", "g/m³", "kg/m³"}:
|
||||
if device_class in {"humidity", "moisture"}:
|
||||
return "humidity"
|
||||
if device_class in {"power", "energy", "current", "voltage"} or unit in {"w", "kw", "kwh", "a", "v", "va", "var"}:
|
||||
if device_class in {"power", "energy", "current", "voltage"}:
|
||||
return "energy_power"
|
||||
if device_class in {"battery", "signal_strength"} or unit in {"%", "dbm"}:
|
||||
if device_class in {"battery", "signal_strength"}:
|
||||
return "diagnostic"
|
||||
if unit:
|
||||
return f"measurement:{unit}"
|
||||
return "measurement"
|
||||
|
||||
|
||||
@@ -261,7 +253,7 @@ def _binary_category(entity: HaEntitySummary) -> str:
|
||||
device_class = entity.device_class or ""
|
||||
if device_class in {"motion", "occupancy", "presence"}:
|
||||
return "presence_motion"
|
||||
if device_class in {"door", "garage_door", "opening", "window", "button"}:
|
||||
if device_class in {"door", "garage_door", "opening", "window"}:
|
||||
return "opening"
|
||||
if device_class in {"smoke", "safety", "problem"}:
|
||||
return "safety"
|
||||
@@ -273,8 +265,4 @@ def _context_category(entity: HaEntitySummary) -> str:
|
||||
return "helper"
|
||||
if entity.domain in {"person", "device_tracker", "zone"}:
|
||||
return "presence_location"
|
||||
if entity.domain == "sun":
|
||||
return "weather"
|
||||
if entity.domain == "weather":
|
||||
return "weather"
|
||||
return entity.domain
|
||||
|
||||
@@ -285,15 +285,14 @@ function optionGroups(entities, selectedIds = new Set()) {
|
||||
async function loadOverview() {
|
||||
const status = document.getElementById("status");
|
||||
const chips = document.getElementById("status-chips");
|
||||
let reconciliation = null;
|
||||
try {
|
||||
const [health, websocket, ml] = await Promise.all([
|
||||
const [health, websocket, ml, reconciliation, actuators] = await Promise.all([
|
||||
api("health"),
|
||||
api("health/websocket"),
|
||||
api("ml/health"),
|
||||
api("v1/actuators/reconciliation/state"),
|
||||
api("v1/actuators"),
|
||||
]);
|
||||
reconciliation = await api("v1/actuators/reconciliation/state");
|
||||
const actuators = await api("v1/actuators");
|
||||
status.innerHTML = `<p class="ok">System bereit</p><p>Letzte automatische Prüfung: ${escapeHtml(reconciliation.last_completed_at || "noch nie")}</p>`;
|
||||
chips.innerHTML = [
|
||||
`<span class="chip">API: ${escapeHtml(health.status)}</span>`,
|
||||
@@ -314,8 +313,12 @@ async function loadActuatorDiscovery() {
|
||||
const options = document.getElementById("actuator-options");
|
||||
const select = document.getElementById("actuator-select");
|
||||
try {
|
||||
const available = await api("v1/actuators/discovery");
|
||||
actuatorChoices = available;
|
||||
const [available, configured] = await Promise.all([
|
||||
api("v1/actuators/discovery"),
|
||||
api("v1/actuators"),
|
||||
]);
|
||||
const configuredIds = new Set(configured.map(record => record.actuator_entity_id));
|
||||
actuatorChoices = available.filter(entity => !configuredIds.has(entity.entity_id));
|
||||
options.innerHTML = actuatorChoices.slice(0, 120).map(entity =>
|
||||
`<option value="${escapeHtml(entity.entity_id)}">${escapeHtml(entity.friendly_name || entity.entity_id)}${entity.area_name ? ` (${escapeHtml(entity.area_name)})` : ""}</option>`
|
||||
).join("");
|
||||
@@ -354,17 +357,20 @@ async function loadActuatorSuggestions() {
|
||||
|
||||
function actuatorGroupLabel(domain) {
|
||||
const labels = {
|
||||
light: "Lichter",
|
||||
switch_socket: "Steckdosen / Schalter",
|
||||
button: "Buttons",
|
||||
cover_shutter: "Rollläden / Fenster",
|
||||
heating: "Heizungen / Klima",
|
||||
ventilation: "Lüftung / Ventilatoren",
|
||||
climate: "Klima",
|
||||
helper: "Helper",
|
||||
measurement: "Sensoren / Messung",
|
||||
climate: "Heizungen / Klima",
|
||||
light: "Lichter",
|
||||
input_boolean: "Helper-Schalter",
|
||||
input_button: "Helper-Buttons",
|
||||
lock: "Schlösser",
|
||||
media_tv: "TV / Medien",
|
||||
media_player: "TV / Medien",
|
||||
number: "Numerische Helper",
|
||||
remote: "Fernbedienungen",
|
||||
switch: "Schalter / Steckdosen",
|
||||
cover: "Rollläden / Cover",
|
||||
fan: "Lüftung / Ventilatoren",
|
||||
humidifier: "Befeuchter / Entfeuchter",
|
||||
valve: "Ventile",
|
||||
};
|
||||
return labels[domain] || domain;
|
||||
}
|
||||
@@ -812,31 +818,3 @@ loadOverview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
function categoryLabel(category) {
|
||||
const labels = {
|
||||
light: "Licht",
|
||||
switch_socket: "Steckdosen / Schalter",
|
||||
button: "Buttons",
|
||||
cover_shutter: "Rollläden / Cover",
|
||||
heating: "Heizungen / Klima",
|
||||
fan: "Lüftung / Ventilatoren",
|
||||
ventilation: "Lüftung / Ventilatoren",
|
||||
media_tv: "TV / Medien",
|
||||
helper: "Helper",
|
||||
presence_motion: "Bewegung / Präsenz",
|
||||
opening: "Fenster / Türen",
|
||||
weather: "Wetter",
|
||||
brightness: "Helligkeit",
|
||||
temperature: "Temperatur",
|
||||
humidity: "Feuchtigkeit",
|
||||
energy_power: "Strom / Energie",
|
||||
diagnostic: "Diagnose",
|
||||
measurement: "Messung",
|
||||
binary: "Binär",
|
||||
presence_location: "Anwesenheit / Ort",
|
||||
climate: "Klima",
|
||||
};
|
||||
return labels[category] || category;
|
||||
}
|
||||
93
docs/sillyhome-dev-install.md
Normal file
93
docs/sillyhome-dev-install.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Locale Dev-Umgebung — sillyhome-next-dev
|
||||
|
||||
Ziel: Repo lokal klonen, Backend starten und Änderungen entwickeln, ohne das produktive Add-on direkt zu beeinflussen.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Git
|
||||
- Python 3.10+
|
||||
- `pip`
|
||||
- Optional: Nginx als Reverse-Proxy
|
||||
|
||||
## 1. Repo klonen
|
||||
|
||||
```bash
|
||||
git clone http://192.168.6.31:3000/Otto/sillyhome-next-dev.git
|
||||
cd sillyhome-next-dev
|
||||
```
|
||||
|
||||
## 2. Abhängigkeiten installieren
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python3 python3-pip uvicorn
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
## 3. Backend starten (temporär)
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
## 4. Backend dauerhaft per systemd starten
|
||||
|
||||
Datei `/etc/systemd/system/sillyhome-dev.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Sillyhome Next Dev
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/otto/sillyhome-next-dev
|
||||
Environment="PATH=/home/otto/.local/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/usr/local/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Aktivieren:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now sillyhome-dev.service
|
||||
```
|
||||
|
||||
## 5. Optional: Nginx Reverse-Proxy
|
||||
|
||||
Datei `/etc/nginx/sites-available/sillyhome-dev`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 8080;
|
||||
server_name sillyhome-dev.local;
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Aktivieren:
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/sillyhome-dev /etc/nginx/sites-enabled/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 6. Nur Frontend testen
|
||||
|
||||
```bash
|
||||
cd /home/otto/sillyhome-next-dev/app/static
|
||||
python3 -m http.server 8080 --directory .
|
||||
```
|
||||
|
||||
Danach im Browser `http://127.0.0.1:8080` öffnen.
|
||||
|
||||
## Hinweis für Weiterentwicklung
|
||||
|
||||
Dieser Branch ist für lokale Entwicklungsiterationen gedacht. Änderungen hier sollen einzeln reviewt und danach in den passenden Produktiv-Branch übernommen werden.
|
||||
Reference in New Issue
Block a user