mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Add REST/Webhook smart plug type (#472)
This commit is contained in:
@@ -1072,7 +1072,7 @@ export interface CloudDevice {
|
||||
export interface SmartPlug {
|
||||
id: number;
|
||||
name: string;
|
||||
plug_type: 'tasmota' | 'homeassistant' | 'mqtt';
|
||||
plug_type: 'tasmota' | 'homeassistant' | 'mqtt' | 'rest';
|
||||
ip_address: string | null; // Required for Tasmota
|
||||
ha_entity_id: string | null; // Required for Home Assistant (e.g., "switch.printer_plug", "script.turn_on_printer")
|
||||
// Home Assistant energy sensor entities (optional)
|
||||
@@ -1095,6 +1095,18 @@ export interface SmartPlug {
|
||||
mqtt_state_topic: string | null; // Topic for state data
|
||||
mqtt_state_path: string | null; // e.g., "state_l1" for ON/OFF
|
||||
mqtt_state_on_value: string | null; // What value means "ON" (e.g., "ON", "true", "1")
|
||||
// REST/Webhook fields (required when plug_type="rest")
|
||||
rest_on_url: string | null;
|
||||
rest_on_body: string | null;
|
||||
rest_off_url: string | null;
|
||||
rest_off_body: string | null;
|
||||
rest_method: string | null;
|
||||
rest_headers: string | null;
|
||||
rest_status_url: string | null;
|
||||
rest_status_path: string | null;
|
||||
rest_status_on_value: string | null;
|
||||
rest_power_path: string | null;
|
||||
rest_energy_path: string | null;
|
||||
printer_id: number | null;
|
||||
enabled: boolean;
|
||||
auto_on: boolean;
|
||||
@@ -1127,7 +1139,7 @@ export interface SmartPlug {
|
||||
|
||||
export interface SmartPlugCreate {
|
||||
name: string;
|
||||
plug_type?: 'tasmota' | 'homeassistant' | 'mqtt';
|
||||
plug_type?: 'tasmota' | 'homeassistant' | 'mqtt' | 'rest';
|
||||
ip_address?: string | null; // Required for Tasmota
|
||||
ha_entity_id?: string | null; // Required for Home Assistant
|
||||
// Home Assistant energy sensor entities (optional)
|
||||
@@ -1150,6 +1162,18 @@ export interface SmartPlugCreate {
|
||||
mqtt_state_topic?: string | null;
|
||||
mqtt_state_path?: string | null;
|
||||
mqtt_state_on_value?: string | null;
|
||||
// REST fields
|
||||
rest_on_url?: string | null;
|
||||
rest_on_body?: string | null;
|
||||
rest_off_url?: string | null;
|
||||
rest_off_body?: string | null;
|
||||
rest_method?: string | null;
|
||||
rest_headers?: string | null;
|
||||
rest_status_url?: string | null;
|
||||
rest_status_path?: string | null;
|
||||
rest_status_on_value?: string | null;
|
||||
rest_power_path?: string | null;
|
||||
rest_energy_path?: string | null;
|
||||
printer_id?: number | null;
|
||||
enabled?: boolean;
|
||||
auto_on?: boolean;
|
||||
@@ -1175,7 +1199,7 @@ export interface SmartPlugCreate {
|
||||
|
||||
export interface SmartPlugUpdate {
|
||||
name?: string;
|
||||
plug_type?: 'tasmota' | 'homeassistant' | 'mqtt';
|
||||
plug_type?: 'tasmota' | 'homeassistant' | 'mqtt' | 'rest';
|
||||
ip_address?: string | null;
|
||||
ha_entity_id?: string | null;
|
||||
// Home Assistant energy sensor entities (optional)
|
||||
@@ -1197,6 +1221,18 @@ export interface SmartPlugUpdate {
|
||||
mqtt_state_topic?: string | null;
|
||||
mqtt_state_path?: string | null;
|
||||
mqtt_state_on_value?: string | null;
|
||||
// REST fields
|
||||
rest_on_url?: string | null;
|
||||
rest_on_body?: string | null;
|
||||
rest_off_url?: string | null;
|
||||
rest_off_body?: string | null;
|
||||
rest_method?: string | null;
|
||||
rest_headers?: string | null;
|
||||
rest_status_url?: string | null;
|
||||
rest_status_path?: string | null;
|
||||
rest_status_on_value?: string | null;
|
||||
rest_power_path?: string | null;
|
||||
rest_energy_path?: string | null;
|
||||
printer_id?: number | null;
|
||||
enabled?: boolean;
|
||||
auto_on?: boolean;
|
||||
@@ -3362,6 +3398,13 @@ export const api = {
|
||||
getHASensorEntities: () =>
|
||||
request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
|
||||
|
||||
// REST smart plug
|
||||
testRESTConnection: (url: string, method: string = 'GET', headers?: string | null) =>
|
||||
request<{ success: boolean; error: string | null }>('/smart-plugs/rest/test-connection', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ url, method, headers }),
|
||||
}),
|
||||
|
||||
// Print Queue
|
||||
getQueue: (printerId?: number, status?: string, targetModel?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye } from 'lucide-react';
|
||||
import { X, Save, Loader2, Wifi, WifiOff, CheckCircle, Bell, Clock, LayoutGrid, Search, Plug, Power, Home, Radio, Eye, Globe } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../api/client';
|
||||
import type { SmartPlug, SmartPlugCreate, SmartPlugUpdate, DiscoveredTasmotaDevice } from '../api/client';
|
||||
@@ -17,7 +17,7 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
const isEditing = !!plug;
|
||||
|
||||
// Plug type selection
|
||||
const [plugType, setPlugType] = useState<'tasmota' | 'homeassistant' | 'mqtt'>(plug?.plug_type || 'tasmota');
|
||||
const [plugType, setPlugType] = useState<'tasmota' | 'homeassistant' | 'mqtt' | 'rest'>(plug?.plug_type || 'tasmota');
|
||||
|
||||
const [name, setName] = useState(plug?.name || '');
|
||||
// Tasmota fields
|
||||
@@ -42,6 +42,18 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
const [mqttStateTopic, setMqttStateTopic] = useState(plug?.mqtt_state_topic || '');
|
||||
const [mqttStatePath, setMqttStatePath] = useState(plug?.mqtt_state_path || '');
|
||||
const [mqttStateOnValue, setMqttStateOnValue] = useState(plug?.mqtt_state_on_value || '');
|
||||
// REST fields
|
||||
const [restOnUrl, setRestOnUrl] = useState(plug?.rest_on_url || '');
|
||||
const [restOnBody, setRestOnBody] = useState(plug?.rest_on_body || '');
|
||||
const [restOffUrl, setRestOffUrl] = useState(plug?.rest_off_url || '');
|
||||
const [restOffBody, setRestOffBody] = useState(plug?.rest_off_body || '');
|
||||
const [restMethod, setRestMethod] = useState(plug?.rest_method || 'POST');
|
||||
const [restHeaders, setRestHeaders] = useState(plug?.rest_headers || '');
|
||||
const [restStatusUrl, setRestStatusUrl] = useState(plug?.rest_status_url || '');
|
||||
const [restStatusPath, setRestStatusPath] = useState(plug?.rest_status_path || '');
|
||||
const [restStatusOnValue, setRestStatusOnValue] = useState(plug?.rest_status_on_value || '');
|
||||
const [restPowerPath, setRestPowerPath] = useState(plug?.rest_power_path || '');
|
||||
const [restEnergyPath, setRestEnergyPath] = useState(plug?.rest_energy_path || '');
|
||||
// HA energy sensor entities (optional)
|
||||
const [haPowerEntity, setHaPowerEntity] = useState(plug?.ha_power_entity || '');
|
||||
const [haEnergyTodayEntity, setHaEnergyTodayEntity] = useState(plug?.ha_energy_today_entity || '');
|
||||
@@ -321,6 +333,13 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (plugType === 'rest') {
|
||||
if (!restOnUrl.trim() && !restOffUrl.trim()) {
|
||||
setError(t('smartPlugs.restUrlRequired'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: name.trim(),
|
||||
plug_type: plugType,
|
||||
@@ -342,6 +361,18 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
mqtt_state_topic: plugType === 'mqtt' ? (mqttStateTopic.trim() || null) : null,
|
||||
mqtt_state_path: plugType === 'mqtt' ? (mqttStatePath.trim() || null) : null,
|
||||
mqtt_state_on_value: plugType === 'mqtt' ? (mqttStateOnValue.trim() || null) : null,
|
||||
// REST fields
|
||||
rest_on_url: plugType === 'rest' ? (restOnUrl.trim() || null) : null,
|
||||
rest_on_body: plugType === 'rest' ? (restOnBody.trim() || null) : null,
|
||||
rest_off_url: plugType === 'rest' ? (restOffUrl.trim() || null) : null,
|
||||
rest_off_body: plugType === 'rest' ? (restOffBody.trim() || null) : null,
|
||||
rest_method: plugType === 'rest' ? restMethod : null,
|
||||
rest_headers: plugType === 'rest' ? (restHeaders.trim() || null) : null,
|
||||
rest_status_url: plugType === 'rest' ? (restStatusUrl.trim() || null) : null,
|
||||
rest_status_path: plugType === 'rest' ? (restStatusPath.trim() || null) : null,
|
||||
rest_status_on_value: plugType === 'rest' ? (restStatusOnValue.trim() || null) : null,
|
||||
rest_power_path: plugType === 'rest' ? (restPowerPath.trim() || null) : null,
|
||||
rest_energy_path: plugType === 'rest' ? (restEnergyPath.trim() || null) : null,
|
||||
username: plugType === 'tasmota' ? (username.trim() || null) : null,
|
||||
password: plugType === 'tasmota' ? (password.trim() || null) : null,
|
||||
printer_id: printerId,
|
||||
@@ -448,6 +479,22 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
<Radio className="w-4 h-4" />
|
||||
MQTT
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPlugType('rest');
|
||||
setTestResult(null);
|
||||
setError(null);
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2.5 rounded-lg font-medium transition-colors ${
|
||||
plugType === 'rest'
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark text-bambu-gray hover:text-white border border-bambu-dark-tertiary'
|
||||
}`}
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
REST
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1074,6 +1121,184 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* REST API Section */}
|
||||
{plugType === 'rest' && (
|
||||
<div className="space-y-3">
|
||||
{/* Control Section */}
|
||||
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
|
||||
<p className="text-white font-medium text-sm">{t('smartPlugs.restControl')}</p>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restMethod')}</label>
|
||||
<select
|
||||
value={restMethod}
|
||||
onChange={(e) => setRestMethod(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOnUrl')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restOnUrl}
|
||||
onChange={(e) => { setRestOnUrl(e.target.value); setTestResult(null); }}
|
||||
placeholder="http://openhab:8080/rest/items/MyPlug"
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOnBody')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={restOnBody}
|
||||
onChange={(e) => setRestOnBody(e.target.value)}
|
||||
placeholder={t('smartPlugs.restBodyHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOffUrl')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restOffUrl}
|
||||
onChange={(e) => { setRestOffUrl(e.target.value); setTestResult(null); }}
|
||||
placeholder="http://openhab:8080/rest/items/MyPlug"
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restOffBody')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={restOffBody}
|
||||
onChange={(e) => setRestOffBody(e.target.value)}
|
||||
placeholder={t('smartPlugs.restBodyHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Headers Section */}
|
||||
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
|
||||
<p className="text-white font-medium text-sm">{t('smartPlugs.restHeaders')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
|
||||
<div>
|
||||
<textarea
|
||||
value={restHeaders}
|
||||
onChange={(e) => setRestHeaders(e.target.value)}
|
||||
placeholder={t('smartPlugs.restHeadersHint')}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Polling Section (optional) */}
|
||||
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
|
||||
<p className="text-white font-medium text-sm">{t('smartPlugs.stateMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusUrl')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restStatusUrl}
|
||||
onChange={(e) => setRestStatusUrl(e.target.value)}
|
||||
placeholder={t('smartPlugs.restStatusHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusPath')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restStatusPath}
|
||||
onChange={(e) => setRestStatusPath(e.target.value)}
|
||||
placeholder={t('smartPlugs.restPathHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restStatusOnValue')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restStatusOnValue}
|
||||
onChange={(e) => setRestStatusOnValue(e.target.value)}
|
||||
placeholder="ON"
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Energy Monitoring (optional) */}
|
||||
<div className="space-y-3 p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary">
|
||||
<p className="text-white font-medium text-sm">{t('smartPlugs.energyMonitoring')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerPath')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restPowerPath}
|
||||
onChange={(e) => setRestPowerPath(e.target.value)}
|
||||
placeholder={t('smartPlugs.restPathHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyPath')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={restEnergyPath}
|
||||
onChange={(e) => setRestEnergyPath(e.target.value)}
|
||||
placeholder={t('smartPlugs.restPathHint')}
|
||||
className="w-full px-3 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
{t('smartPlugs.restEnergyHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Test Connection */}
|
||||
{(restOnUrl.trim() || restOffUrl.trim()) && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setTestResult(null);
|
||||
try {
|
||||
const url = restOnUrl.trim() || restOffUrl.trim();
|
||||
const result = await api.testRESTConnection(url, restMethod, restHeaders.trim() || null);
|
||||
setTestResult({ success: result.success });
|
||||
if (!result.success) {
|
||||
setError(result.error || t('smartPlugs.addSmartPlug.connectionFailed'));
|
||||
}
|
||||
} catch {
|
||||
setTestResult({ success: false });
|
||||
setError(t('smartPlugs.addSmartPlug.connectionFailed'));
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Wifi className="w-4 h-4" />
|
||||
{t('smartPlugs.testConnection')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{testResult && (
|
||||
<div className={`flex items-center gap-2 text-sm ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
|
||||
{testResult.success ? t('smartPlugs.connectionSuccess') : t('smartPlugs.addSmartPlug.connectionFailed')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IP Address - only show for Tasmota */}
|
||||
{plugType === 'tasmota' && (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||
import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye } from 'lucide-react';
|
||||
import { Plug, Power, PowerOff, Loader2, Trash2, Settings2, Thermometer, Clock, Wifi, WifiOff, Edit2, Bell, Calendar, LayoutGrid, ExternalLink, Home, Radio, Eye, Globe } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../api/client';
|
||||
import type { SmartPlug, SmartPlugUpdate } from '../api/client';
|
||||
@@ -133,6 +133,8 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
<Radio className={`w-5 h-5 ${isReachable ? 'text-teal-400' : 'text-red-400'}`} />
|
||||
) : plug.plug_type === 'homeassistant' ? (
|
||||
<Home className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
) : plug.plug_type === 'rest' ? (
|
||||
<Globe className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
) : (
|
||||
<Plug className={`w-5 h-5 ${isReachable ? (isOn ? 'text-bambu-green' : 'text-bambu-gray') : 'text-red-400'}`} />
|
||||
)}
|
||||
@@ -141,9 +143,9 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
<h3 className="font-medium text-white truncate">{plug.name}</h3>
|
||||
<p
|
||||
className="text-sm text-bambu-gray truncate"
|
||||
title={plug.plug_type === 'mqtt' ? plug.mqtt_topic ?? undefined : plug.plug_type === 'homeassistant' ? plug.ha_entity_id ?? undefined : plug.ip_address ?? undefined}
|
||||
title={plug.plug_type === 'mqtt' ? plug.mqtt_topic ?? undefined : plug.plug_type === 'homeassistant' ? plug.ha_entity_id ?? undefined : plug.plug_type === 'rest' ? plug.rest_on_url ?? plug.rest_off_url ?? undefined : plug.ip_address ?? undefined}
|
||||
>
|
||||
{plug.plug_type === 'mqtt' ? plug.mqtt_topic : plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.ip_address}
|
||||
{plug.plug_type === 'mqtt' ? plug.mqtt_topic : plug.plug_type === 'homeassistant' ? plug.ha_entity_id : plug.plug_type === 'rest' ? (plug.rest_on_url || plug.rest_off_url) : plug.ip_address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,6 +167,13 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
|
||||
{isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
|
||||
</span>
|
||||
</div>
|
||||
) : plug.plug_type === 'rest' ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<span className="px-1 py-0.5 bg-purple-500/20 text-purple-400 text-[10px] font-medium rounded">REST</span>
|
||||
<span className={isReachable ? (isOn ? 'text-status-ok' : 'text-bambu-gray') : 'text-status-error'}>
|
||||
{isReachable ? (status?.state || '?') : t('smartPlugs.offline')}
|
||||
</span>
|
||||
</div>
|
||||
) : isReachable ? (
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Wifi className="w-4 h-4 text-status-ok" />
|
||||
|
||||
@@ -3837,6 +3837,27 @@ export default {
|
||||
mqttPowerHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload (z.B. "power_l1"). Leer lassen wenn Topic rohe numerische Werte sendet.\nMultiplikator 0.001 für mW→W, 1000 für kW→W verwenden.',
|
||||
mqttEnergyHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload. Leer lassen für rohe Werte.\nMultiplikator 0.001 für Wh→kWh, 1000 für MWh→kWh verwenden.',
|
||||
mqttStateHint: 'JSON-Pfad extrahiert Wert aus JSON-Payload. Leer lassen für rohe Werte.\nEIN-Wert: der genaue String der "EIN" bedeutet. Leer lassen für Auto-Erkennung (ON, true, 1).',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'Keine Schalter in der Schaltleiste',
|
||||
enableSwitchbarHint: '"In Schaltleiste anzeigen" unter Einstellungen > Smart Plugs aktivieren',
|
||||
},
|
||||
|
||||
@@ -3843,6 +3843,27 @@ export default {
|
||||
mqttPowerHint: 'JSON path extracts value from JSON payload (e.g., "power_l1"). Leave empty if topic publishes raw numeric values.\nUse multiplier 0.001 for mW→W, 1000 for kW→W.',
|
||||
mqttEnergyHint: 'JSON path extracts value from JSON payload. Leave empty for raw values.\nUse multiplier 0.001 for Wh→kWh, 1000 for MWh→kWh.',
|
||||
mqttStateHint: 'JSON path extracts value from JSON payload. Leave empty for raw values.\nON value: the exact string that means "ON". Leave empty for auto-detect (ON, true, 1).',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'No switches in switchbar',
|
||||
enableSwitchbarHint: 'Enable "Show in Switchbar" in Settings > Smart Plugs',
|
||||
},
|
||||
|
||||
@@ -3829,6 +3829,27 @@ export default {
|
||||
mqttPowerHint: 'Le chemin JSON extrait la valeur du payload JSON (ex: "power_l1"). Laisser vide si le topic publie des valeurs numériques brutes.\nUtiliser le multiplicateur 0.001 pour mW→W, 1000 pour kW→W.',
|
||||
mqttEnergyHint: 'Le chemin JSON extrait la valeur du payload JSON. Laisser vide pour les valeurs brutes.\nUtiliser le multiplicateur 0.001 pour Wh→kWh, 1000 pour MWh→kWh.',
|
||||
mqttStateHint: 'Le chemin JSON extrait la valeur du payload JSON. Laisser vide pour les valeurs brutes.\nValeur ON : la chaîne exacte signifiant "ON". Laisser vide pour la détection auto (ON, true, 1).',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'Aucun commutateur dans la barre',
|
||||
enableSwitchbarHint: 'Activez "Afficher dans la barre de commutateurs" dans Paramètres > Smart Plugs',
|
||||
},
|
||||
|
||||
@@ -3828,6 +3828,27 @@ export default {
|
||||
mqttPowerHint: 'Il percorso JSON estrae il valore dal payload JSON (es. "power_l1"). Lascia vuoto se il topic pubblica valori numerici grezzi.\nUsa moltiplicatore 0.001 per mW→W, 1000 per kW→W.',
|
||||
mqttEnergyHint: 'Il percorso JSON estrae il valore dal payload JSON. Lascia vuoto per valori grezzi.\nUsa moltiplicatore 0.001 per Wh→kWh, 1000 per MWh→kWh.',
|
||||
mqttStateHint: 'Il percorso JSON estrae il valore dal payload JSON. Lascia vuoto per valori grezzi.\nValore ON: la stringa esatta che significa "ON". Lascia vuoto per rilevamento auto (ON, true, 1).',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'Nessun interruttore nella barra',
|
||||
enableSwitchbarHint: 'Abilita "Mostra nella barra interruttori" in Impostazioni > Smart Plugs',
|
||||
},
|
||||
|
||||
@@ -3841,6 +3841,27 @@ export default {
|
||||
mqttPowerHint: 'JSONパスはJSONペイロードから値を抽出します(例: "power_l1")。トピックが生の数値を送信する場合は空のままにしてください。\n乗数: mW→Wは0.001、kW→Wは1000を使用。',
|
||||
mqttEnergyHint: 'JSONパスはJSONペイロードから値を抽出します。生の値の場合は空のままにしてください。\n乗数: Wh→kWhは0.001、MWh→kWhは1000を使用。',
|
||||
mqttStateHint: 'JSONパスはJSONペイロードから値を抽出します。生の値の場合は空のままにしてください。\nON値: "ON"を意味する正確な文字列。自動検出(ON、true、1)の場合は空のままにしてください。',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'スイッチバーにスイッチがありません',
|
||||
enableSwitchbarHint: '設定 > スマートプラグで「スイッチバーに表示」を有効にしてください',
|
||||
},
|
||||
|
||||
@@ -3828,6 +3828,27 @@ export default {
|
||||
mqttPowerHint: 'O caminho JSON extrai o valor do payload JSON (ex: "power_l1"). Deixe vazio se o tópico publica valores numéricos brutos.\nUse multiplicador 0.001 para mW→W, 1000 para kW→W.',
|
||||
mqttEnergyHint: 'O caminho JSON extrai o valor do payload JSON. Deixe vazio para valores brutos.\nUse multiplicador 0.001 para Wh→kWh, 1000 para MWh→kWh.',
|
||||
mqttStateHint: 'O caminho JSON extrai o valor do payload JSON. Deixe vazio para valores brutos.\nValor ON: a string exata que significa "ON". Deixe vazio para detecção automática (ON, true, 1).',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: 'Nenhum interruptor na barra',
|
||||
enableSwitchbarHint: 'Ative "Mostrar na barra de interruptores" em Configurações > Smart Plugs',
|
||||
},
|
||||
|
||||
@@ -3828,6 +3828,27 @@ export default {
|
||||
mqttPowerHint: 'JSON路径从JSON负载中提取值(例如"power_l1")。如果主题发布原始数值,请留空。\n乘数:mW→W使用0.001,kW→W使用1000。',
|
||||
mqttEnergyHint: 'JSON路径从JSON负载中提取值。原始值请留空。\n乘数:Wh→kWh使用0.001,MWh→kWh使用1000。',
|
||||
mqttStateHint: 'JSON路径从JSON负载中提取值。原始值请留空。\nON值:表示"ON"的确切字符串。留空以自动检测(ON、true、1)。',
|
||||
// REST smart plug
|
||||
restControl: 'Control',
|
||||
restOnUrl: 'Turn ON URL',
|
||||
restOffUrl: 'Turn OFF URL',
|
||||
restOnBody: 'ON Request Body',
|
||||
restOffBody: 'OFF Request Body',
|
||||
restMethod: 'HTTP Method',
|
||||
restHeaders: 'Custom Headers (JSON)',
|
||||
restStatusUrl: 'Status URL',
|
||||
restStatusPath: 'State JSON Path',
|
||||
restStatusOnValue: 'ON Value',
|
||||
restPowerPath: 'Power JSON Path',
|
||||
restEnergyPath: 'Energy JSON Path',
|
||||
restUrlRequired: 'At least one URL (ON or OFF) is required for REST plugs',
|
||||
restHeadersHint: 'e.g. {"Authorization": "Bearer your-token"}',
|
||||
restBodyHint: 'e.g. ON, {"state": "on"}',
|
||||
restStatusHint: 'URL to poll for current state',
|
||||
restPathHint: 'e.g. state or data.power.status',
|
||||
restEnergyHint: 'JSON paths to extract power (watts) and energy (kWh) from the status response.',
|
||||
testConnection: 'Test Connection',
|
||||
connectionSuccess: 'Connection successful',
|
||||
noSwitchesInSwitchbar: '开关栏中没有开关',
|
||||
enableSwitchbarHint: '在设置 > 智能插座中启用"在开关栏显示"',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user