Add separate power/energy URLs and multipliers for REST smart plugs (#472)

REST/Webhook smart plugs can now fetch power and energy data from
  individual URLs instead of requiring all values in a single status
  response. Each value falls back to the shared Status URL when no
  separate URL is set, preserving backward compatibility. Added power
  and energy multipliers for unit conversion (e.g. 0.001 for Wh→kWh).
This commit is contained in:
maziggy
2026-04-03 08:31:08 +02:00
parent d327c70006
commit 76adf70fd4
18 changed files with 391 additions and 144 deletions
+12
View File
@@ -1111,8 +1111,12 @@ export interface SmartPlug {
rest_status_url: string | null;
rest_status_path: string | null;
rest_status_on_value: string | null;
rest_power_url: string | null;
rest_power_path: string | null;
rest_power_multiplier: number;
rest_energy_url: string | null;
rest_energy_path: string | null;
rest_energy_multiplier: number;
printer_id: number | null;
enabled: boolean;
auto_on: boolean;
@@ -1178,8 +1182,12 @@ export interface SmartPlugCreate {
rest_status_url?: string | null;
rest_status_path?: string | null;
rest_status_on_value?: string | null;
rest_power_url?: string | null;
rest_power_path?: string | null;
rest_power_multiplier?: number;
rest_energy_url?: string | null;
rest_energy_path?: string | null;
rest_energy_multiplier?: number;
printer_id?: number | null;
enabled?: boolean;
auto_on?: boolean;
@@ -1237,8 +1245,12 @@ export interface SmartPlugUpdate {
rest_status_url?: string | null;
rest_status_path?: string | null;
rest_status_on_value?: string | null;
rest_power_url?: string | null;
rest_power_path?: string | null;
rest_power_multiplier?: number;
rest_energy_url?: string | null;
rest_energy_path?: string | null;
rest_energy_multiplier?: number;
printer_id?: number | null;
enabled?: boolean;
auto_on?: boolean;
@@ -52,8 +52,12 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
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 [restPowerUrl, setRestPowerUrl] = useState(plug?.rest_power_url || '');
const [restPowerPath, setRestPowerPath] = useState(plug?.rest_power_path || '');
const [restPowerMultiplier, setRestPowerMultiplier] = useState<string>((plug?.rest_power_multiplier ?? 1).toString());
const [restEnergyUrl, setRestEnergyUrl] = useState(plug?.rest_energy_url || '');
const [restEnergyPath, setRestEnergyPath] = useState(plug?.rest_energy_path || '');
const [restEnergyMultiplier, setRestEnergyMultiplier] = useState<string>((plug?.rest_energy_multiplier ?? 1).toString());
// HA energy sensor entities (optional)
const [haPowerEntity, setHaPowerEntity] = useState(plug?.ha_power_entity || '');
const [haEnergyTodayEntity, setHaEnergyTodayEntity] = useState(plug?.ha_energy_today_entity || '');
@@ -371,8 +375,12 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
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_url: plugType === 'rest' ? (restPowerUrl.trim() || null) : null,
rest_power_path: plugType === 'rest' ? (restPowerPath.trim() || null) : null,
rest_power_multiplier: plugType === 'rest' ? (parseFloat(restPowerMultiplier) || 1) : 1,
rest_energy_url: plugType === 'rest' ? (restEnergyUrl.trim() || null) : null,
rest_energy_path: plugType === 'rest' ? (restEnergyPath.trim() || null) : null,
rest_energy_multiplier: plugType === 'rest' ? (parseFloat(restEnergyMultiplier) || 1) : 1,
username: plugType === 'tasmota' ? (username.trim() || null) : null,
password: plugType === 'tasmota' ? (password.trim() || null) : null,
printer_id: printerId,
@@ -1236,6 +1244,18 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
{/* 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>
{/* Power */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restPowerUrl')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restPowerUrl}
onChange={(e) => setRestPowerUrl(e.target.value)}
placeholder={t('smartPlugs.restPowerUrlHint')}
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.restPowerPath')}</label>
@@ -1247,6 +1267,30 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
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.restPowerMultiplier')}</label>
<input
type="text"
value={restPowerMultiplier}
onChange={(e) => setRestPowerMultiplier(e.target.value)}
placeholder="1"
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>
{/* Energy */}
<div>
<label className="block text-sm text-bambu-gray mb-1">{t('smartPlugs.restEnergyUrl')} <span className="text-bambu-gray font-normal">({t('smartPlugs.optional')})</span></label>
<input
type="text"
value={restEnergyUrl}
onChange={(e) => setRestEnergyUrl(e.target.value)}
placeholder={t('smartPlugs.restEnergyUrlHint')}
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.restEnergyPath')}</label>
<input
@@ -1257,7 +1301,18 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
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.restEnergyMultiplier')}</label>
<input
type="text"
value={restEnergyMultiplier}
onChange={(e) => setRestEnergyMultiplier(e.target.value)}
placeholder="1"
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>
+7 -1
View File
@@ -3874,14 +3874,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: 'Power URL',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: 'Power Multiplikator',
restEnergyUrl: 'Energie URL',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'Energie Multiplikator',
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.',
restPowerUrlHint: 'Eigene URL für Leistungsdaten (nutzt Status URL wenn leer)',
restEnergyUrlHint: 'Eigene URL für Energiedaten (nutzt Status URL wenn leer)',
restEnergyHint: 'Jeder Wert kann eine eigene URL verwenden oder auf die Status URL zurückgreifen. Multiplikatoren für Einheitenumrechnung verwenden (z.B. 0.001 für Wh zu kWh).',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'Keine Schalter in der Schaltleiste',
+7 -1
View File
@@ -3880,14 +3880,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: 'Power URL',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: 'Power Multiplier',
restEnergyUrl: 'Energy URL',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'Energy Multiplier',
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.',
restPowerUrlHint: 'Separate URL for power data (uses Status URL if empty)',
restEnergyUrlHint: 'Separate URL for energy data (uses Status URL if empty)',
restEnergyHint: 'Each value can use its own URL or fall back to the Status URL. Use multipliers for unit conversion (e.g. 0.001 to convert Wh to kWh).',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'No switches in switchbar',
+7 -1
View File
@@ -3866,14 +3866,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: 'URL de puissance',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: 'Multiplicateur de puissance',
restEnergyUrl: 'URL d\'énergie',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'Multiplicateur d\'énergie',
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.',
restPowerUrlHint: 'URL séparée pour les données de puissance (utilise l\'URL de statut si vide)',
restEnergyUrlHint: 'URL séparée pour les données d\'énergie (utilise l\'URL de statut si vide)',
restEnergyHint: 'Chaque valeur peut utiliser sa propre URL ou se rabattre sur l\'URL de statut. Utilisez les multiplicateurs pour la conversion d\'unités (ex : 0.001 pour convertir Wh en kWh).',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'Aucun commutateur dans la barre',
+7 -1
View File
@@ -3865,14 +3865,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: 'URL potenza',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: 'Moltiplicatore potenza',
restEnergyUrl: 'URL energia',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'Moltiplicatore energia',
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.',
restPowerUrlHint: 'URL separato per i dati di potenza (usa l\'URL di stato se vuoto)',
restEnergyUrlHint: 'URL separato per i dati di energia (usa l\'URL di stato se vuoto)',
restEnergyHint: 'Ogni valore può usare il proprio URL o ricadere sull\'URL di stato. Usa i moltiplicatori per la conversione delle unità (es. 0.001 per convertire Wh in kWh).',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'Nessun interruttore nella barra',
+7 -1
View File
@@ -3878,14 +3878,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: '電力URL',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: '電力乗数',
restEnergyUrl: 'エネルギーURL',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'エネルギー乗数',
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.',
restPowerUrlHint: '電力データ用の個別URL(空欄の場合はステータスURLを使用)',
restEnergyUrlHint: 'エネルギーデータ用の個別URL(空欄の場合はステータスURLを使用)',
restEnergyHint: '各値は個別のURLを使用するか、ステータスURLにフォールバックできます。乗数で単位変換が可能です(例:WhからkWhへの変換は0.001)。',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'スイッチバーにスイッチがありません',
+7 -1
View File
@@ -3865,14 +3865,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: 'URL de potência',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: 'Multiplicador de potência',
restEnergyUrl: 'URL de energia',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: 'Multiplicador de energia',
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.',
restPowerUrlHint: 'URL separada para dados de potência (usa a URL de status se vazio)',
restEnergyUrlHint: 'URL separada para dados de energia (usa a URL de status se vazio)',
restEnergyHint: 'Cada valor pode usar sua própria URL ou recorrer à URL de status. Use multiplicadores para conversão de unidades (ex: 0.001 para converter Wh em kWh).',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: 'Nenhum interruptor na barra',
+7 -1
View File
@@ -3865,14 +3865,20 @@ export default {
restStatusUrl: 'Status URL',
restStatusPath: 'State JSON Path',
restStatusOnValue: 'ON Value',
restPowerUrl: '功率URL',
restPowerPath: 'Power JSON Path',
restPowerMultiplier: '功率乘数',
restEnergyUrl: '能耗URL',
restEnergyPath: 'Energy JSON Path',
restEnergyMultiplier: '能耗乘数',
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.',
restPowerUrlHint: '功率数据的独立URL(留空则使用状态URL)',
restEnergyUrlHint: '能耗数据的独立URL(留空则使用状态URL)',
restEnergyHint: '每个值可以使用独立的URL,或回退到状态URL。使用乘数进行单位转换(例如:0.001 将 Wh 转换为 kWh)。',
testConnection: 'Test Connection',
connectionSuccess: 'Connection successful',
noSwitchesInSwitchbar: '开关栏中没有开关',