Initial commit
This commit is contained in:
@@ -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": "清除检测器的置信度、结果和错误状态。"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user