Initial commit

This commit is contained in:
Mustafa KURU
2026-05-03 23:47:54 +03:00
commit 1dac1749bf
74 changed files with 6697 additions and 0 deletions
@@ -0,0 +1,179 @@
"""Home Assistant integration for Elegoo spaghetti detection."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_CONFIG_ENTRY,
CONF_DETECTOR,
CONF_FORCE,
CONF_IMAGE_URL,
CONF_OBICO_AUTH_TOKEN,
CONF_OBICO_HOST,
DOMAIN,
PLATFORMS,
REQUIRED_CONFIG_KEYS,
RUNTIME_ML_LOCK,
RUNTIME_BY_DETECTOR,
RUNTIME_DATA,
SERVICE_PREDICT,
SERVICE_RESET_STATE,
SERVICE_RUN_DETECTION,
)
from .runtime import SpaghettiDetectorRuntime
LOGGER = logging.getLogger(__package__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
PREDICT_SCHEMA = vol.Schema(
{
vol.Required(CONF_OBICO_HOST): str,
vol.Required(CONF_OBICO_AUTH_TOKEN): str,
vol.Required(CONF_IMAGE_URL): str,
}
)
DETECTOR_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CONFIG_ENTRY): str,
vol.Optional(CONF_DETECTOR): str,
vol.Optional(CONF_FORCE, default=True): bool,
}
)
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up global services for Elegoo spaghetti detection."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(RUNTIME_DATA, {})
hass.data[DOMAIN].setdefault(RUNTIME_BY_DETECTOR, {})
hass.data[DOMAIN].setdefault(RUNTIME_ML_LOCK, asyncio.Lock())
async def predict_handler(call: ServiceCall) -> ServiceResponse:
"""Run the Obico ML model for a raw image URL."""
result = await _async_predict_raw(
hass,
call.data[CONF_OBICO_HOST],
call.data[CONF_OBICO_AUTH_TOKEN],
call.data[CONF_IMAGE_URL],
)
return {"result": result}
async def run_detection_handler(call: ServiceCall) -> ServiceResponse:
"""Run one detection against the configured detector."""
runtime = _runtime_from_call(hass, call)
return await runtime.async_run_detection(manual=bool(call.data[CONF_FORCE]))
async def reset_handler(call: ServiceCall) -> None:
"""Reset detector state."""
runtime = _runtime_from_call(hass, call)
runtime.reset()
hass.services.async_register(
DOMAIN,
SERVICE_PREDICT,
predict_handler,
schema=PREDICT_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_RUN_DETECTION,
run_detection_handler,
schema=DETECTOR_SERVICE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_RESET_STATE,
reset_handler,
schema=DETECTOR_SERVICE_SCHEMA,
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up one spaghetti detector."""
missing = sorted(
key
for key in REQUIRED_CONFIG_KEYS
if key not in entry.data and key not in entry.options
)
if missing:
LOGGER.error(
"Config entry %s is incomplete and must be removed and recreated. Missing: %s",
entry.title,
", ".join(missing),
)
return False
runtime = SpaghettiDetectorRuntime(hass, entry)
hass.data[DOMAIN][RUNTIME_DATA][entry.entry_id] = runtime
hass.data[DOMAIN][RUNTIME_BY_DETECTOR][runtime.detector_id] = runtime
await runtime.async_setup()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload one spaghetti detector."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
runtime = hass.data[DOMAIN][RUNTIME_DATA].pop(entry.entry_id, None)
if runtime is not None:
hass.data[DOMAIN][RUNTIME_BY_DETECTOR].pop(runtime.detector_id, None)
await runtime.async_unload()
return unload_ok
async def _async_predict_raw(
hass: HomeAssistant,
obico_host: str,
obico_auth_token: str,
image_url: str,
) -> dict[str, Any]:
"""Call Obico ML directly."""
try:
session = async_get_clientsession(hass)
async with session.get(
f"{obico_host.rstrip('/')}/p/",
params={"img": image_url},
headers={"Authorization": f"Bearer {obico_auth_token}"},
timeout=aiohttp.ClientTimeout(total=60),
) as response:
response.raise_for_status()
result = await response.json()
if not isinstance(result, dict):
return {"detections": []}
return result
except (aiohttp.ClientError, TimeoutError) as err:
LOGGER.warning("Obico ML request failed: %s", err)
return {"detections": []}
def _runtime_from_call(
hass: HomeAssistant,
call: ServiceCall,
) -> SpaghettiDetectorRuntime:
"""Resolve a runtime from a service call."""
runtime: SpaghettiDetectorRuntime | None = None
if config_entry_id := call.data.get(CONF_CONFIG_ENTRY):
runtime = hass.data[DOMAIN][RUNTIME_DATA].get(config_entry_id)
elif detector := call.data.get(CONF_DETECTOR):
runtime = hass.data[DOMAIN][RUNTIME_BY_DETECTOR].get(detector)
if runtime is None:
raise HomeAssistantError("Unknown Elegoo spaghetti detector")
return runtime
@@ -0,0 +1,47 @@
"""Binary sensors for Elegoo spaghetti detection."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import CONF_INSTANCE_ID, DOMAIN, RUNTIME_DATA
from .entity import SpaghettiDetectorEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities,
) -> None:
"""Set up binary sensors."""
runtime = hass.data[DOMAIN][RUNTIME_DATA][entry.entry_id]
async_add_entities([SpaghettiDetectedBinarySensor(entry, runtime)])
class SpaghettiDetectedBinarySensor(SpaghettiDetectorEntity, BinarySensorEntity):
"""Spaghetti detected state."""
_attr_name = "Spaghetti Detected"
_attr_icon = "mdi:alert-octagram"
def __init__(self, entry: ConfigEntry, runtime) -> None:
super().__init__(entry, runtime, "spaghetti_detected")
self.entity_id = (
f"binary_sensor.{entry.data[CONF_INSTANCE_ID]}_spaghetti_detected"
)
@property
def is_on(self) -> bool:
"""Return true if spaghetti was detected."""
return self.runtime.detected
@property
def extra_state_attributes(self) -> dict:
"""Return debug attributes."""
return {
"confidence": self.runtime.confidence,
"raw_score": self.runtime.raw_score,
"warning": self.runtime.warning,
"detections": self.runtime.detection_count,
"last_error": self.runtime.last_error,
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

@@ -0,0 +1,77 @@
"""Buttons for Elegoo spaghetti detection."""
from dataclasses import dataclass
from typing import Awaitable, Callable
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import CONF_INSTANCE_ID, DOMAIN, RUNTIME_DATA
from .entity import SpaghettiDetectorEntity
from .runtime import SpaghettiDetectorRuntime
@dataclass(frozen=True, kw_only=True)
class DetectorButtonDescription(ButtonEntityDescription):
"""Detector button description."""
press_fn: Callable[[SpaghettiDetectorRuntime], Awaitable[None]]
async def _run_detection(runtime: SpaghettiDetectorRuntime) -> None:
"""Run one manual detection."""
await runtime.async_run_detection(manual=True)
async def _reset_state(runtime: SpaghettiDetectorRuntime) -> None:
"""Reset detection state."""
runtime.reset()
BUTTONS: tuple[DetectorButtonDescription, ...] = (
DetectorButtonDescription(
key="test_spaghetti_detection",
name="Test Spaghetti Detection",
icon="mdi:camera-iris",
press_fn=_run_detection,
),
DetectorButtonDescription(
key="reset_detection_state",
name="Reset Detection State",
icon="mdi:restart",
press_fn=_reset_state,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities,
) -> None:
"""Set up buttons."""
runtime = hass.data[DOMAIN][RUNTIME_DATA][entry.entry_id]
async_add_entities(
DetectorButton(entry, runtime, description) for description in BUTTONS
)
class DetectorButton(SpaghettiDetectorEntity, ButtonEntity):
"""Detector action button."""
entity_description: DetectorButtonDescription
def __init__(
self,
entry: ConfigEntry,
runtime: SpaghettiDetectorRuntime,
description: DetectorButtonDescription,
) -> None:
super().__init__(entry, runtime, description.key)
self.entity_description = description
self.entity_id = f"button.{entry.data[CONF_INSTANCE_ID]}_{description.key}"
async def async_press(self) -> None:
"""Handle button press."""
await self.entity_description.press_fn(self.runtime)
@@ -0,0 +1,499 @@
"""Config flow for Elegoo spaghetti detection."""
from __future__ import annotations
from typing import Any
import aiohttp
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import slugify
import voluptuous as vol
from .const import (
CONF_ACTIVE_PRINT_STATES,
CONF_CAMERA,
CONF_CHAMBER_LIGHT,
CONF_COOLDOWN_SECONDS,
CONF_DETECTION_INTERVAL,
CONF_FAILURE_THRESHOLD,
CONF_HOME_ASSISTANT_HOST,
CONF_INSTANCE_ID,
CONF_LIGHT_CONTROL_MODE,
CONF_LIGHT_SETTLE_SECONDS,
CONF_OBICO_AUTH_TOKEN,
CONF_OBICO_HOST,
CONF_PRINT_STATUS_SENSOR,
CONF_RUN_WITHOUT_PRINTING,
CONF_SENSITIVITY,
CONF_SNAPSHOT_URL,
CONF_WARNING_THRESHOLD,
DEFAULT_ACTIVE_PRINT_STATES,
DEFAULT_COOLDOWN_SECONDS,
DEFAULT_DETECTION_INTERVAL,
DEFAULT_FAILURE_THRESHOLD,
DEFAULT_HOME_ASSISTANT_HOST,
DEFAULT_INSTANCE_ID,
DEFAULT_LIGHT_CONTROL_MODE,
DEFAULT_LIGHT_SETTLE_SECONDS,
DEFAULT_NAME,
DEFAULT_OBICO_AUTH_TOKEN,
DEFAULT_OBICO_HOST,
DEFAULT_SENSITIVITY,
DEFAULT_WARNING_THRESHOLD,
DOMAIN,
LIGHT_CONTROL_LEAVE_ON,
LIGHT_CONTROL_OFF,
LIGHT_CONTROL_RESTORE,
)
OPTIONAL_ENTITY_FIELDS: tuple[tuple[str, str | list[str]], ...] = (
(CONF_PRINT_STATUS_SENSOR, ["sensor", "binary_sensor"]),
(CONF_CHAMBER_LIGHT, "light"),
)
def _entry_values(entry: ConfigEntry) -> dict[str, Any]:
"""Return config entry data with options overriding editable settings."""
return {**entry.data, **entry.options}
def _default_value(defaults: dict[str, Any], key: str, fallback: Any) -> Any:
"""Return a form default without leaking None into selectors."""
value = defaults.get(key)
return fallback if value is None else value
def _optional_marker(key: str, defaults: dict[str, Any]) -> vol.Optional:
"""Return an optional voluptuous marker with an existing default if present."""
if defaults.get(key):
return vol.Optional(key, default=defaults[key])
return vol.Optional(key)
def _light_control_mode(defaults: dict[str, Any]) -> str:
"""Return the default light-control mode for setup/options forms."""
mode = defaults.get(CONF_LIGHT_CONTROL_MODE)
if mode in {LIGHT_CONTROL_OFF, LIGHT_CONTROL_LEAVE_ON, LIGHT_CONTROL_RESTORE}:
return mode
return DEFAULT_LIGHT_CONTROL_MODE
def _schema(
defaults: dict[str, Any] | None = None,
*,
include_identity: bool,
) -> vol.Schema:
"""Return detector setup/options schema."""
defaults = defaults or {}
data_schema: dict[Any, Any] = {}
if include_identity:
data_schema[
vol.Required(
CONF_NAME,
default=_default_value(defaults, CONF_NAME, DEFAULT_NAME),
)
] = str
data_schema[
vol.Required(
CONF_INSTANCE_ID,
default=_default_value(
defaults,
CONF_INSTANCE_ID,
DEFAULT_INSTANCE_ID,
),
)
] = str
data_schema[
vol.Required(
CONF_HOME_ASSISTANT_HOST,
default=_default_value(
defaults,
CONF_HOME_ASSISTANT_HOST,
DEFAULT_HOME_ASSISTANT_HOST,
),
)
] = str
data_schema[
vol.Required(
CONF_OBICO_HOST,
default=_default_value(defaults, CONF_OBICO_HOST, DEFAULT_OBICO_HOST),
)
] = str
data_schema[
vol.Required(
CONF_OBICO_AUTH_TOKEN,
default=_default_value(
defaults,
CONF_OBICO_AUTH_TOKEN,
DEFAULT_OBICO_AUTH_TOKEN,
),
)
] = str
camera_marker = (
vol.Required(CONF_CAMERA, default=defaults[CONF_CAMERA])
if defaults.get(CONF_CAMERA)
else vol.Required(CONF_CAMERA)
)
data_schema[camera_marker] = selector.EntitySelector(
selector.EntitySelectorConfig(domain="camera")
)
data_schema[
vol.Optional(
CONF_SNAPSHOT_URL,
default=_default_value(defaults, CONF_SNAPSHOT_URL, ""),
)
] = str
for key, domain in OPTIONAL_ENTITY_FIELDS:
data_schema[_optional_marker(key, defaults)] = selector.EntitySelector(
selector.EntitySelectorConfig(domain=domain)
)
data_schema[
vol.Required(
CONF_ACTIVE_PRINT_STATES,
default=_default_value(
defaults,
CONF_ACTIVE_PRINT_STATES,
DEFAULT_ACTIVE_PRINT_STATES,
),
)
] = str
data_schema[
vol.Required(
CONF_LIGHT_CONTROL_MODE,
default=_light_control_mode(defaults),
)
] = selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
{"label": "Do not control light", "value": LIGHT_CONTROL_OFF},
{
"label": "Turn on before detection and leave on",
"value": LIGHT_CONTROL_LEAVE_ON,
},
{
"label": "Restore previous state after detection",
"value": LIGHT_CONTROL_RESTORE,
},
],
mode="dropdown",
)
)
data_schema[
vol.Required(
CONF_LIGHT_SETTLE_SECONDS,
default=_default_value(
defaults,
CONF_LIGHT_SETTLE_SECONDS,
DEFAULT_LIGHT_SETTLE_SECONDS,
),
)
] = selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=30,
step=1,
mode="box",
unit_of_measurement="s",
)
)
data_schema[
vol.Required(
CONF_RUN_WITHOUT_PRINTING,
default=_default_value(defaults, CONF_RUN_WITHOUT_PRINTING, False),
)
] = selector.BooleanSelector()
data_schema[
vol.Required(
CONF_DETECTION_INTERVAL,
default=_default_value(
defaults,
CONF_DETECTION_INTERVAL,
DEFAULT_DETECTION_INTERVAL,
),
)
] = selector.NumberSelector(
selector.NumberSelectorConfig(
min=5,
max=3600,
step=5,
mode="box",
unit_of_measurement="s",
)
)
data_schema[
vol.Required(
CONF_SENSITIVITY,
default=_default_value(defaults, CONF_SENSITIVITY, DEFAULT_SENSITIVITY),
)
] = selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
{"label": "High sensitivity", "value": "high"},
{"label": "Normal sensitivity", "value": "normal"},
{"label": "Low sensitivity", "value": "low"},
{"label": "Custom thresholds", "value": "custom"},
],
mode="dropdown",
)
)
data_schema[
vol.Required(
CONF_WARNING_THRESHOLD,
default=_default_value(
defaults,
CONF_WARNING_THRESHOLD,
DEFAULT_WARNING_THRESHOLD,
),
)
] = selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=1, step=0.01, mode="box")
)
data_schema[
vol.Required(
CONF_FAILURE_THRESHOLD,
default=_default_value(
defaults,
CONF_FAILURE_THRESHOLD,
DEFAULT_FAILURE_THRESHOLD,
),
)
] = selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=1, step=0.01, mode="box")
)
data_schema[
vol.Required(
CONF_COOLDOWN_SECONDS,
default=_default_value(
defaults,
CONF_COOLDOWN_SECONDS,
DEFAULT_COOLDOWN_SECONDS,
),
)
] = selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
max=3600,
step=5,
mode="box",
unit_of_measurement="s",
)
)
return vol.Schema(data_schema)
def _build_image_url(
hass,
data: dict[str, Any],
) -> str | None:
"""Build the image URL that the ML server will fetch during checks."""
if snapshot_url := data.get(CONF_SNAPSHOT_URL):
return snapshot_url
state = hass.states.get(data[CONF_CAMERA])
if state is None:
return None
entity_picture = state.attributes.get("entity_picture")
if not entity_picture:
return None
return f"{data[CONF_HOME_ASSISTANT_HOST].rstrip('/')}{entity_picture}"
def _validate_thresholds(data: dict[str, Any]) -> dict[str, str]:
"""Validate threshold fields."""
if float(data[CONF_WARNING_THRESHOLD]) > float(data[CONF_FAILURE_THRESHOLD]):
return {CONF_WARNING_THRESHOLD: "warning_above_failure"}
return {}
def _camera_in_use(
entries: list[ConfigEntry],
camera: str,
*,
exclude_entry_id: str | None = None,
) -> bool:
"""Return whether a camera is already used by a detector."""
return any(
entry.entry_id != exclude_entry_id
and _entry_values(entry).get(CONF_CAMERA) == camera
for entry in entries
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Elegoo spaghetti detection."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Configure one detector target."""
errors: dict[str, str] = {}
form_defaults = self._defaults_from_existing_entry()
if user_input is not None:
data = dict(user_input)
data[CONF_INSTANCE_ID] = slugify(data[CONF_INSTANCE_ID])
errors.update(_validate_thresholds(data))
if not data[CONF_INSTANCE_ID]:
errors[CONF_INSTANCE_ID] = "invalid_instance_id"
elif self._instance_id_exists(data[CONF_INSTANCE_ID]):
errors[CONF_INSTANCE_ID] = "instance_id_exists"
elif not errors:
await self.async_set_unique_id(data[CONF_CAMERA])
self._abort_if_unique_id_configured()
if not errors:
errors.update(await self._async_validate_backend(data))
if not errors:
name = data.pop(CONF_NAME)
return self.async_create_entry(title=name, data=data)
form_defaults = {**form_defaults, **data}
return self.async_show_form(
step_id="user",
data_schema=_schema(form_defaults, include_identity=True),
errors=errors,
)
def _defaults_from_existing_entry(self) -> dict[str, Any]:
"""Use the first existing detector to reduce repeated server entry."""
for entry in self._async_current_entries():
values = _entry_values(entry)
defaults = {
CONF_HOME_ASSISTANT_HOST: values.get(CONF_HOME_ASSISTANT_HOST),
CONF_OBICO_HOST: values.get(CONF_OBICO_HOST),
CONF_OBICO_AUTH_TOKEN: values.get(CONF_OBICO_AUTH_TOKEN),
CONF_DETECTION_INTERVAL: values.get(CONF_DETECTION_INTERVAL),
CONF_LIGHT_CONTROL_MODE: values.get(CONF_LIGHT_CONTROL_MODE),
CONF_LIGHT_SETTLE_SECONDS: values.get(CONF_LIGHT_SETTLE_SECONDS),
CONF_SENSITIVITY: values.get(CONF_SENSITIVITY),
CONF_WARNING_THRESHOLD: values.get(CONF_WARNING_THRESHOLD),
CONF_FAILURE_THRESHOLD: values.get(CONF_FAILURE_THRESHOLD),
CONF_COOLDOWN_SECONDS: values.get(CONF_COOLDOWN_SECONDS),
}
return {key: value for key, value in defaults.items() if value is not None}
return {CONF_HOME_ASSISTANT_HOST: self._home_assistant_url_default()}
def _home_assistant_url_default(self) -> str:
"""Return the best available HA URL for the ML server to fetch images."""
return (
getattr(self.hass.config, "internal_url", None)
or getattr(self.hass.config, "external_url", None)
or DEFAULT_HOME_ASSISTANT_HOST
)
def _instance_id_exists(self, instance_id: str) -> bool:
"""Return whether an entity prefix is already used."""
return any(
entry.data.get(CONF_INSTANCE_ID) == instance_id
for entry in self._async_current_entries()
)
def _camera_exists(self, camera: str) -> bool:
"""Return whether a camera is already used by another detector."""
return _camera_in_use(self._async_current_entries(), camera)
async def _async_validate_backend(self, data: dict[str, Any]) -> dict[str, str]:
"""Validate ML health and whether it can fetch the configured image."""
return await _async_validate_backend(self.hass, data)
class OptionsFlowHandler(config_entries.OptionsFlowWithReload):
"""Handle detector options."""
async def async_step_init(
self,
user_input: dict[str, Any] | None = None,
) -> FlowResult:
"""Manage detector options."""
errors: dict[str, str] = {}
defaults = _entry_values(self.config_entry)
if user_input is not None:
data = dict(user_input)
errors.update(_validate_thresholds(data))
if _camera_in_use(
self.hass.config_entries.async_entries(DOMAIN),
data[CONF_CAMERA],
exclude_entry_id=self.config_entry.entry_id,
):
errors[CONF_CAMERA] = "already_configured"
if not errors:
errors.update(await _async_validate_backend(self.hass, data))
if not errors:
return self.async_create_entry(data=data)
defaults = {**defaults, **data}
return self.async_show_form(
step_id="init",
data_schema=_schema(defaults, include_identity=False),
errors=errors,
)
async def _async_validate_backend(hass, data: dict[str, Any]) -> dict[str, str]:
"""Return form errors for backend/camera connectivity problems."""
image_url = _build_image_url(hass, data)
if not image_url:
return {CONF_CAMERA: "camera_image_unavailable"}
session = async_get_clientsession(hass)
obico_host = data[CONF_OBICO_HOST].rstrip("/")
token = data[CONF_OBICO_AUTH_TOKEN]
headers = {"Authorization": f"Bearer {token}"}
try:
async with session.get(
f"{obico_host}/hc/",
timeout=aiohttp.ClientTimeout(total=10),
) as response:
if response.status >= 400:
return {CONF_OBICO_HOST: "ml_health_failed"}
except (aiohttp.ClientError, TimeoutError):
return {CONF_OBICO_HOST: "ml_health_failed"}
try:
async with session.get(
f"{obico_host}/debug/image",
params={"img": image_url},
headers=headers,
timeout=aiohttp.ClientTimeout(total=20),
) as response:
if response.status == 401:
return {CONF_OBICO_AUTH_TOKEN: "ml_auth_failed"}
if response.status >= 400:
return {CONF_CAMERA: "ml_image_fetch_failed"}
except (aiohttp.ClientError, TimeoutError):
return {CONF_CAMERA: "ml_image_fetch_failed"}
return {}
@@ -0,0 +1,86 @@
"""Constants for Elegoo spaghetti detection."""
from homeassistant.const import Platform
DOMAIN = "elegoo_spaghetti_detection"
BRAND = "Elegoo Spaghetti Detection"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.BUTTON]
CONF_INSTANCE_ID = "instance_id"
CONF_HOME_ASSISTANT_HOST = "home_assistant_host"
CONF_OBICO_HOST = "obico_host"
CONF_OBICO_AUTH_TOKEN = "obico_auth_token"
CONF_CAMERA = "camera"
CONF_SNAPSHOT_URL = "snapshot_url"
CONF_PRINT_STATUS_SENSOR = "print_status_sensor"
CONF_ACTIVE_PRINT_STATES = "active_print_states"
CONF_CHAMBER_LIGHT = "chamber_light"
CONF_LIGHT_CONTROL_MODE = "light_control_mode"
CONF_LIGHT_SETTLE_SECONDS = "light_settle_seconds"
CONF_DETECTION_INTERVAL = "detection_interval"
CONF_RUN_WITHOUT_PRINTING = "run_without_printing"
CONF_FAILURE_THRESHOLD = "failure_threshold"
CONF_WARNING_THRESHOLD = "warning_threshold"
CONF_SENSITIVITY = "sensitivity"
CONF_COOLDOWN_SECONDS = "cooldown_seconds"
CONF_IMAGE_URL = "image_url"
CONF_CONFIG_ENTRY = "config_entry"
CONF_DETECTOR = "detector"
CONF_FORCE = "force"
DEFAULT_NAME = "Elegoo Spaghetti Detector"
DEFAULT_INSTANCE_ID = DOMAIN
DEFAULT_HOME_ASSISTANT_HOST = "http://homeassistant.local:8123"
DEFAULT_OBICO_HOST = "http://192.168.1.123:3333"
DEFAULT_OBICO_AUTH_TOKEN = "obico_api_secret"
DEFAULT_ACTIVE_PRINT_STATES = "printing"
DEFAULT_DETECTION_INTERVAL = 10
DEFAULT_COOLDOWN_SECONDS = 900
DEFAULT_FAILURE_THRESHOLD = 0.50
DEFAULT_WARNING_THRESHOLD = 0.30
DEFAULT_SENSITIVITY = "normal"
DEFAULT_LIGHT_CONTROL_MODE = "restore"
DEFAULT_LIGHT_SETTLE_SECONDS = 3
LIGHT_CONTROL_OFF = "off"
LIGHT_CONTROL_LEAVE_ON = "leave_on"
LIGHT_CONTROL_RESTORE = "restore"
REQUIRED_CONFIG_KEYS = frozenset(
{
CONF_INSTANCE_ID,
CONF_HOME_ASSISTANT_HOST,
CONF_OBICO_HOST,
CONF_OBICO_AUTH_TOKEN,
CONF_CAMERA,
}
)
SENSITIVITY_THRESHOLDS = {
"high": (0.20, 0.35),
"normal": (DEFAULT_WARNING_THRESHOLD, DEFAULT_FAILURE_THRESHOLD),
"low": (0.45, 0.70),
"custom": (DEFAULT_WARNING_THRESHOLD, DEFAULT_FAILURE_THRESHOLD),
}
EVENT_DETECTION_RESULT = f"{DOMAIN}_result"
EVENT_SPAGHETTI_DETECTED = f"{DOMAIN}_detected"
SERVICE_PREDICT = "predict"
SERVICE_RUN_DETECTION = "run_detection"
SERVICE_RESET_STATE = "reset_state"
RUNTIME_DATA = "runtime"
RUNTIME_BY_DETECTOR = "runtime_by_detector"
RUNTIME_ML_LOCK = "ml_lock"
ATTR_CONFIDENCE = "confidence"
ATTR_RAW_SCORE = "raw_score"
ATTR_DETECTED = "detected"
ATTR_DETECTIONS = "detections"
ATTR_IMAGE_URL = "image_url"
ATTR_LAST_ERROR = "last_error"
ATTR_LAST_RUN = "last_run"
ATTR_NEXT_RUN = "next_run"
ATTR_STATUS = "status"
@@ -0,0 +1,36 @@
"""Base entities for Elegoo spaghetti detection."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN
from .runtime import SpaghettiDetectorRuntime
class SpaghettiDetectorEntity(Entity):
"""Base entity for a detector runtime."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
self,
entry: ConfigEntry,
runtime: SpaghettiDetectorRuntime,
key: str,
) -> None:
self.entry = entry
self.runtime = runtime
self._attr_unique_id = f"{entry.entry_id}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Elegoo",
model="Spaghetti detection",
name=entry.title,
)
async def async_added_to_hass(self) -> None:
"""Subscribe to runtime updates."""
self.async_on_remove(
self.runtime.async_add_listener(self.async_write_ha_state)
)
@@ -0,0 +1,16 @@
{
"domain": "elegoo_spaghetti_detection",
"name": "Elegoo Spaghetti Detection",
"codeowners": [
"@hepter"
],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/hepter/ha-elegoo-spaghetti-detection",
"integration_type": "hub",
"iot_class": "calculated",
"issue_tracker": "https://github.com/hepter/ha-elegoo-spaghetti-detection/issues",
"requirements": [],
"version": "1.0.0"
}
@@ -0,0 +1,583 @@
"""Runtime detection logic for Elegoo spaghetti detection."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import datetime, timedelta
import logging
from typing import Any
import aiohttp
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.util import dt as dt_util
from .const import (
ATTR_CONFIDENCE,
ATTR_DETECTED,
ATTR_DETECTIONS,
ATTR_IMAGE_URL,
ATTR_LAST_ERROR,
ATTR_LAST_RUN,
ATTR_NEXT_RUN,
ATTR_RAW_SCORE,
ATTR_STATUS,
CONF_ACTIVE_PRINT_STATES,
CONF_CAMERA,
CONF_CHAMBER_LIGHT,
CONF_COOLDOWN_SECONDS,
CONF_DETECTION_INTERVAL,
CONF_FAILURE_THRESHOLD,
CONF_HOME_ASSISTANT_HOST,
CONF_INSTANCE_ID,
CONF_LIGHT_CONTROL_MODE,
CONF_LIGHT_SETTLE_SECONDS,
CONF_OBICO_AUTH_TOKEN,
CONF_OBICO_HOST,
CONF_PRINT_STATUS_SENSOR,
CONF_RUN_WITHOUT_PRINTING,
CONF_SENSITIVITY,
CONF_SNAPSHOT_URL,
CONF_WARNING_THRESHOLD,
DEFAULT_ACTIVE_PRINT_STATES,
DEFAULT_COOLDOWN_SECONDS,
DEFAULT_DETECTION_INTERVAL,
DEFAULT_FAILURE_THRESHOLD,
DEFAULT_LIGHT_CONTROL_MODE,
DEFAULT_LIGHT_SETTLE_SECONDS,
DEFAULT_SENSITIVITY,
DEFAULT_WARNING_THRESHOLD,
DOMAIN,
EVENT_DETECTION_RESULT,
EVENT_SPAGHETTI_DETECTED,
LIGHT_CONTROL_LEAVE_ON,
LIGHT_CONTROL_OFF,
LIGHT_CONTROL_RESTORE,
RUNTIME_ML_LOCK,
SENSITIVITY_THRESHOLDS,
)
LOGGER = logging.getLogger(__name__)
def _parse_states(value: str | None) -> set[str]:
"""Parse a comma-separated list of states."""
if not value:
value = DEFAULT_ACTIVE_PRINT_STATES
return {item.strip().lower() for item in value.split(",") if item.strip()}
def _normalize_state(value: Any) -> str:
"""Normalize a Home Assistant state string for comparisons."""
return str(value).strip().lower()
def _score_detections(result: dict[str, Any]) -> tuple[float, int]:
"""Return a simple confidence score from the Obico detection payload."""
score = 0.0
detections = result.get("detections") or []
for detection in detections:
try:
score += float(detection[1])
except (TypeError, ValueError, IndexError):
continue
return min(1.0, max(0.0, score)), len(detections)
class SpaghettiDetectorRuntime:
"""Manage one camera/detector target."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
self.hass = hass
self.entry = entry
self.data = {**entry.data, **entry.options}
self.detector_id: str = self.data[CONF_INSTANCE_ID]
self.name = entry.title
self.listeners: list[Callable[[], None]] = []
self.unsubscribers: list[CALLBACK_TYPE] = []
self.enabled = True
self.running = False
self.status = "idle"
self.printer_state: str | None = None
self.confidence = 0.0
self.raw_score = 0.0
self.detection_count = 0
self.detected = False
self.warning = False
self.last_run: datetime | None = None
self.last_detected: datetime | None = None
self.next_run: datetime | None = None
self.last_error: str | None = None
self.last_image_url: str | None = None
self.last_result: dict[str, Any] = {"detections": []}
self.lifetime_frames = 0
self.detected_event_sent_for_active_period = False
@property
def active_states(self) -> set[str]:
"""Return states that mean the printer is actively printing."""
return _parse_states(self.data.get(CONF_ACTIVE_PRINT_STATES))
@property
def warning_threshold(self) -> float:
"""Return warning threshold for this detector."""
sensitivity = self.data.get(CONF_SENSITIVITY, DEFAULT_SENSITIVITY)
default_warning, _ = SENSITIVITY_THRESHOLDS.get(
sensitivity,
SENSITIVITY_THRESHOLDS[DEFAULT_SENSITIVITY],
)
if sensitivity != "custom":
return float(default_warning)
return float(self.data.get(CONF_WARNING_THRESHOLD, default_warning))
@property
def failure_threshold(self) -> float:
"""Return failure threshold for this detector."""
sensitivity = self.data.get(CONF_SENSITIVITY, DEFAULT_SENSITIVITY)
_, default_failure = SENSITIVITY_THRESHOLDS.get(
sensitivity,
SENSITIVITY_THRESHOLDS[DEFAULT_SENSITIVITY],
)
if sensitivity != "custom":
return float(default_failure)
return float(self.data.get(CONF_FAILURE_THRESHOLD, default_failure))
@property
def cooldown(self) -> timedelta:
"""Return notification/action cooldown."""
return timedelta(
seconds=int(self.data.get(CONF_COOLDOWN_SECONDS, DEFAULT_COOLDOWN_SECONDS))
)
@property
def detection_interval(self) -> timedelta:
"""Return scheduled detection interval."""
return timedelta(
seconds=int(
self.data.get(CONF_DETECTION_INTERVAL, DEFAULT_DETECTION_INTERVAL)
)
)
@property
def light_control_mode(self) -> str:
"""Return how the detector should manage the configured light."""
mode = self.data.get(CONF_LIGHT_CONTROL_MODE, DEFAULT_LIGHT_CONTROL_MODE)
if mode in {LIGHT_CONTROL_OFF, LIGHT_CONTROL_LEAVE_ON, LIGHT_CONTROL_RESTORE}:
return mode
return DEFAULT_LIGHT_CONTROL_MODE
@property
def light_settle_seconds(self) -> int:
"""Return seconds to wait after turning on a light before snapshot."""
try:
seconds = int(
float(
self.data.get(
CONF_LIGHT_SETTLE_SECONDS,
DEFAULT_LIGHT_SETTLE_SECONDS,
)
)
)
except (TypeError, ValueError):
seconds = DEFAULT_LIGHT_SETTLE_SECONDS
return max(0, seconds)
async def async_setup(self) -> None:
"""Start scheduled detection."""
interval = self.detection_interval
self.next_run = dt_util.utcnow() + interval
self.unsubscribers.append(
async_track_time_interval(
self.hass,
self._async_interval_update,
interval,
)
)
if status_entity := self.data.get(CONF_PRINT_STATUS_SENSOR):
status_entities = [status_entity]
if guard_entity := self._inferred_guard_entity(status_entity):
status_entities.append(guard_entity)
self.unsubscribers.append(
async_track_state_change_event(
self.hass,
status_entities,
self._async_status_changed,
)
)
async def async_unload(self) -> None:
"""Stop scheduled detection."""
for unsubscribe in self.unsubscribers:
unsubscribe()
self.unsubscribers.clear()
self.listeners.clear()
@callback
def async_add_listener(self, listener: Callable[[], None]) -> CALLBACK_TYPE:
"""Add a listener for runtime state changes."""
self.listeners.append(listener)
@callback
def remove_listener() -> None:
self.listeners.remove(listener)
return remove_listener
@callback
def _notify_listeners(self) -> None:
"""Notify entities that runtime state changed."""
for listener in list(self.listeners):
listener()
async def _async_interval_update(self, now: datetime) -> None:
"""Run detection on interval if the target is active."""
self.next_run = now + self.detection_interval
self._notify_listeners()
if self.enabled and self._should_run_scheduled():
await self.async_run_detection(manual=False)
@callback
def _async_status_changed(self, event) -> None:
"""Reset state when a new print starts."""
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
if new_state is None:
return
was_active = (
old_state is not None
and _normalize_state(old_state.state) in self.active_states
)
is_active = _normalize_state(new_state.state) in self.active_states
if was_active and not is_active:
self.detected_event_sent_for_active_period = False
if is_active and not was_active:
self.reset()
def _should_run_scheduled(self) -> bool:
"""Return if scheduled detection should run."""
status_entity = self.data.get(CONF_PRINT_STATUS_SENSOR)
if not status_entity:
self.printer_state = None
if bool(self.data.get(CONF_RUN_WITHOUT_PRINTING, False)):
return True
self.status = "waiting_for_print"
self._notify_listeners()
return False
state = self.hass.states.get(status_entity)
if state is None:
self.status = "status_unavailable"
self.printer_state = None
self._notify_listeners()
return False
self.printer_state = str(state.state)
normalized_state = _normalize_state(state.state)
if normalized_state not in self.active_states:
self.status = (
"status_unavailable"
if normalized_state in {"unknown", "unavailable"}
else "waiting_for_print"
)
self._notify_listeners()
return False
if not self._passes_inferred_guard_sensor(status_entity):
if self.status != "status_unavailable":
self.status = "waiting_for_print"
self._notify_listeners()
return False
self._notify_listeners()
return True
def _passes_inferred_guard_sensor(self, status_entity: str) -> bool:
"""Return false when an inferred companion status says not active."""
guard_entity = self._inferred_guard_entity(status_entity)
if guard_entity is None:
return True
state = self.hass.states.get(guard_entity)
if state is None:
return True
self.printer_state = f"{self.printer_state}; {guard_entity}={state.state}"
normalized_state = _normalize_state(state.state)
if normalized_state in {"unknown", "unavailable"}:
self.status = "status_unavailable"
return False
return normalized_state in self.active_states
def _inferred_guard_entity(self, status_entity: str) -> str | None:
"""Infer an Elegoo companion current-status sensor when available."""
suffix = "_print_status"
if not status_entity.endswith(suffix):
return None
candidate = f"{status_entity[: -len(suffix)]}_current_status"
if candidate == status_entity:
return None
return candidate
def reset(self) -> None:
"""Reset detection state."""
self.status = "idle"
self.confidence = 0.0
self.raw_score = 0.0
self.detection_count = 0
self.detected = False
self.warning = False
self.last_detected = None
self.last_error = None
self.last_result = {"detections": []}
self.lifetime_frames = 0
self.detected_event_sent_for_active_period = False
self._notify_listeners()
async def async_run_detection(self, *, manual: bool) -> dict[str, Any]:
"""Run one detection request."""
if self.running:
self.status = "busy"
self._notify_listeners()
return self._service_result()
if not manual and not self._should_run_scheduled():
return self._service_result()
self.running = True
self.status = "checking"
self.last_run = dt_util.utcnow()
self.last_error = None
self._notify_listeners()
restore_light: str | None = None
try:
restore_light = await self._async_prepare_light()
if not manual and not self._should_run_scheduled():
return self._service_result()
image_url = self._build_image_url()
if not image_url:
self._set_error("camera_image_unavailable")
return self._service_result()
self.last_image_url = image_url
try:
ml_lock = self.hass.data[DOMAIN][RUNTIME_ML_LOCK]
async with ml_lock:
result = await self._async_predict(image_url)
except (aiohttp.ClientError, TimeoutError) as err:
self._set_error(str(err))
LOGGER.warning(
"Obico ML request failed for %s: %s",
self.detector_id,
err,
)
return self._service_result()
self.last_result = result
self.raw_score, self.detection_count = _score_detections(result)
self.confidence = self.raw_score
self.warning = self.confidence >= self.warning_threshold
self.detected = self.confidence >= self.failure_threshold
self.status = (
"detected" if self.detected else "warning" if self.warning else "clear"
)
self.lifetime_frames += 1
self._fire_result_event(manual)
if self.detected and self._can_fire_detected_event(manual):
self.last_detected = dt_util.utcnow()
if not manual and self.data.get(CONF_PRINT_STATUS_SENSOR):
self.detected_event_sent_for_active_period = True
self._fire_detected_event(manual)
self._notify_listeners()
return self._service_result()
finally:
if restore_light is not None:
await self._async_restore_light(restore_light)
self.running = False
async def _async_predict(self, image_url: str) -> dict[str, Any]:
"""Call the Obico ML API."""
session = async_get_clientsession(self.hass)
async with session.get(
f"{self.data[CONF_OBICO_HOST].rstrip('/')}/p/",
params={"img": image_url},
headers={"Authorization": f"Bearer {self.data[CONF_OBICO_AUTH_TOKEN]}"},
timeout=aiohttp.ClientTimeout(total=60),
) as response:
if response.status >= 400:
error_message = await _response_error_message(response)
raise aiohttp.ClientResponseError(
response.request_info,
response.history,
status=response.status,
message=error_message,
headers=response.headers,
)
response.raise_for_status()
result = await response.json()
if not isinstance(result, dict):
return {"detections": []}
return result
async def _async_prepare_light(self) -> str | None:
"""Prepare the configured light before detection.
Returns the entity ID to restore when the integration turned an off
light on and the selected mode wants the previous state restored.
"""
if self.light_control_mode == LIGHT_CONTROL_OFF:
return None
light_entity = self.data.get(CONF_CHAMBER_LIGHT)
if not light_entity:
return None
state = self.hass.states.get(light_entity)
if state is None or _normalize_state(state.state) != "off":
return None
try:
await self.hass.services.async_call(
"light",
"turn_on",
{"entity_id": light_entity},
blocking=True,
)
except HomeAssistantError as err:
LOGGER.warning("Could not turn on light %s: %s", light_entity, err)
return None
restore_light = (
light_entity if self.light_control_mode == LIGHT_CONTROL_RESTORE else None
)
try:
if self.light_settle_seconds:
await asyncio.sleep(self.light_settle_seconds)
except asyncio.CancelledError:
if restore_light is not None:
await self._async_restore_light(restore_light)
raise
return restore_light
async def _async_restore_light(self, light_entity: str) -> None:
"""Restore a light that the detector temporarily turned on."""
state = self.hass.states.get(light_entity)
if state is not None and _normalize_state(state.state) == "off":
return
try:
await self.hass.services.async_call(
"light",
"turn_off",
{"entity_id": light_entity},
blocking=True,
)
except HomeAssistantError as err:
LOGGER.warning("Could not restore light %s: %s", light_entity, err)
def _build_image_url(self) -> str | None:
"""Build a snapshot URL for the configured camera."""
if snapshot_url := self.data.get(CONF_SNAPSHOT_URL):
return snapshot_url
camera_entity = self.data.get(CONF_CAMERA)
state = self.hass.states.get(camera_entity)
if state is None:
return None
entity_picture = state.attributes.get("entity_picture")
if not entity_picture:
return None
return f"{self.data[CONF_HOME_ASSISTANT_HOST].rstrip('/')}{entity_picture}"
def _cooldown_elapsed(self) -> bool:
"""Return whether a detected event can be fired."""
if self.last_detected is None:
return True
return dt_util.utcnow() - self.last_detected >= self.cooldown
def _can_fire_detected_event(self, manual: bool) -> bool:
"""Return whether the detected event should be emitted."""
if not manual and self.data.get(CONF_PRINT_STATUS_SENSOR):
return not self.detected_event_sent_for_active_period
return self._cooldown_elapsed()
def _event_data(self, manual: bool) -> dict[str, Any]:
"""Return event payload."""
return {
"config_entry": self.entry.entry_id,
"detector": self.detector_id,
"name": self.name,
"camera": self.data.get(CONF_CAMERA),
"manual": manual,
"printer_state": self.printer_state,
ATTR_CONFIDENCE: self.confidence,
ATTR_RAW_SCORE: self.raw_score,
ATTR_DETECTED: self.detected,
ATTR_DETECTIONS: self.detection_count,
ATTR_IMAGE_URL: self.last_image_url,
ATTR_LAST_ERROR: self.last_error,
ATTR_LAST_RUN: self.last_run.isoformat() if self.last_run else None,
ATTR_NEXT_RUN: self.next_run.isoformat() if self.next_run else None,
ATTR_STATUS: self.status,
}
def _fire_result_event(self, manual: bool) -> None:
"""Fire an event for every detection result."""
self.hass.bus.async_fire(EVENT_DETECTION_RESULT, self._event_data(manual))
def _fire_detected_event(self, manual: bool) -> None:
"""Fire an event when spaghetti is detected."""
self.hass.bus.async_fire(EVENT_SPAGHETTI_DETECTED, self._event_data(manual))
def _service_result(self) -> dict[str, Any]:
"""Return service response payload."""
return {
"result": self.last_result,
ATTR_CONFIDENCE: self.confidence,
ATTR_RAW_SCORE: self.raw_score,
ATTR_DETECTED: self.detected,
ATTR_DETECTIONS: self.detection_count,
ATTR_IMAGE_URL: self.last_image_url,
ATTR_LAST_ERROR: self.last_error,
ATTR_NEXT_RUN: self.next_run.isoformat() if self.next_run else None,
ATTR_STATUS: self.status,
}
def _set_error(self, error: str) -> None:
"""Set a runtime error and notify listeners."""
self.status = "error"
self.last_error = error
self.detected = False
self.warning = False
self.confidence = 0.0
self.raw_score = 0.0
self.detection_count = 0
self._notify_listeners()
async def _response_error_message(response: aiohttp.ClientResponse) -> str:
"""Return a useful error message from an ML server error response."""
try:
payload = await response.json()
except (aiohttp.ContentTypeError, ValueError):
return await response.text()
if not isinstance(payload, dict):
return str(payload)
if error := payload.get("error"):
message = payload.get("message")
return f"{error}: {message}" if message else str(error)
return str(payload)
@@ -0,0 +1,106 @@
"""Sensors for Elegoo spaghetti detection."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from .const import CONF_INSTANCE_ID, DOMAIN, RUNTIME_DATA
from .entity import SpaghettiDetectorEntity
@dataclass(frozen=True, kw_only=True)
class DetectorSensorDescription(SensorEntityDescription):
"""Detector sensor description."""
value_fn: Any
SENSORS: tuple[DetectorSensorDescription, ...] = (
DetectorSensorDescription(
key="confidence",
name="Confidence",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda runtime: round(runtime.confidence * 100, 1),
),
DetectorSensorDescription(
key="raw_score",
name="Raw Score",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda runtime: round(runtime.raw_score, 4),
),
DetectorSensorDescription(
key="detections",
name="Detection Count",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda runtime: runtime.detection_count,
),
DetectorSensorDescription(
key="status",
name="Status",
value_fn=lambda runtime: runtime.status,
),
DetectorSensorDescription(
key="last_error",
name="Last Error",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda runtime: runtime.last_error or "none",
),
DetectorSensorDescription(
key="last_run",
name="Last Run",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda runtime: runtime.last_run,
),
DetectorSensorDescription(
key="next_run",
name="Next Run",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda runtime: runtime.next_run,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities,
) -> None:
"""Set up sensors."""
runtime = hass.data[DOMAIN][RUNTIME_DATA][entry.entry_id]
async_add_entities(
DetectorSensor(entry, runtime, description) for description in SENSORS
)
class DetectorSensor(SpaghettiDetectorEntity, SensorEntity):
"""Detector sensor."""
entity_description: DetectorSensorDescription
def __init__(
self,
entry: ConfigEntry,
runtime,
description: DetectorSensorDescription,
) -> None:
super().__init__(entry, runtime, description.key)
self.entity_description = description
self.entity_id = f"sensor.{entry.data[CONF_INSTANCE_ID]}_{description.key}"
@property
def native_value(self):
"""Return the current sensor value."""
return self.entity_description.value_fn(self.runtime)
@@ -0,0 +1,60 @@
predict:
name: "Predict spaghetti from URL"
description: "Runs the Obico ML model against a raw image URL. This is mainly for debugging."
fields:
obico_host:
description: "Obico ML Server URL."
example: "http://192.168.1.123:3333"
required: true
selector:
text:
obico_auth_token:
description: "Obico ML Server authentication token."
example: "obico_api_secret"
required: true
selector:
text:
image_url:
description: "Snapshot URL to check."
example: "https://home.example.com/api/camera_proxy/camera.example?token=..."
required: true
selector:
text:
run_detection:
name: "Run detection"
description: "Runs one detection check for a configured detector. With force enabled this works even when the printer is not printing."
fields:
detector:
description: "Detector/entity prefix, for example elegoo_spaghetti_detection."
required: false
selector:
text:
config_entry:
description: "Detector config entry."
required: false
selector:
config_entry:
integration: elegoo_spaghetti_detection
force:
description: "Run as a manual test and bypass the print-status gate."
required: false
default: true
selector:
boolean:
reset_state:
name: "Reset detection state"
description: "Clears the current detector confidence, result, and error state."
fields:
detector:
description: "Detector/entity prefix."
required: false
selector:
text:
config_entry:
description: "Detector config entry."
required: false
selector:
config_entry:
integration: elegoo_spaghetti_detection
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "ينشئ كاشفا لكاميرا طابعة Elegoo. هذا التكامل يكتشف الاخطاء فقط وينشئ entities/events؛ تبقى اجراءات pause و stop والتنبيهات داخل automations الخاصة بك.",
"data": {
"name": "اسم الكاشف",
"instance_id": "بادئة entity",
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "الكاميرا",
"snapshot_url": "رابط snapshot مباشر",
"print_status_sensor": "حساس حالة الطباعة",
"active_print_states": "حالات الطباعة النشطة",
"chamber_light": "ضوء الحجرة",
"light_control_mode": "التحكم بالضوء",
"light_settle_seconds": "تأخير استقرار الضوء",
"run_without_printing": "تشغيل الكشف المجدول بدون حالة طباعة",
"detection_interval": "فاصل الكشف",
"sensitivity": "الحساسية",
"warning_threshold": "حد التحذير",
"failure_threshold": "حد الفشل",
"cooldown_seconds": "فترة تهدئة حدث detected"
},
"data_description": {
"home_assistant_host": "URL يمكن لخادم ML الوصول اليه. عند تشغيل Docker على مضيف LAN اخر، استخدم HA LAN URL مثل http://192.168.1.90:8123.",
"obico_host": "Base URL لخادم ML الخاص بهذا المشروع، مثل http://192.168.1.100:3333. يقوم الاعداد بفحص /hc/ و /debug/image.",
"obico_auth_token": "يجب ان يطابق ML_API_TOKEN / obico_api_secret المكون على خادم ML.",
"instance_id": "Slug ثابت يستخدم في entity IDs. استخدم بادئة مختلفة لكل كاشف، مثل elegoo_cc2_left.",
"camera": "اي HA camera entity. مثال من elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. قد يختلف اسم جهازك.",
"snapshot_url": "اختياري. اتركه فارغا لاستخدام صورة Home Assistant camera proxy للكاميرا المحددة. استخدمه فقط للكاميرات غير المعتادة.",
"print_status_sensor": "اختياري لكن موصى به. مثال: sensor.elegoo_centauri_carbon2_print_status. للاسماء بأسلوب Elegoo، يتم استخدام حساس current_status المطابق تلقائيا كحارس اضافي.",
"active_print_states": "حالات مفصولة بفواصل تعني الطباعة، مثل printing,printing_recovery. تستخدم Elegoo CC2 عادة printing.",
"chamber_light": "اختياري. مثال: light.elegoo_centauri_carbon2_chamber_light. يستخدم فقط بواسطة اعداد التحكم بالضوء.",
"light_control_mode": "اختر ما اذا كان الكشف لا يتحكم بالضوء، او يشغله ويبقيه مشغلا، او يعيد حالة الضوء السابقة بعد كل snapshot.",
"light_settle_seconds": "عدد الثواني للانتظار بعد ان يشغل التكامل ضوءا كان مطفأ قبل اخذ snapshot. الافتراضي 3 ثوان للتعريض/التركيز.",
"run_without_printing": "اذا لم يتم تحديد حساس حالة طباعة، يعمل الكشف المجدول فقط عند تفعيل هذا الخيار. زر Test يشغل فحصا واحدا دائما.",
"detection_interval": "عدد الثواني بين الفحوصات المجدولة عندما تكون حالة الطباعة نشطة. امثلة: 600 لعشر دقائق، 900 لخمس عشرة دقيقة.",
"warning_threshold": "يستخدم عندما تكون الحساسية Custom thresholds.",
"failure_threshold": "يستخدم عندما تكون الحساسية Custom thresholds.",
"cooldown_seconds": "الافتراضي 900 ثانية. مع حساس حالة طباعة، ترسل الفحوصات المجدولة حدث detected واحدا لكل نافذة طباعة نشطة؛ وتستمر result events في الارسال مع كل فحص."
}
}
},
"error": {
"invalid_instance_id": "يجب ان تحتوي بادئة entity على حرف slug صالح واحد على الاقل.",
"instance_id_exists": "بادئة entity هذه مستخدمة بالفعل بواسطة كاشف اخر.",
"warning_above_failure": "يجب ان يكون حد التحذير اقل من حد الفشل او مساويا له.",
"already_configured": "هذه الكاميرا مكونة بالفعل.",
"camera_image_unavailable": "الكاميرا المحددة لا تعرض entity_picture URL. جرب كاميرا اخرى او اضبط snapshot URL مباشر.",
"ml_health_failed": "فشل فحص صحة خادم ML. تأكد ان المضيف قابل للوصول ويشير الى base URL مثل http://192.168.1.100:3333.",
"ml_auth_failed": "رفض خادم ML الرمز.",
"ml_image_fetch_failed": "تعذر على خادم ML جلب صورة الكاميرا او فك ترميزها. تحقق من Home Assistant Host ووصول الكاميرا من خادم ML."
},
"abort": {
"already_configured": "هذه الكاميرا مكونة بالفعل."
}
},
"options": {
"step": {
"init": {
"title": "اعدادات الكاشف",
"description": "حدث اعدادات الكاميرا وخادم ML وحالة الطباعة والكشف لهذا الكاشف.",
"data": {
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "الكاميرا",
"snapshot_url": "رابط snapshot مباشر",
"print_status_sensor": "حساس حالة الطباعة",
"active_print_states": "حالات الطباعة النشطة",
"chamber_light": "ضوء الحجرة",
"light_control_mode": "التحكم بالضوء",
"light_settle_seconds": "تأخير استقرار الضوء",
"run_without_printing": "تشغيل الكشف المجدول بدون حالة طباعة",
"detection_interval": "فاصل الكشف",
"sensitivity": "الحساسية",
"warning_threshold": "حد التحذير",
"failure_threshold": "حد الفشل",
"cooldown_seconds": "فترة تهدئة حدث detected"
},
"data_description": {
"home_assistant_host": "URL يمكن لخادم ML الوصول اليه. عند تشغيل Docker على مضيف LAN اخر، استخدم HA LAN URL مثل http://192.168.1.90:8123.",
"obico_host": "Base URL لخادم ML الخاص بهذا المشروع، مثل http://192.168.1.100:3333. يقوم الاعداد بفحص /hc/ و /debug/image.",
"obico_auth_token": "يجب ان يطابق ML_API_TOKEN / obico_api_secret المكون على خادم ML.",
"camera": "اي HA camera entity. مثال من elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. قد يختلف اسم جهازك.",
"snapshot_url": "اختياري. اتركه فارغا لاستخدام صورة Home Assistant camera proxy للكاميرا المحددة.",
"print_status_sensor": "اختياري لكن موصى به. مثال: sensor.elegoo_centauri_carbon2_print_status. للاسماء بأسلوب Elegoo، يتم استخدام حساس current_status المطابق تلقائيا كحارس اضافي.",
"active_print_states": "حالات مفصولة بفواصل تعني الطباعة، مثل printing,printing_recovery.",
"chamber_light": "اختياري. مثال: light.elegoo_centauri_carbon2_chamber_light. يستخدم فقط بواسطة اعداد التحكم بالضوء.",
"light_control_mode": "اختر ما اذا كان الكشف لا يتحكم بالضوء، او يشغله ويبقيه مشغلا، او يعيد حالة الضوء السابقة بعد كل snapshot.",
"light_settle_seconds": "عدد الثواني للانتظار بعد ان يشغل التكامل ضوءا كان مطفأ قبل اخذ snapshot. الافتراضي 3 ثوان للتعريض/التركيز.",
"run_without_printing": "اذا لم يتم تحديد حساس حالة طباعة، يعمل الكشف المجدول فقط عند تفعيل هذا الخيار. زر Test يشغل فحصا واحدا دائما.",
"detection_interval": "عدد الثواني بين الفحوصات المجدولة عندما تكون حالة الطباعة نشطة. امثلة: 600 لعشر دقائق، 900 لخمس عشرة دقيقة.",
"warning_threshold": "يستخدم عندما تكون الحساسية Custom thresholds.",
"failure_threshold": "يستخدم عندما تكون الحساسية Custom thresholds.",
"cooldown_seconds": "الافتراضي 900 ثانية. مع حساس حالة طباعة، ترسل الفحوصات المجدولة حدث detected واحدا لكل نافذة طباعة نشطة؛ وتستمر result events في الارسال مع كل فحص."
}
}
},
"error": {
"warning_above_failure": "يجب ان يكون حد التحذير اقل من حد الفشل او مساويا له.",
"camera_image_unavailable": "الكاميرا المحددة لا تعرض entity_picture URL. جرب كاميرا اخرى او اضبط snapshot URL مباشر.",
"ml_health_failed": "فشل فحص صحة خادم ML. تأكد ان المضيف قابل للوصول ويشير الى base URL مثل http://192.168.1.100:3333.",
"ml_auth_failed": "رفض خادم ML الرمز.",
"ml_image_fetch_failed": "تعذر على خادم ML جلب صورة الكاميرا او فك ترميزها. تحقق من Home Assistant Host ووصول الكاميرا من خادم ML."
}
},
"services": {
"predict": {
"name": "توقع spaghetti من URL",
"description": "يشغل نموذج Obico ML على URL صورة خام",
"fields": {
"obico_host": {
"name": "Obico ML API Host",
"description": "Obico ML API host"
},
"obico_auth_token": {
"name": "Obico ML API Auth Token",
"description": "رمز مصادقة Obico ML API"
},
"image_url": {
"name": "Image URL",
"description": "Snapshot URL"
}
}
},
"run_detection": {
"name": "تشغيل الكشف",
"description": "يشغل فحص كشف واحدا لكاشف مكون.",
"fields": {
"detector": {
"name": "الكاشف"
},
"config_entry": {
"name": "Config entry"
},
"force": {
"name": "اجبار"
}
}
},
"reset_state": {
"name": "اعادة ضبط حالة الكشف",
"description": "يمسح confidence والنتيجة وحالة الخطأ للكاشف."
}
}
}
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "Create one detector for an Elegoo printer camera. The integration only detects failures and fires entities/events; pause, stop, and notify actions stay in your own automations.",
"data": {
"name": "Detector name",
"instance_id": "Entity prefix",
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Camera",
"snapshot_url": "Direct snapshot URL",
"print_status_sensor": "Print status sensor",
"active_print_states": "Active print states",
"chamber_light": "Chamber light",
"light_control_mode": "Light control",
"light_settle_seconds": "Light settle delay",
"run_without_printing": "Run scheduled detection without print status",
"detection_interval": "Detection interval",
"sensitivity": "Sensitivity",
"warning_threshold": "Warning threshold",
"failure_threshold": "Failure threshold",
"cooldown_seconds": "Detected event cooldown"
},
"data_description": {
"home_assistant_host": "URL reachable by the ML server. For Docker on another LAN host, use the HA LAN URL, for example http://192.168.1.90:8123.",
"obico_host": "Base URL of this project's ML server, for example http://192.168.1.100:3333. The setup checks /hc/ and /debug/image.",
"obico_auth_token": "Must match ML_API_TOKEN / obico_api_secret configured on the ML server.",
"instance_id": "Stable slug used in entity IDs. Use a different prefix for each detector, for example elegoo_cc2_left.",
"camera": "Any HA camera entity. Example from elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. Your device name may differ.",
"snapshot_url": "Optional. Leave empty to use the selected camera entity's Home Assistant camera proxy image. Use this only for unusual cameras.",
"print_status_sensor": "Optional but recommended. Example: sensor.elegoo_centauri_carbon2_print_status. For Elegoo-style names, the matching current_status sensor is used automatically as an extra guard.",
"active_print_states": "Comma-separated states that mean printing, for example printing,printing_recovery. Elegoo CC2 usually uses printing.",
"chamber_light": "Optional. Example: light.elegoo_centauri_carbon2_chamber_light. Used only by the light-control setting.",
"light_control_mode": "Choose whether detection should leave the light alone, turn it on and leave it on, or restore the previous light state after each snapshot.",
"light_settle_seconds": "Seconds to wait after this integration turns on an off light before taking the snapshot. Default is 3 seconds for camera exposure/focus.",
"run_without_printing": "If no print status sensor is selected, scheduled detection only runs when this is enabled. The Test button always runs one check.",
"detection_interval": "Seconds between scheduled checks while the print status is active. Examples: 600 for 10 minutes, 900 for 15 minutes.",
"warning_threshold": "Used when Sensitivity is set to Custom thresholds.",
"failure_threshold": "Used when Sensitivity is set to Custom thresholds.",
"cooldown_seconds": "Default is 900 seconds. Scheduled checks with a print status sensor emit one detected event per active print window; result events still fire for every check."
}
}
},
"error": {
"invalid_instance_id": "Entity prefix must contain at least one valid slug character.",
"instance_id_exists": "This entity prefix is already used by another detector.",
"warning_above_failure": "Warning threshold must be lower than or equal to failure threshold.",
"already_configured": "This camera is already configured.",
"camera_image_unavailable": "The selected camera does not expose an entity_picture URL. Try another camera or set a direct snapshot URL.",
"ml_health_failed": "The ML server health check failed. Confirm the host is reachable and points to the base URL, for example http://192.168.1.100:3333.",
"ml_auth_failed": "The ML server rejected the token.",
"ml_image_fetch_failed": "The ML server could not fetch or decode the camera image. Check Home Assistant Host and camera access from the ML server."
},
"abort": {
"already_configured": "This camera is already configured."
}
},
"options": {
"step": {
"init": {
"title": "Detector settings",
"description": "Update camera, ML server, print-state, and detection settings for this detector.",
"data": {
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Camera",
"snapshot_url": "Direct snapshot URL",
"print_status_sensor": "Print status sensor",
"active_print_states": "Active print states",
"chamber_light": "Chamber light",
"light_control_mode": "Light control",
"light_settle_seconds": "Light settle delay",
"run_without_printing": "Run scheduled detection without print status",
"detection_interval": "Detection interval",
"sensitivity": "Sensitivity",
"warning_threshold": "Warning threshold",
"failure_threshold": "Failure threshold",
"cooldown_seconds": "Detected event cooldown"
},
"data_description": {
"home_assistant_host": "URL reachable by the ML server. For Docker on another LAN host, use the HA LAN URL, for example http://192.168.1.90:8123.",
"obico_host": "Base URL of this project's ML server, for example http://192.168.1.100:3333. The setup checks /hc/ and /debug/image.",
"obico_auth_token": "Must match ML_API_TOKEN / obico_api_secret configured on the ML server.",
"camera": "Any HA camera entity. Example from elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. Your device name may differ.",
"snapshot_url": "Optional. Leave empty to use the selected camera entity's Home Assistant camera proxy image.",
"print_status_sensor": "Optional but recommended. Example: sensor.elegoo_centauri_carbon2_print_status. For Elegoo-style names, the matching current_status sensor is used automatically as an extra guard.",
"active_print_states": "Comma-separated states that mean printing, for example printing,printing_recovery.",
"chamber_light": "Optional. Example: light.elegoo_centauri_carbon2_chamber_light. Used only by the light-control setting.",
"light_control_mode": "Choose whether detection should leave the light alone, turn it on and leave it on, or restore the previous light state after each snapshot.",
"light_settle_seconds": "Seconds to wait after this integration turns on an off light before taking the snapshot. Default is 3 seconds for camera exposure/focus.",
"run_without_printing": "If no print status sensor is selected, scheduled detection only runs when this is enabled. The Test button always runs one check.",
"detection_interval": "Seconds between scheduled checks while the print status is active. Examples: 600 for 10 minutes, 900 for 15 minutes.",
"warning_threshold": "Used when Sensitivity is set to Custom thresholds.",
"failure_threshold": "Used when Sensitivity is set to Custom thresholds.",
"cooldown_seconds": "Default is 900 seconds. Scheduled checks with a print status sensor emit one detected event per active print window; result events still fire for every check."
}
}
},
"error": {
"warning_above_failure": "Warning threshold must be lower than or equal to failure threshold.",
"camera_image_unavailable": "The selected camera does not expose an entity_picture URL. Try another camera or set a direct snapshot URL.",
"ml_health_failed": "The ML server health check failed. Confirm the host is reachable and points to the base URL, for example http://192.168.1.100:3333.",
"ml_auth_failed": "The ML server rejected the token.",
"ml_image_fetch_failed": "The ML server could not fetch or decode the camera image. Check Home Assistant Host and camera access from the ML server."
}
},
"services": {
"predict": {
"name": "Predict spaghetti from URL",
"description": "Runs the Obico ML model against a raw image URL",
"fields": {
"obico_host": {
"name": "Obico ML API Host",
"description": "Obico ML API host"
},
"obico_auth_token": {
"name": "Obico ML API Auth Token",
"description": "Obico ML API authentication token"
},
"image_url": {
"name": "Image URL",
"description": "Snapshot URL"
}
}
},
"run_detection": {
"name": "Run detection",
"description": "Runs one detection check for a configured detector.",
"fields": {
"detector": {
"name": "Detector"
},
"config_entry": {
"name": "Config entry"
},
"force": {
"name": "Force"
}
}
},
"reset_state": {
"name": "Reset detection state",
"description": "Clears the detector confidence, result, and error state."
}
}
}
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "Crea un detector para una camara de impresora Elegoo. La integracion solo detecta fallos y genera entidades/eventos; las acciones de pausar, detener y notificar quedan en tus automatizaciones.",
"data": {
"name": "Nombre del detector",
"instance_id": "Prefijo de entidad",
"home_assistant_host": "Host de Home Assistant",
"obico_host": "Host de la API ML de Obico",
"obico_auth_token": "Token de API ML de Obico",
"camera": "Camara",
"snapshot_url": "URL directa de captura",
"print_status_sensor": "Sensor de estado de impresion",
"active_print_states": "Estados activos de impresion",
"chamber_light": "Luz de camara",
"light_control_mode": "Control de luz",
"light_settle_seconds": "Espera de luz",
"run_without_printing": "Ejecutar deteccion programada sin estado de impresion",
"detection_interval": "Intervalo de deteccion",
"sensitivity": "Sensibilidad",
"warning_threshold": "Umbral de advertencia",
"failure_threshold": "Umbral de fallo",
"cooldown_seconds": "Enfriamiento del evento detectado"
},
"data_description": {
"home_assistant_host": "URL accesible por el servidor ML. Para Docker en otro host LAN, usa la URL LAN de HA, por ejemplo http://192.168.1.90:8123.",
"obico_host": "URL base del servidor ML de este proyecto, por ejemplo http://192.168.1.100:3333. La configuracion comprueba /hc/ y /debug/image.",
"obico_auth_token": "Debe coincidir con ML_API_TOKEN / obico_api_secret configurado en el servidor ML.",
"instance_id": "Slug estable usado en los ID de entidad. Usa un prefijo distinto para cada detector, por ejemplo elegoo_cc2_left.",
"camera": "Cualquier entidad de camara de HA. Ejemplo de elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. El nombre de tu dispositivo puede variar.",
"snapshot_url": "Opcional. Dejalo vacio para usar la imagen proxy de la camara seleccionada en Home Assistant. Usalo solo para camaras poco comunes.",
"print_status_sensor": "Opcional pero recomendado. Ejemplo: sensor.elegoo_centauri_carbon2_print_status. Para nombres estilo Elegoo, el sensor current_status coincidente se usa automaticamente como proteccion extra.",
"active_print_states": "Estados separados por comas que significan impresion, por ejemplo printing,printing_recovery. Elegoo CC2 normalmente usa printing.",
"chamber_light": "Opcional. Ejemplo: light.elegoo_centauri_carbon2_chamber_light. Solo lo usa el ajuste de control de luz.",
"light_control_mode": "Elige si la deteccion no controla la luz, la enciende y la deja encendida, o restaura el estado anterior despues de cada captura.",
"light_settle_seconds": "Segundos que se esperan despues de encender una luz apagada antes de tomar la captura. El valor predeterminado es 3 segundos para exposicion/enfoque.",
"run_without_printing": "Si no se selecciona sensor de estado, la deteccion programada solo se ejecuta cuando esto esta activado. El boton Test siempre ejecuta una comprobacion.",
"detection_interval": "Segundos entre comprobaciones programadas mientras el estado de impresion esta activo. Ejemplos: 600 para 10 minutos, 900 para 15 minutos.",
"warning_threshold": "Se usa cuando Sensibilidad esta en Custom thresholds.",
"failure_threshold": "Se usa cuando Sensibilidad esta en Custom thresholds.",
"cooldown_seconds": "El valor predeterminado es 900 segundos. Con sensor de estado, las comprobaciones programadas emiten un evento detected por ventana activa de impresion; los eventos result siguen emitiendose en cada comprobacion."
}
}
},
"error": {
"invalid_instance_id": "El prefijo de entidad debe contener al menos un caracter slug valido.",
"instance_id_exists": "Este prefijo de entidad ya lo usa otro detector.",
"warning_above_failure": "El umbral de advertencia debe ser menor o igual que el umbral de fallo.",
"already_configured": "Esta camara ya esta configurada.",
"camera_image_unavailable": "La camara seleccionada no expone una URL entity_picture. Prueba otra camara o define una URL directa de captura.",
"ml_health_failed": "La comprobacion de salud del servidor ML fallo. Confirma que el host sea accesible y apunte a la URL base, por ejemplo http://192.168.1.100:3333.",
"ml_auth_failed": "El servidor ML rechazo el token.",
"ml_image_fetch_failed": "El servidor ML no pudo obtener o decodificar la imagen de la camara. Revisa Home Assistant Host y el acceso a la camara desde el servidor ML."
},
"abort": {
"already_configured": "Esta camara ya esta configurada."
}
},
"options": {
"step": {
"init": {
"title": "Ajustes del detector",
"description": "Actualiza la camara, servidor ML, estado de impresion y ajustes de deteccion para este detector.",
"data": {
"home_assistant_host": "Host de Home Assistant",
"obico_host": "Host de la API ML de Obico",
"obico_auth_token": "Token de API ML de Obico",
"camera": "Camara",
"snapshot_url": "URL directa de captura",
"print_status_sensor": "Sensor de estado de impresion",
"active_print_states": "Estados activos de impresion",
"chamber_light": "Luz de camara",
"light_control_mode": "Control de luz",
"light_settle_seconds": "Espera de luz",
"run_without_printing": "Ejecutar deteccion programada sin estado de impresion",
"detection_interval": "Intervalo de deteccion",
"sensitivity": "Sensibilidad",
"warning_threshold": "Umbral de advertencia",
"failure_threshold": "Umbral de fallo",
"cooldown_seconds": "Enfriamiento del evento detectado"
},
"data_description": {
"home_assistant_host": "URL accesible por el servidor ML. Para Docker en otro host LAN, usa la URL LAN de HA, por ejemplo http://192.168.1.90:8123.",
"obico_host": "URL base del servidor ML de este proyecto, por ejemplo http://192.168.1.100:3333. La configuracion comprueba /hc/ y /debug/image.",
"obico_auth_token": "Debe coincidir con ML_API_TOKEN / obico_api_secret configurado en el servidor ML.",
"camera": "Cualquier entidad de camara de HA. Ejemplo de elegoo-homeassistant: camera.elegoo_centauri_carbon2_chamber_camera. El nombre de tu dispositivo puede variar.",
"snapshot_url": "Opcional. Dejalo vacio para usar la imagen proxy de la camara seleccionada en Home Assistant.",
"print_status_sensor": "Opcional pero recomendado. Ejemplo: sensor.elegoo_centauri_carbon2_print_status. Para nombres estilo Elegoo, el sensor current_status coincidente se usa automaticamente como proteccion extra.",
"active_print_states": "Estados separados por comas que significan impresion, por ejemplo printing,printing_recovery.",
"chamber_light": "Opcional. Ejemplo: light.elegoo_centauri_carbon2_chamber_light. Solo lo usa el ajuste de control de luz.",
"light_control_mode": "Elige si la deteccion no controla la luz, la enciende y la deja encendida, o restaura el estado anterior despues de cada captura.",
"light_settle_seconds": "Segundos que se esperan despues de encender una luz apagada antes de tomar la captura. El valor predeterminado es 3 segundos para exposicion/enfoque.",
"run_without_printing": "Si no se selecciona sensor de estado, la deteccion programada solo se ejecuta cuando esto esta activado. El boton Test siempre ejecuta una comprobacion.",
"detection_interval": "Segundos entre comprobaciones programadas mientras el estado de impresion esta activo. Ejemplos: 600 para 10 minutos, 900 para 15 minutos.",
"warning_threshold": "Se usa cuando Sensibilidad esta en Custom thresholds.",
"failure_threshold": "Se usa cuando Sensibilidad esta en Custom thresholds.",
"cooldown_seconds": "El valor predeterminado es 900 segundos. Con sensor de estado, las comprobaciones programadas emiten un evento detected por ventana activa de impresion; los eventos result siguen emitiendose en cada comprobacion."
}
}
},
"error": {
"warning_above_failure": "El umbral de advertencia debe ser menor o igual que el umbral de fallo.",
"camera_image_unavailable": "La camara seleccionada no expone una URL entity_picture. Prueba otra camara o define una URL directa de captura.",
"ml_health_failed": "La comprobacion de salud del servidor ML fallo. Confirma que el host sea accesible y apunte a la URL base, por ejemplo http://192.168.1.100:3333.",
"ml_auth_failed": "El servidor ML rechazo el token.",
"ml_image_fetch_failed": "El servidor ML no pudo obtener o decodificar la imagen de la camara. Revisa Home Assistant Host y el acceso a la camara desde el servidor ML."
}
},
"services": {
"predict": {
"name": "Predecir spaghetti desde URL",
"description": "Ejecuta el modelo ML de Obico sobre una URL de imagen sin procesar",
"fields": {
"obico_host": {
"name": "Host de la API ML de Obico",
"description": "Host de la API ML de Obico"
},
"obico_auth_token": {
"name": "Token de API ML de Obico",
"description": "Token de autenticacion de la API ML de Obico"
},
"image_url": {
"name": "URL de imagen",
"description": "URL de captura"
}
}
},
"run_detection": {
"name": "Ejecutar deteccion",
"description": "Ejecuta una comprobacion de deteccion para un detector configurado.",
"fields": {
"detector": {
"name": "Detector"
},
"config_entry": {
"name": "Entrada de configuracion"
},
"force": {
"name": "Forzar"
}
}
},
"reset_state": {
"name": "Restablecer estado de deteccion",
"description": "Limpia la confianza, el resultado y el estado de error del detector."
}
}
}
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "Elegoo प्रिंटर कैमरा के लिए एक detector बनाता है। यह integration केवल failures detect करता है और entities/events बनाता है; pause, stop और notify actions आपकी अपनी automations में रहते हैं।",
"data": {
"name": "Detector name",
"instance_id": "Entity prefix",
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Camera",
"snapshot_url": "Direct snapshot URL",
"print_status_sensor": "Print status sensor",
"active_print_states": "Active print states",
"chamber_light": "Chamber light",
"light_control_mode": "Light control",
"light_settle_seconds": "Light settle delay",
"run_without_printing": "Print status के बिना scheduled detection चलाएं",
"detection_interval": "Detection interval",
"sensitivity": "Sensitivity",
"warning_threshold": "Warning threshold",
"failure_threshold": "Failure threshold",
"cooldown_seconds": "Detected event cooldown"
},
"data_description": {
"home_assistant_host": "ML server द्वारा reachable URL। किसी दूसरे LAN host पर Docker के लिए HA LAN URL इस्तेमाल करें, जैसे http://192.168.1.90:8123.",
"obico_host": "इस project के ML server का base URL, जैसे http://192.168.1.100:3333। Setup /hc/ और /debug/image check करता है।",
"obico_auth_token": "ML server पर configured ML_API_TOKEN / obico_api_secret से match करना चाहिए।",
"instance_id": "Entity IDs में इस्तेमाल होने वाला stable slug। हर detector के लिए अलग prefix इस्तेमाल करें, जैसे elegoo_cc2_left.",
"camera": "कोई भी HA camera entity। elegoo-homeassistant example: camera.elegoo_centauri_carbon2_chamber_camera. आपका device name अलग हो सकता है।",
"snapshot_url": "Optional। Selected camera entity की Home Assistant camera proxy image इस्तेमाल करने के लिए खाली छोड़ें। इसे केवल unusual cameras के लिए इस्तेमाल करें।",
"print_status_sensor": "Optional लेकिन recommended। Example: sensor.elegoo_centauri_carbon2_print_status. Elegoo-style names में matching current_status sensor अपने आप extra guard के रूप में इस्तेमाल होता है।",
"active_print_states": "Printing बताने वाले comma-separated states, जैसे printing,printing_recovery. Elegoo CC2 आम तौर पर printing इस्तेमाल करता है।",
"chamber_light": "Optional। Example: light.elegoo_centauri_carbon2_chamber_light. केवल light-control setting द्वारा इस्तेमाल होता है।",
"light_control_mode": "चुनें कि detection light को न छुए, उसे on करके on रखे, या हर snapshot के बाद पिछली state restore करे।",
"light_settle_seconds": "Integration द्वारा off light को on करने के बाद snapshot से पहले wait करने के seconds। Camera exposure/focus के लिए default 3 seconds है।",
"run_without_printing": "अगर print status sensor selected नहीं है, scheduled detection केवल यह enabled होने पर चलता है। Test button हमेशा एक check चलाता है।",
"detection_interval": "Print status active होने पर scheduled checks के बीच seconds। Examples: 10 minutes के लिए 600, 15 minutes के लिए 900.",
"warning_threshold": "Sensitivity Custom thresholds होने पर इस्तेमाल होता है।",
"failure_threshold": "Sensitivity Custom thresholds होने पर इस्तेमाल होता है।",
"cooldown_seconds": "Default 900 seconds है। Print status sensor के साथ scheduled checks हर active print window में एक detected event emit करते हैं; result events हर check पर आते रहते हैं।"
}
}
},
"error": {
"invalid_instance_id": "Entity prefix में कम से कम एक valid slug character होना चाहिए।",
"instance_id_exists": "यह entity prefix पहले से किसी दूसरे detector द्वारा इस्तेमाल हो रहा है।",
"warning_above_failure": "Warning threshold failure threshold से कम या उसके बराबर होना चाहिए।",
"already_configured": "यह camera पहले से configured है।",
"camera_image_unavailable": "Selected camera entity_picture URL expose नहीं करता। दूसरा camera try करें या direct snapshot URL set करें।",
"ml_health_failed": "ML server health check failed। Confirm करें कि host reachable है और base URL पर point करता है, जैसे http://192.168.1.100:3333.",
"ml_auth_failed": "ML server ने token reject किया।",
"ml_image_fetch_failed": "ML server camera image fetch या decode नहीं कर सका। Home Assistant Host और ML server से camera access check करें।"
},
"abort": {
"already_configured": "यह camera पहले से configured है।"
}
},
"options": {
"step": {
"init": {
"title": "Detector settings",
"description": "इस detector के लिए camera, ML server, print-state और detection settings update करें।",
"data": {
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Camera",
"snapshot_url": "Direct snapshot URL",
"print_status_sensor": "Print status sensor",
"active_print_states": "Active print states",
"chamber_light": "Chamber light",
"light_control_mode": "Light control",
"light_settle_seconds": "Light settle delay",
"run_without_printing": "Print status के बिना scheduled detection चलाएं",
"detection_interval": "Detection interval",
"sensitivity": "Sensitivity",
"warning_threshold": "Warning threshold",
"failure_threshold": "Failure threshold",
"cooldown_seconds": "Detected event cooldown"
},
"data_description": {
"home_assistant_host": "ML server द्वारा reachable URL। किसी दूसरे LAN host पर Docker के लिए HA LAN URL इस्तेमाल करें, जैसे http://192.168.1.90:8123.",
"obico_host": "इस project के ML server का base URL, जैसे http://192.168.1.100:3333। Setup /hc/ और /debug/image check करता है।",
"obico_auth_token": "ML server पर configured ML_API_TOKEN / obico_api_secret से match करना चाहिए।",
"camera": "कोई भी HA camera entity। elegoo-homeassistant example: camera.elegoo_centauri_carbon2_chamber_camera. आपका device name अलग हो सकता है।",
"snapshot_url": "Optional। Selected camera entity की Home Assistant camera proxy image इस्तेमाल करने के लिए खाली छोड़ें।",
"print_status_sensor": "Optional लेकिन recommended। Example: sensor.elegoo_centauri_carbon2_print_status. Elegoo-style names में matching current_status sensor अपने आप extra guard के रूप में इस्तेमाल होता है।",
"active_print_states": "Printing बताने वाले comma-separated states, जैसे printing,printing_recovery.",
"chamber_light": "Optional। Example: light.elegoo_centauri_carbon2_chamber_light. केवल light-control setting द्वारा इस्तेमाल होता है।",
"light_control_mode": "चुनें कि detection light को न छुए, उसे on करके on रखे, या हर snapshot के बाद पिछली state restore करे।",
"light_settle_seconds": "Integration द्वारा off light को on करने के बाद snapshot से पहले wait करने के seconds। Camera exposure/focus के लिए default 3 seconds है।",
"run_without_printing": "अगर print status sensor selected नहीं है, scheduled detection केवल यह enabled होने पर चलता है। Test button हमेशा एक check चलाता है।",
"detection_interval": "Print status active होने पर scheduled checks के बीच seconds। Examples: 10 minutes के लिए 600, 15 minutes के लिए 900.",
"warning_threshold": "Sensitivity Custom thresholds होने पर इस्तेमाल होता है।",
"failure_threshold": "Sensitivity Custom thresholds होने पर इस्तेमाल होता है।",
"cooldown_seconds": "Default 900 seconds है। Print status sensor के साथ scheduled checks हर active print window में एक detected event emit करते हैं; result events हर check पर आते रहते हैं।"
}
}
},
"error": {
"warning_above_failure": "Warning threshold failure threshold से कम या उसके बराबर होना चाहिए।",
"camera_image_unavailable": "Selected camera entity_picture URL expose नहीं करता। दूसरा camera try करें या direct snapshot URL set करें।",
"ml_health_failed": "ML server health check failed। Confirm करें कि host reachable है और base URL पर point करता है, जैसे http://192.168.1.100:3333.",
"ml_auth_failed": "ML server ने token reject किया।",
"ml_image_fetch_failed": "ML server camera image fetch या decode नहीं कर सका। Home Assistant Host और ML server से camera access check करें।"
}
},
"services": {
"predict": {
"name": "URL से spaghetti predict करें",
"description": "Raw image URL पर Obico ML model चलाता है",
"fields": {
"obico_host": {
"name": "Obico ML API Host",
"description": "Obico ML API host"
},
"obico_auth_token": {
"name": "Obico ML API Auth Token",
"description": "Obico ML API authentication token"
},
"image_url": {
"name": "Image URL",
"description": "Snapshot URL"
}
}
},
"run_detection": {
"name": "Detection चलाएं",
"description": "Configured detector के लिए एक detection check चलाता है।",
"fields": {
"detector": {
"name": "Detector"
},
"config_entry": {
"name": "Config entry"
},
"force": {
"name": "Force"
}
}
},
"reset_state": {
"name": "Detection state reset करें",
"description": "Detector confidence, result और error state साफ करता है।"
}
}
}
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "Elegoo yazici kamerasi icin bir algilayici olusturur. Entegrasyon yalnizca hatalari algilar ve entity/event uretir; pause, stop ve bildirim aksiyonlari kendi otomasyonlarinizda kalir.",
"data": {
"name": "Algilayici adi",
"instance_id": "Entity on eki",
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Kamera",
"snapshot_url": "Dogrudan snapshot URL",
"print_status_sensor": "Baski durum sensoru",
"active_print_states": "Aktif baski durumlari",
"chamber_light": "Kabin isigi",
"light_control_mode": "Isik kontrolu",
"light_settle_seconds": "Isik bekleme suresi",
"run_without_printing": "Baski durumu olmadan zamanlanmis algilama calistir",
"detection_interval": "Algilama araligi",
"sensitivity": "Hassasiyet",
"warning_threshold": "Uyari esigi",
"failure_threshold": "Hata esigi",
"cooldown_seconds": "Algilandi eventi bekleme suresi"
},
"data_description": {
"home_assistant_host": "ML sunucusunun erisebildigi URL. Baska bir LAN makinesindeki Docker icin HA LAN URL kullanin; ornegin http://192.168.1.90:8123.",
"obico_host": "Bu projenin ML sunucusu base URL adresi; ornegin http://192.168.1.100:3333. Kurulum /hc/ ve /debug/image kontrollerini yapar.",
"obico_auth_token": "ML sunucusunda ayarlanan ML_API_TOKEN / obico_api_secret ile ayni olmalidir.",
"instance_id": "Entity ID'lerinde kullanilan kalici slug. Her algilayici icin farkli bir on ek kullanin; ornegin elegoo_cc2_left.",
"camera": "Herhangi bir HA kamera entity'si. elegoo-homeassistant ornegi: camera.elegoo_centauri_carbon2_chamber_camera. Cihaz adiniz farkli olabilir.",
"snapshot_url": "Istege bagli. Secilen kamera entity'sinin Home Assistant camera proxy gorselini kullanmak icin bos birakin. Bunu yalnizca ozel kamera durumlarinda kullanin.",
"print_status_sensor": "Istege bagli ama onerilir. Ornek: sensor.elegoo_centauri_carbon2_print_status. Elegoo tarzi adlarda eslesen current_status sensoru otomatik ek koruma olarak kullanilir.",
"active_print_states": "Baski anlamina gelen virgulle ayrilmis durumlar; ornegin printing,printing_recovery. Elegoo CC2 genelde printing kullanir.",
"chamber_light": "Istege bagli. Ornek: light.elegoo_centauri_carbon2_chamber_light. Yalnizca isik kontrol ayari tarafindan kullanilir.",
"light_control_mode": "Algilama isigi hic kontrol etmesin mi, acip acik mi biraksin, yoksa her snapshot sonrasinda onceki duruma mi dondursun secin.",
"light_settle_seconds": "Bu entegrasyon kapali isigi actiktan sonra snapshot almadan once bekleyecegi saniye. Kamera pozlama/netleme icin varsayilan 3 saniyedir.",
"run_without_printing": "Baski durum sensoru secilmediyse zamanlanmis algilama yalnizca bu ayar acikken calisir. Test butonu her zaman tek kontrol calistirir.",
"detection_interval": "Baski durumu aktifken zamanlanmis kontroller arasindaki saniye. Ornek: 10 dakika icin 600, 15 dakika icin 900.",
"warning_threshold": "Hassasiyet Custom thresholds oldugunda kullanilir.",
"failure_threshold": "Hassasiyet Custom thresholds oldugunda kullanilir.",
"cooldown_seconds": "Varsayilan 900 saniyedir. Baski durum sensoru olan zamanlanmis kontroller aktif baski penceresi basina bir detected event uretir; result event'leri her kontrolde gelmeye devam eder."
}
}
},
"error": {
"invalid_instance_id": "Entity on eki en az bir gecerli slug karakteri icermelidir.",
"instance_id_exists": "Bu entity on eki baska bir algilayici tarafindan kullaniliyor.",
"warning_above_failure": "Uyari esigi hata esiginden kucuk veya ona esit olmalidir.",
"already_configured": "Bu kamera zaten yapilandirilmis.",
"camera_image_unavailable": "Secilen kamera entity_picture URL sunmuyor. Baska kamera deneyin veya dogrudan snapshot URL ayarlayin.",
"ml_health_failed": "ML sunucusu saglik kontrolu basarisiz oldu. Host erisilebilir olmali ve base URL'ye isaret etmelidir; ornegin http://192.168.1.100:3333.",
"ml_auth_failed": "ML sunucusu token'i reddetti.",
"ml_image_fetch_failed": "ML sunucusu kamera gorselini alamadi veya decode edemedi. Home Assistant Host ve kamera erisimini ML sunucusundan kontrol edin."
},
"abort": {
"already_configured": "Bu kamera zaten yapilandirilmis."
}
},
"options": {
"step": {
"init": {
"title": "Algilayici ayarlari",
"description": "Bu algilayici icin kamera, ML sunucusu, baski durumu ve algilama ayarlarini guncelleyin.",
"data": {
"home_assistant_host": "Home Assistant Host",
"obico_host": "Obico ML API Host",
"obico_auth_token": "Obico ML API Auth Token",
"camera": "Kamera",
"snapshot_url": "Dogrudan snapshot URL",
"print_status_sensor": "Baski durum sensoru",
"active_print_states": "Aktif baski durumlari",
"chamber_light": "Kabin isigi",
"light_control_mode": "Isik kontrolu",
"light_settle_seconds": "Isik bekleme suresi",
"run_without_printing": "Baski durumu olmadan zamanlanmis algilama calistir",
"detection_interval": "Algilama araligi",
"sensitivity": "Hassasiyet",
"warning_threshold": "Uyari esigi",
"failure_threshold": "Hata esigi",
"cooldown_seconds": "Algilandi eventi bekleme suresi"
},
"data_description": {
"home_assistant_host": "ML sunucusunun erisebildigi URL. Baska bir LAN makinesindeki Docker icin HA LAN URL kullanin; ornegin http://192.168.1.90:8123.",
"obico_host": "Bu projenin ML sunucusu base URL adresi; ornegin http://192.168.1.100:3333. Kurulum /hc/ ve /debug/image kontrollerini yapar.",
"obico_auth_token": "ML sunucusunda ayarlanan ML_API_TOKEN / obico_api_secret ile ayni olmalidir.",
"camera": "Herhangi bir HA kamera entity'si. elegoo-homeassistant ornegi: camera.elegoo_centauri_carbon2_chamber_camera. Cihaz adiniz farkli olabilir.",
"snapshot_url": "Istege bagli. Secilen kamera entity'sinin Home Assistant camera proxy gorselini kullanmak icin bos birakin.",
"print_status_sensor": "Istege bagli ama onerilir. Ornek: sensor.elegoo_centauri_carbon2_print_status. Elegoo tarzi adlarda eslesen current_status sensoru otomatik ek koruma olarak kullanilir.",
"active_print_states": "Baski anlamina gelen virgulle ayrilmis durumlar; ornegin printing,printing_recovery.",
"chamber_light": "Istege bagli. Ornek: light.elegoo_centauri_carbon2_chamber_light. Yalnizca isik kontrol ayari tarafindan kullanilir.",
"light_control_mode": "Algilama isigi hic kontrol etmesin mi, acip acik mi biraksin, yoksa her snapshot sonrasinda onceki duruma mi dondursun secin.",
"light_settle_seconds": "Bu entegrasyon kapali isigi actiktan sonra snapshot almadan once bekleyecegi saniye. Kamera pozlama/netleme icin varsayilan 3 saniyedir.",
"run_without_printing": "Baski durum sensoru secilmediyse zamanlanmis algilama yalnizca bu ayar acikken calisir. Test butonu her zaman tek kontrol calistirir.",
"detection_interval": "Baski durumu aktifken zamanlanmis kontroller arasindaki saniye. Ornek: 10 dakika icin 600, 15 dakika icin 900.",
"warning_threshold": "Hassasiyet Custom thresholds oldugunda kullanilir.",
"failure_threshold": "Hassasiyet Custom thresholds oldugunda kullanilir.",
"cooldown_seconds": "Varsayilan 900 saniyedir. Baski durum sensoru olan zamanlanmis kontroller aktif baski penceresi basina bir detected event uretir; result event'leri her kontrolde gelmeye devam eder."
}
}
},
"error": {
"warning_above_failure": "Uyari esigi hata esiginden kucuk veya ona esit olmalidir.",
"camera_image_unavailable": "Secilen kamera entity_picture URL sunmuyor. Baska kamera deneyin veya dogrudan snapshot URL ayarlayin.",
"ml_health_failed": "ML sunucusu saglik kontrolu basarisiz oldu. Host erisilebilir olmali ve base URL'ye isaret etmelidir; ornegin http://192.168.1.100:3333.",
"ml_auth_failed": "ML sunucusu token'i reddetti.",
"ml_image_fetch_failed": "ML sunucusu kamera gorselini alamadi veya decode edemedi. Home Assistant Host ve kamera erisimini ML sunucusundan kontrol edin."
}
},
"services": {
"predict": {
"name": "URL'den spaghetti tahmini",
"description": "Obico ML modelini ham bir gorsel URL'si uzerinde calistirir",
"fields": {
"obico_host": {
"name": "Obico ML API Host",
"description": "Obico ML API host"
},
"obico_auth_token": {
"name": "Obico ML API Auth Token",
"description": "Obico ML API kimlik dogrulama token'i"
},
"image_url": {
"name": "Gorsel URL",
"description": "Snapshot URL"
}
}
},
"run_detection": {
"name": "Algilama calistir",
"description": "Yapilandirilmis bir algilayici icin tek algilama kontrolu calistirir.",
"fields": {
"detector": {
"name": "Algilayici"
},
"config_entry": {
"name": "Config entry"
},
"force": {
"name": "Zorla"
}
}
},
"reset_state": {
"name": "Algilama durumunu sifirla",
"description": "Algilayici confidence, sonuc ve hata durumunu temizler."
}
}
}
@@ -0,0 +1,150 @@
{
"config": {
"step": {
"user": {
"title": "Elegoo Spaghetti Detection",
"description": "为 Elegoo 打印机摄像头创建一个检测器。该集成只检测失败并产生实体/事件;暂停、停止和通知动作仍由你的自动化处理。",
"data": {
"name": "检测器名称",
"instance_id": "实体前缀",
"home_assistant_host": "Home Assistant 主机",
"obico_host": "Obico ML API 主机",
"obico_auth_token": "Obico ML API 令牌",
"camera": "摄像头",
"snapshot_url": "直接快照 URL",
"print_status_sensor": "打印状态传感器",
"active_print_states": "活动打印状态",
"chamber_light": "腔体灯",
"light_control_mode": "灯光控制",
"light_settle_seconds": "灯光稳定延迟",
"run_without_printing": "无打印状态时运行计划检测",
"detection_interval": "检测间隔",
"sensitivity": "灵敏度",
"warning_threshold": "警告阈值",
"failure_threshold": "失败阈值",
"cooldown_seconds": "检测事件冷却时间"
},
"data_description": {
"home_assistant_host": "ML 服务器可访问的 URL。Docker 在另一台局域网主机上运行时,请使用 HA 的局域网 URL,例如 http://192.168.1.90:8123。",
"obico_host": "本项目 ML 服务器的基础 URL,例如 http://192.168.1.100:3333。配置会检查 /hc/ 和 /debug/image。",
"obico_auth_token": "必须与 ML 服务器上配置的 ML_API_TOKEN / obico_api_secret 匹配。",
"instance_id": "用于实体 ID 的稳定 slug。每个检测器使用不同前缀,例如 elegoo_cc2_left。",
"camera": "任意 HA 摄像头实体。elegoo-homeassistant 示例:camera.elegoo_centauri_carbon2_chamber_camera。你的设备名称可能不同。",
"snapshot_url": "可选。留空则使用所选摄像头实体的 Home Assistant camera proxy 图像。仅在特殊摄像头场景中使用。",
"print_status_sensor": "可选但推荐。示例:sensor.elegoo_centauri_carbon2_print_status。对于 Elegoo 风格命名,匹配的 current_status 传感器会自动作为额外保护。",
"active_print_states": "表示正在打印的逗号分隔状态,例如 printing,printing_recovery。Elegoo CC2 通常使用 printing。",
"chamber_light": "可选。示例:light.elegoo_centauri_carbon2_chamber_light。仅由灯光控制设置使用。",
"light_control_mode": "选择检测时不控制灯光、打开并保持开启,或每次快照后恢复之前的灯光状态。",
"light_settle_seconds": "集成打开原本关闭的灯光后,拍摄快照前等待的秒数。默认 3 秒,用于曝光/对焦。",
"run_without_printing": "未选择打印状态传感器时,计划检测只会在启用此项后运行。测试按钮始终运行一次检查。",
"detection_interval": "打印状态活动时,两次计划检查之间的秒数。示例:600 表示 10 分钟,900 表示 15 分钟。",
"warning_threshold": "当灵敏度设置为 Custom thresholds 时使用。",
"failure_threshold": "当灵敏度设置为 Custom thresholds 时使用。",
"cooldown_seconds": "默认 900 秒。带打印状态传感器的计划检查在每个活动打印窗口只发出一个 detected 事件;result 事件仍会在每次检查时发出。"
}
}
},
"error": {
"invalid_instance_id": "实体前缀必须至少包含一个有效的 slug 字符。",
"instance_id_exists": "此实体前缀已被另一个检测器使用。",
"warning_above_failure": "警告阈值必须小于或等于失败阈值。",
"already_configured": "此摄像头已配置。",
"camera_image_unavailable": "所选摄像头没有提供 entity_picture URL。请尝试其他摄像头或设置直接快照 URL。",
"ml_health_failed": "ML 服务器健康检查失败。请确认主机可访问并指向基础 URL,例如 http://192.168.1.100:3333。",
"ml_auth_failed": "ML 服务器拒绝了令牌。",
"ml_image_fetch_failed": "ML 服务器无法获取或解码摄像头图像。请检查 Home Assistant Host 以及 ML 服务器对摄像头的访问。"
},
"abort": {
"already_configured": "此摄像头已配置。"
}
},
"options": {
"step": {
"init": {
"title": "检测器设置",
"description": "更新此检测器的摄像头、ML 服务器、打印状态和检测设置。",
"data": {
"home_assistant_host": "Home Assistant 主机",
"obico_host": "Obico ML API 主机",
"obico_auth_token": "Obico ML API 令牌",
"camera": "摄像头",
"snapshot_url": "直接快照 URL",
"print_status_sensor": "打印状态传感器",
"active_print_states": "活动打印状态",
"chamber_light": "腔体灯",
"light_control_mode": "灯光控制",
"light_settle_seconds": "灯光稳定延迟",
"run_without_printing": "无打印状态时运行计划检测",
"detection_interval": "检测间隔",
"sensitivity": "灵敏度",
"warning_threshold": "警告阈值",
"failure_threshold": "失败阈值",
"cooldown_seconds": "检测事件冷却时间"
},
"data_description": {
"home_assistant_host": "ML 服务器可访问的 URL。Docker 在另一台局域网主机上运行时,请使用 HA 的局域网 URL,例如 http://192.168.1.90:8123。",
"obico_host": "本项目 ML 服务器的基础 URL,例如 http://192.168.1.100:3333。配置会检查 /hc/ 和 /debug/image。",
"obico_auth_token": "必须与 ML 服务器上配置的 ML_API_TOKEN / obico_api_secret 匹配。",
"camera": "任意 HA 摄像头实体。elegoo-homeassistant 示例:camera.elegoo_centauri_carbon2_chamber_camera。你的设备名称可能不同。",
"snapshot_url": "可选。留空则使用所选摄像头实体的 Home Assistant camera proxy 图像。",
"print_status_sensor": "可选但推荐。示例:sensor.elegoo_centauri_carbon2_print_status。对于 Elegoo 风格命名,匹配的 current_status 传感器会自动作为额外保护。",
"active_print_states": "表示正在打印的逗号分隔状态,例如 printing,printing_recovery。",
"chamber_light": "可选。示例:light.elegoo_centauri_carbon2_chamber_light。仅由灯光控制设置使用。",
"light_control_mode": "选择检测时不控制灯光、打开并保持开启,或每次快照后恢复之前的灯光状态。",
"light_settle_seconds": "集成打开原本关闭的灯光后,拍摄快照前等待的秒数。默认 3 秒,用于曝光/对焦。",
"run_without_printing": "未选择打印状态传感器时,计划检测只会在启用此项后运行。测试按钮始终运行一次检查。",
"detection_interval": "打印状态活动时,两次计划检查之间的秒数。示例:600 表示 10 分钟,900 表示 15 分钟。",
"warning_threshold": "当灵敏度设置为 Custom thresholds 时使用。",
"failure_threshold": "当灵敏度设置为 Custom thresholds 时使用。",
"cooldown_seconds": "默认 900 秒。带打印状态传感器的计划检查在每个活动打印窗口只发出一个 detected 事件;result 事件仍会在每次检查时发出。"
}
}
},
"error": {
"warning_above_failure": "警告阈值必须小于或等于失败阈值。",
"camera_image_unavailable": "所选摄像头没有提供 entity_picture URL。请尝试其他摄像头或设置直接快照 URL。",
"ml_health_failed": "ML 服务器健康检查失败。请确认主机可访问并指向基础 URL,例如 http://192.168.1.100:3333。",
"ml_auth_failed": "ML 服务器拒绝了令牌。",
"ml_image_fetch_failed": "ML 服务器无法获取或解码摄像头图像。请检查 Home Assistant Host 以及 ML 服务器对摄像头的访问。"
}
},
"services": {
"predict": {
"name": "从 URL 预测 spaghetti",
"description": "针对原始图像 URL 运行 Obico ML 模型",
"fields": {
"obico_host": {
"name": "Obico ML API 主机",
"description": "Obico ML API 主机"
},
"obico_auth_token": {
"name": "Obico ML API 令牌",
"description": "Obico ML API 认证令牌"
},
"image_url": {
"name": "图像 URL",
"description": "快照 URL"
}
}
},
"run_detection": {
"name": "运行检测",
"description": "为已配置的检测器运行一次检测检查。",
"fields": {
"detector": {
"name": "检测器"
},
"config_entry": {
"name": "配置项"
},
"force": {
"name": "强制"
}
}
},
"reset_state": {
"name": "重置检测状态",
"description": "清除检测器的置信度、结果和错误状态。"
}
}
}