feat: NUT (Network UPS Tools) service provider + provider-agnostic UI
Add full NUT support as a polling-based service provider: - Async TCP client for upsd protocol (port 3493, configurable) - 8 event types: online, on_battery, low_battery, battery_restored, comms_lost, comms_restored, replace_battery, overload - 3 bot commands: /status, /devices, /battery - 38 Jinja2 templates (EN+RU notification + command templates) - Database: tracking config fields, migration, seeds - Frontend: provider form with host/port/credentials, grid items, i18n Provider-agnostic UI improvements: - Remove hardcoded 'immich' defaults from all config forms - Dynamic collection labels per provider type (Albums/Repos/Boards/UPS Devices) - Capability-driven test types instead of provider type checks - Template variable helpers for all providers (was Immich-only) - Guard Immich-only shared link check to Immich providers - Auto-clear stale global provider filter from localStorage - EntitySelect search placeholder shows current selection - Fix noneLabel in linked target config selectors New CLAUDE.md rule #8: no provider-specific hardcoding
This commit is contained in:
@@ -13,7 +13,7 @@ import aiohttp
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import ServiceProvider, User
|
||||
from ..services import make_immich_provider, make_gitea_provider, make_planka_provider
|
||||
from ..services import make_immich_provider, make_gitea_provider, make_planka_provider, make_nut_provider
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,11 +69,19 @@ class SchedulerProviderConfig(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class NutProviderConfig(BaseModel):
|
||||
host: str
|
||||
port: int = 3493
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
|
||||
|
||||
_PROVIDER_CONFIG_MODELS: dict[str, type[BaseModel]] = {
|
||||
"immich": ImmichProviderConfig,
|
||||
"gitea": GiteaProviderConfig,
|
||||
"planka": PlankaProviderConfig,
|
||||
"scheduler": SchedulerProviderConfig,
|
||||
"nut": NutProviderConfig,
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +171,17 @@ async def create_provider(
|
||||
detail=test_result.get("message", "Cannot connect to Planka"),
|
||||
)
|
||||
|
||||
elif body.type == "nut":
|
||||
nut = make_nut_provider(ServiceProvider(
|
||||
id=0, user_id=0, type="nut", name=body.name, config=body.config,
|
||||
))
|
||||
test_result = await nut.test_connection()
|
||||
if not test_result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=test_result.get("message", "Cannot connect to NUT server"),
|
||||
)
|
||||
|
||||
# Scheduler: no validation needed (virtual provider)
|
||||
|
||||
provider = ServiceProvider(
|
||||
@@ -297,6 +316,14 @@ async def update_provider(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Connection error: {err}",
|
||||
)
|
||||
elif config_changed and provider.type == "nut":
|
||||
nut = make_nut_provider(provider)
|
||||
test_result = await nut.test_connection()
|
||||
if not test_result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=test_result.get("message", "Cannot connect to NUT server"),
|
||||
)
|
||||
|
||||
session.add(provider)
|
||||
await session.commit()
|
||||
@@ -349,6 +376,10 @@ async def test_provider(
|
||||
if provider.type == "scheduler":
|
||||
return {"ok": True, "message": "Virtual provider — always available"}
|
||||
|
||||
if provider.type == "nut":
|
||||
nut = make_nut_provider(provider)
|
||||
return await nut.test_connection()
|
||||
|
||||
return {"ok": False, "message": f"Unknown provider type: {provider.type}"}
|
||||
|
||||
|
||||
@@ -417,6 +448,10 @@ async def list_collections(
|
||||
planka = make_planka_provider(http_session, provider)
|
||||
return await planka.list_collections()
|
||||
|
||||
if provider.type == "nut":
|
||||
nut = make_nut_provider(provider)
|
||||
return await nut.list_collections()
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@@ -475,7 +510,7 @@ def _provider_response(p: ServiceProvider) -> dict:
|
||||
"""Build a safe response dict for a provider."""
|
||||
config = dict(p.config)
|
||||
# Mask sensitive fields
|
||||
for secret_field in ("api_key", "api_token", "webhook_secret"):
|
||||
for secret_field in ("api_key", "api_token", "webhook_secret", "password"):
|
||||
if secret_field in config:
|
||||
key = config[secret_field]
|
||||
config[secret_field] = f"{key[:8]}...{key[-4:]}" if len(key) > 12 else "***"
|
||||
|
||||
@@ -249,6 +249,138 @@ async def get_template_variables():
|
||||
"variables": {**scheduled_vars, "assets": "List of asset dicts (use {% for asset in assets %})"},
|
||||
"asset_fields": asset_fields,
|
||||
},
|
||||
# --- Gitea slots ---
|
||||
**_gitea_variables(),
|
||||
# --- Planka slots ---
|
||||
**_planka_variables(),
|
||||
# --- NUT (UPS) slots ---
|
||||
**_nut_variables(),
|
||||
# --- Scheduler slots ---
|
||||
"message_scheduled_message": {
|
||||
"description": "Notification for scheduled message events",
|
||||
"variables": {
|
||||
"tracker_name": "Name of the tracker that fired",
|
||||
"fire_count": "How many times this tracker has fired",
|
||||
"current_date": "Current date (formatted)",
|
||||
"current_time": "Current time (formatted)",
|
||||
"current_datetime": "Current date and time (formatted)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _gitea_variables() -> dict:
|
||||
common = {
|
||||
"sender": "Username who triggered the event",
|
||||
"sender_name": "Display name of the sender",
|
||||
"repo_name": "Repository full name (owner/repo)",
|
||||
"repo_url": "Repository URL",
|
||||
}
|
||||
return {
|
||||
"message_push": {
|
||||
"description": "Code pushed to repository",
|
||||
"variables": {**common, "branch": "Branch name", "commit_count": "Number of commits",
|
||||
"compare_url": "Comparison URL", "commits": "List of commit dicts"},
|
||||
},
|
||||
"message_issue_opened": {
|
||||
"description": "Issue opened",
|
||||
"variables": {**common, "issue_number": "Issue number", "issue_title": "Issue title",
|
||||
"issue_url": "Issue URL", "issue_body": "Issue body text", "issue_labels": "Labels list"},
|
||||
},
|
||||
"message_issue_closed": {
|
||||
"description": "Issue closed",
|
||||
"variables": {**common, "issue_number": "Issue number", "issue_title": "Issue title",
|
||||
"issue_url": "Issue URL", "issue_state": "Issue state"},
|
||||
},
|
||||
"message_issue_commented": {
|
||||
"description": "Comment on issue",
|
||||
"variables": {**common, "issue_number": "Issue number", "issue_title": "Issue title",
|
||||
"issue_url": "Issue URL", "comment_body": "Comment text",
|
||||
"comment_url": "Comment URL", "comment_author": "Comment author"},
|
||||
},
|
||||
"message_pr_opened": {
|
||||
"description": "Pull request opened",
|
||||
"variables": {**common, "pr_number": "PR number", "pr_title": "PR title",
|
||||
"pr_url": "PR URL", "pr_body": "PR body text",
|
||||
"pr_base": "Base branch", "pr_head": "Head branch", "pr_labels": "Labels list"},
|
||||
},
|
||||
"message_pr_closed": {
|
||||
"description": "Pull request closed",
|
||||
"variables": {**common, "pr_number": "PR number", "pr_title": "PR title",
|
||||
"pr_url": "PR URL", "pr_state": "PR state", "pr_merged": "Whether PR was merged"},
|
||||
},
|
||||
"message_pr_merged": {
|
||||
"description": "Pull request merged",
|
||||
"variables": {**common, "pr_number": "PR number", "pr_title": "PR title",
|
||||
"pr_url": "PR URL", "pr_base": "Base branch", "pr_head": "Head branch"},
|
||||
},
|
||||
"message_pr_commented": {
|
||||
"description": "Comment on pull request",
|
||||
"variables": {**common, "pr_number": "PR number", "pr_title": "PR title",
|
||||
"pr_url": "PR URL", "comment_body": "Comment text",
|
||||
"comment_url": "Comment URL", "comment_author": "Comment author"},
|
||||
},
|
||||
"message_release_published": {
|
||||
"description": "Release published",
|
||||
"variables": {**common, "release_tag": "Release tag", "release_name": "Release name",
|
||||
"release_url": "Release URL", "release_body": "Release notes",
|
||||
"release_draft": "Is draft (boolean)", "release_prerelease": "Is prerelease (boolean)"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _planka_variables() -> dict:
|
||||
common = {
|
||||
"sender": "Username who triggered the event",
|
||||
"sender_name": "Display name of the sender",
|
||||
"board_name": "Board name",
|
||||
"board_id": "Board ID",
|
||||
"board_url": "Board URL",
|
||||
}
|
||||
card = {**common, "card_name": "Card name", "card_id": "Card ID", "card_url": "Card URL"}
|
||||
return {
|
||||
"message_card_created": {"description": "Card created", "variables": {**card, "list_name": "List name", "card_description": "Card description"}},
|
||||
"message_card_updated": {"description": "Card updated", "variables": {**card, "card_description": "Card description", "card_due_date": "Due date"}},
|
||||
"message_card_moved": {"description": "Card moved between lists", "variables": {**card, "old_list_name": "Previous list", "new_list_name": "New list"}},
|
||||
"message_card_deleted": {"description": "Card deleted", "variables": card},
|
||||
"message_card_commented": {"description": "Comment added to card", "variables": {**card, "comment_text": "Comment text"}},
|
||||
"message_comment_updated": {"description": "Comment updated", "variables": {**card, "comment_text": "Updated comment text"}},
|
||||
"message_board_created": {"description": "Board created", "variables": common},
|
||||
"message_board_updated": {"description": "Board updated", "variables": common},
|
||||
"message_board_deleted": {"description": "Board deleted", "variables": common},
|
||||
"message_list_created": {"description": "List created", "variables": {**common, "list_name": "List name"}},
|
||||
"message_list_updated": {"description": "List updated", "variables": {**common, "list_name": "List name"}},
|
||||
"message_list_deleted": {"description": "List deleted", "variables": {**common, "list_name": "List name"}},
|
||||
"message_attachment_created": {"description": "Attachment added", "variables": {**card, "attachment_name": "Attachment filename"}},
|
||||
"message_card_label_added": {"description": "Label added to card", "variables": {**card, "label_name": "Label name", "label_color": "Label color"}},
|
||||
"message_task_completed": {"description": "Task completed", "variables": {**card, "task_name": "Task name", "task_completed": "Completed (boolean)"}},
|
||||
}
|
||||
|
||||
|
||||
def _nut_variables() -> dict:
|
||||
common = {
|
||||
"ups_name": "UPS device name",
|
||||
"ups_model": "UPS hardware model",
|
||||
"ups_manufacturer": "UPS manufacturer",
|
||||
"battery_charge": "Battery charge percentage",
|
||||
"battery_runtime": "Estimated runtime (formatted)",
|
||||
"battery_runtime_seconds": "Estimated runtime in seconds",
|
||||
"ups_load": "UPS load percentage",
|
||||
"ups_status": "Raw status flags (e.g. OL, OB, LB)",
|
||||
"input_voltage": "Input voltage",
|
||||
"output_voltage": "Output voltage",
|
||||
"event_description": "Human-readable event description",
|
||||
"previous_status": "Previous UPS status flags",
|
||||
}
|
||||
return {
|
||||
"message_ups_online": {"description": "UPS back on mains power", "variables": common},
|
||||
"message_ups_on_battery": {"description": "UPS switched to battery", "variables": common},
|
||||
"message_ups_low_battery": {"description": "Battery critically low", "variables": common},
|
||||
"message_ups_battery_restored": {"description": "Battery charge recovered", "variables": common},
|
||||
"message_ups_comms_lost": {"description": "Communication with UPS lost", "variables": {"ups_name": common["ups_name"], "previous_status": common["previous_status"], "event_description": common["event_description"]}},
|
||||
"message_ups_comms_restored": {"description": "Communication restored", "variables": common},
|
||||
"message_ups_replace_battery": {"description": "Battery needs replacement", "variables": common},
|
||||
"message_ups_overload": {"description": "UPS load exceeded capacity", "variables": common},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,10 +32,12 @@ def _auto_register() -> None:
|
||||
from .immich import ImmichCommandHandler
|
||||
from .gitea_handler import GiteaCommandHandler
|
||||
from .planka_handler import PlankaCommandHandler
|
||||
from .nut_handler import NutCommandHandler
|
||||
|
||||
register_handler(ImmichCommandHandler())
|
||||
register_handler(GiteaCommandHandler())
|
||||
register_handler(PlankaCommandHandler())
|
||||
register_handler(NutCommandHandler())
|
||||
|
||||
|
||||
# Auto-register on import
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""NUT (UPS)-specific bot command handler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..database.models import CommandConfig, CommandTracker, ServiceProvider, TelegramBot
|
||||
from ..services import make_nut_provider
|
||||
from .base import ProviderCommandHandler
|
||||
from .handler import _render_cmd_template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_NUT_COMMANDS = {"status", "devices", "battery"}
|
||||
|
||||
|
||||
class NutCommandHandler(ProviderCommandHandler):
|
||||
"""Handles NUT-specific bot commands."""
|
||||
|
||||
provider_type = "nut"
|
||||
|
||||
def get_provider_commands(self) -> set[str]:
|
||||
return _NUT_COMMANDS
|
||||
|
||||
def get_rate_categories(self) -> dict[str, str]:
|
||||
return {"devices": "api", "battery": "api", "status": "api"}
|
||||
|
||||
async def handle(
|
||||
self,
|
||||
cmd: str,
|
||||
args: str,
|
||||
count: int,
|
||||
locale: str,
|
||||
response_mode: str,
|
||||
providers_map: dict[int, ServiceProvider],
|
||||
cmd_templates: dict[str, dict[str, str]],
|
||||
bot: TelegramBot,
|
||||
ctx_tuples: list[tuple[CommandTracker, CommandConfig, ServiceProvider]],
|
||||
) -> str | list[dict[str, Any]] | None:
|
||||
if cmd == "status":
|
||||
ctx = await _cmd_status(providers_map)
|
||||
return _render_cmd_template(cmd_templates, "status", locale, ctx)
|
||||
if cmd == "devices":
|
||||
ctx = await _cmd_devices(providers_map)
|
||||
return _render_cmd_template(cmd_templates, "devices", locale, ctx)
|
||||
if cmd == "battery":
|
||||
ctx = await _cmd_battery(providers_map)
|
||||
return _render_cmd_template(cmd_templates, "battery", locale, ctx)
|
||||
return None
|
||||
|
||||
|
||||
async def _query_all_ups(
|
||||
providers_map: dict[int, ServiceProvider],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Connect to all NUT providers and query UPS data."""
|
||||
from notify_bridge_core.providers.nut.models import NutUpsData
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
for provider in providers_map.values():
|
||||
if provider.type != "nut":
|
||||
continue
|
||||
nut = make_nut_provider(provider)
|
||||
try:
|
||||
client = nut._make_client()
|
||||
await client.connect()
|
||||
try:
|
||||
devices = await client.list_ups()
|
||||
for dev in devices:
|
||||
variables = await client.list_var(dev.name)
|
||||
data = NutUpsData.from_variables(dev.name, variables)
|
||||
results.append({
|
||||
"name": data.name,
|
||||
"description": data.description,
|
||||
"model": data.model,
|
||||
"manufacturer": data.manufacturer,
|
||||
"status": data.status,
|
||||
"battery_charge": int(data.battery_charge) if data.battery_charge is not None else None,
|
||||
"battery_runtime": data.battery_runtime_formatted,
|
||||
"ups_load": int(data.ups_load) if data.ups_load is not None else None,
|
||||
"input_voltage": str(data.input_voltage) if data.input_voltage is not None else None,
|
||||
"output_voltage": str(data.output_voltage) if data.output_voltage is not None else None,
|
||||
})
|
||||
finally:
|
||||
await client.disconnect()
|
||||
except Exception as exc:
|
||||
_LOGGER.warning("Failed to query NUT provider %s: %s", provider.name, exc)
|
||||
return results
|
||||
|
||||
|
||||
async def _cmd_status(providers_map: dict[int, ServiceProvider]) -> dict[str, Any]:
|
||||
devices = await _query_all_ups(providers_map)
|
||||
return {"devices": devices}
|
||||
|
||||
|
||||
async def _cmd_devices(providers_map: dict[int, ServiceProvider]) -> dict[str, Any]:
|
||||
devices: list[dict[str, Any]] = []
|
||||
for provider in providers_map.values():
|
||||
if provider.type != "nut":
|
||||
continue
|
||||
nut = make_nut_provider(provider)
|
||||
try:
|
||||
device_list = await nut.list_collections()
|
||||
devices.extend(device_list)
|
||||
except Exception as exc:
|
||||
_LOGGER.warning("Failed to list devices from %s: %s", provider.name, exc)
|
||||
return {"devices": devices}
|
||||
|
||||
|
||||
async def _cmd_battery(providers_map: dict[int, ServiceProvider]) -> dict[str, Any]:
|
||||
devices = await _query_all_ups(providers_map)
|
||||
return {"devices": devices}
|
||||
@@ -185,6 +185,41 @@ async def migrate_schema(engine: AsyncEngine) -> None:
|
||||
)
|
||||
logger.info("Added %s column to tracking_config table", col_name)
|
||||
|
||||
# Add NUT (UPS) tracking flags to tracking_config if missing
|
||||
if await _has_table(conn, "tracking_config"):
|
||||
nut_flags = [
|
||||
("track_ups_online", "INTEGER DEFAULT 1"),
|
||||
("track_ups_on_battery", "INTEGER DEFAULT 1"),
|
||||
("track_ups_low_battery", "INTEGER DEFAULT 1"),
|
||||
("track_ups_battery_restored", "INTEGER DEFAULT 1"),
|
||||
("track_ups_comms_lost", "INTEGER DEFAULT 1"),
|
||||
("track_ups_comms_restored", "INTEGER DEFAULT 1"),
|
||||
("track_ups_replace_battery", "INTEGER DEFAULT 1"),
|
||||
("track_ups_overload", "INTEGER DEFAULT 1"),
|
||||
]
|
||||
for col_name, col_type in nut_flags:
|
||||
if not await _has_column(conn, "tracking_config", col_name):
|
||||
await conn.execute(
|
||||
text(f"ALTER TABLE tracking_config ADD COLUMN {col_name} {col_type}")
|
||||
)
|
||||
logger.info("Added %s column to tracking_config table", col_name)
|
||||
|
||||
# Drop legacy template content columns from template_config
|
||||
# (template content moved to template_slot child rows)
|
||||
if await _has_table(conn, "template_config"):
|
||||
legacy_cols = [
|
||||
"message_assets_added", "message_assets_removed",
|
||||
"message_collection_renamed", "message_collection_deleted",
|
||||
"message_sharing_changed", "periodic_summary_message",
|
||||
"scheduled_assets_message", "memory_mode_message",
|
||||
]
|
||||
for col_name in legacy_cols:
|
||||
if await _has_column(conn, "template_config", col_name):
|
||||
await conn.execute(
|
||||
text(f"ALTER TABLE template_config DROP COLUMN {col_name}")
|
||||
)
|
||||
logger.info("Dropped legacy column %s from template_config", col_name)
|
||||
|
||||
# Add collection_name and shared to tracker_state if missing
|
||||
state_table = "notification_tracker_state" if await _has_table(conn, "notification_tracker_state") else "tracker_state"
|
||||
if await _has_table(conn, state_table):
|
||||
|
||||
@@ -150,6 +150,16 @@ class TrackingConfig(SQLModel, table=True):
|
||||
# Scheduler event tracking
|
||||
track_scheduled_message: bool = Field(default=True)
|
||||
|
||||
# NUT (UPS) event tracking
|
||||
track_ups_online: bool = Field(default=True)
|
||||
track_ups_on_battery: bool = Field(default=True)
|
||||
track_ups_low_battery: bool = Field(default=True)
|
||||
track_ups_battery_restored: bool = Field(default=True)
|
||||
track_ups_comms_lost: bool = Field(default=True)
|
||||
track_ups_comms_restored: bool = Field(default=True)
|
||||
track_ups_replace_battery: bool = Field(default=True)
|
||||
track_ups_overload: bool = Field(default=True)
|
||||
|
||||
# Immich asset display
|
||||
track_images: bool = Field(default=True)
|
||||
track_videos: bool = Field(default=True)
|
||||
|
||||
@@ -151,6 +151,7 @@ async def _seed_default_templates() -> None:
|
||||
await _seed_provider_template(session, "gitea", "Gitea")
|
||||
await _seed_provider_template(session, "planka", "Planka")
|
||||
await _seed_provider_template(session, "scheduler", "Scheduler")
|
||||
await _seed_provider_template(session, "nut", "NUT")
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -171,6 +172,9 @@ async def _seed_default_command_templates() -> None:
|
||||
await _seed_provider_command_template(
|
||||
session, "planka", "Default Planka Commands", "Default Planka command templates",
|
||||
)
|
||||
await _seed_provider_command_template(
|
||||
session, "nut", "Default NUT Commands", "Default NUT command templates",
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@@ -221,6 +225,18 @@ async def _seed_default_tracking_configs() -> None:
|
||||
"name": "Default Scheduler",
|
||||
"track_scheduled_message": True,
|
||||
},
|
||||
{
|
||||
"provider_type": "nut",
|
||||
"name": "Default NUT",
|
||||
"track_ups_online": True,
|
||||
"track_ups_on_battery": True,
|
||||
"track_ups_low_battery": True,
|
||||
"track_ups_battery_restored": True,
|
||||
"track_ups_comms_lost": True,
|
||||
"track_ups_comms_restored": True,
|
||||
"track_ups_replace_battery": True,
|
||||
"track_ups_overload": True,
|
||||
},
|
||||
]
|
||||
|
||||
for cfg in defaults:
|
||||
@@ -279,6 +295,16 @@ async def _seed_default_command_configs() -> None:
|
||||
"default_count": 10,
|
||||
"rate_limits": {"api": 15, "default": 10},
|
||||
},
|
||||
{
|
||||
"provider_type": "nut",
|
||||
"name": "Default NUT",
|
||||
"enabled_commands": [
|
||||
"help", "status", "devices", "battery",
|
||||
],
|
||||
"response_mode": "text",
|
||||
"default_count": 5,
|
||||
"rate_limits": {"api": 15, "default": 10},
|
||||
},
|
||||
]
|
||||
|
||||
for cfg in defaults:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from notify_bridge_core.providers.immich import ImmichServiceProvider
|
||||
from notify_bridge_core.providers.gitea import GiteaServiceProvider
|
||||
from notify_bridge_core.providers.planka import PlankaServiceProvider
|
||||
from notify_bridge_core.providers.nut import NutServiceProvider
|
||||
|
||||
from ..database.models import ServiceProvider
|
||||
|
||||
@@ -39,3 +40,15 @@ def make_planka_provider(http_session, provider: ServiceProvider) -> PlankaServi
|
||||
config.get("api_key", ""),
|
||||
provider.name,
|
||||
)
|
||||
|
||||
|
||||
def make_nut_provider(provider: ServiceProvider) -> NutServiceProvider:
|
||||
"""Create a NutServiceProvider from a DB provider model."""
|
||||
config = provider.config or {}
|
||||
return NutServiceProvider(
|
||||
host=config.get("host", "localhost"),
|
||||
port=config.get("port", 3493),
|
||||
username=config.get("username"),
|
||||
password=config.get("password"),
|
||||
name=provider.name,
|
||||
)
|
||||
|
||||
@@ -83,6 +83,15 @@ def event_allowed_by_config(event: ServiceEvent, tc: TrackingConfig) -> bool:
|
||||
"task_completed": tc.track_task_completed,
|
||||
# Scheduler events
|
||||
"scheduled_message": tc.track_scheduled_message,
|
||||
# NUT (UPS) events
|
||||
"ups_online": tc.track_ups_online,
|
||||
"ups_on_battery": tc.track_ups_on_battery,
|
||||
"ups_low_battery": tc.track_ups_low_battery,
|
||||
"ups_battery_restored": tc.track_ups_battery_restored,
|
||||
"ups_comms_lost": tc.track_ups_comms_lost,
|
||||
"ups_comms_restored": tc.track_ups_comms_restored,
|
||||
"ups_replace_battery": tc.track_ups_replace_battery,
|
||||
"ups_overload": tc.track_ups_overload,
|
||||
}
|
||||
return flag_map.get(event_type, True)
|
||||
|
||||
|
||||
@@ -123,6 +123,16 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
|
||||
custom_variables=custom_vars,
|
||||
)
|
||||
events, new_state = await sched.poll(collection_ids, state_dict)
|
||||
elif provider_type == "nut":
|
||||
from notify_bridge_core.providers.nut import NutServiceProvider
|
||||
nut = NutServiceProvider(
|
||||
host=provider_config.get("host", "localhost"),
|
||||
port=provider_config.get("port", 3493),
|
||||
username=provider_config.get("username"),
|
||||
password=provider_config.get("password"),
|
||||
name=provider_name,
|
||||
)
|
||||
events, new_state = await nut.poll(collection_ids, state_dict)
|
||||
else:
|
||||
return {"status": "error", "reason": f"unsupported provider type: {provider_type}"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user