Modernize invertergui: MQTT write support, HA integration, UI updates
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
Some checks failed
build / inverter_gui_pipeline (push) Has been cancelled
This commit is contained in:
@@ -98,6 +98,117 @@
|
||||
<div class="alert alert-danger" role="alert" v-if="error.has_error">
|
||||
{{ error.error_message }}
|
||||
</div>
|
||||
<div
|
||||
class="alert"
|
||||
v-if="control.message !== ''"
|
||||
v-bind:class="[control.has_error ? 'alert-danger' : 'alert-success']"
|
||||
>
|
||||
{{ control.message }}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Remote Panel Control</h4>
|
||||
<p class="text-muted mb-2">
|
||||
Mode and current limit are applied together, equivalent to
|
||||
<code>set_remote_panel_state</code>.
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>Current Mode:</strong>
|
||||
{{ remoteModeLabel(state.remote_panel) }}
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>Current Limit:</strong>
|
||||
{{ state.remote_panel.current_limit === null || state.remote_panel.current_limit === undefined ? 'Unknown' : state.remote_panel.current_limit + ' A' }}
|
||||
</p>
|
||||
<p class="mb-3">
|
||||
<strong>Standby:</strong>
|
||||
{{ remoteStandbyLabel(state.remote_panel) }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form v-on:submit.prevent="applyRemotePanelState">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="modeSelect">Remote Panel Mode</label>
|
||||
<select
|
||||
class="form-control"
|
||||
id="modeSelect"
|
||||
v-model="remote_form.mode"
|
||||
v-bind:disabled="!state.remote_panel.writable || control.busy"
|
||||
>
|
||||
<option value="on">on</option>
|
||||
<option value="off">off</option>
|
||||
<option value="charger_only">charger_only</option>
|
||||
<option value="inverter_only">inverter_only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="currentLimitInput">AC Input Current Limit (A)</label>
|
||||
<input
|
||||
id="currentLimitInput"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
class="form-control"
|
||||
v-model="remote_form.current_limit"
|
||||
placeholder="leave blank to keep current"
|
||||
v-bind:disabled="!state.remote_panel.writable || control.busy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
v-bind:disabled="!state.remote_panel.writable || control.busy"
|
||||
>
|
||||
Apply Mode + Current Limit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<form v-on:submit.prevent="applyStandby">
|
||||
<div class="form-group">
|
||||
<div class="form-check mt-4">
|
||||
<input
|
||||
id="standbySwitch"
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
v-model="remote_form.standby"
|
||||
v-bind:disabled="!state.remote_panel.writable || control.busy"
|
||||
/>
|
||||
<label class="form-check-label" for="standbySwitch">
|
||||
Prevent sleep while off
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
v-bind:disabled="!state.remote_panel.writable || control.busy"
|
||||
>
|
||||
Apply Standby
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-muted" v-if="state.remote_panel.last_updated">
|
||||
Last update {{ state.remote_panel.last_updated }}
|
||||
<span v-if="state.remote_panel.last_command">
|
||||
({{ state.remote_panel.last_command }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-danger" v-if="state.remote_panel.last_error">
|
||||
{{ state.remote_panel.last_error }}
|
||||
</div>
|
||||
<div class="mt-2 text-warning" v-if="!state.remote_panel.writable">
|
||||
Remote control is unavailable for this data source.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<hr />
|
||||
|
||||
@@ -3,6 +3,46 @@ const timeoutMax = 30000;
|
||||
const timeoutMin = 1000;
|
||||
var timeout = timeoutMin;
|
||||
|
||||
function defaultRemotePanelState() {
|
||||
return {
|
||||
writable: false,
|
||||
mode: "unknown",
|
||||
current_limit: null,
|
||||
standby: null,
|
||||
last_command: "",
|
||||
last_error: "",
|
||||
last_updated: ""
|
||||
};
|
||||
}
|
||||
|
||||
function defaultState() {
|
||||
return {
|
||||
output_current: null,
|
||||
output_voltage: 0,
|
||||
output_frequency: 0,
|
||||
output_power: 0,
|
||||
input_current: 0,
|
||||
input_voltage: 0,
|
||||
input_frequency: 0,
|
||||
input_power: 0,
|
||||
battery_current: 0,
|
||||
battery_voltage: 0,
|
||||
battery_charge: 0,
|
||||
battery_power: 0,
|
||||
led_map: {
|
||||
led_mains: "dot-off",
|
||||
led_absorb: "dot-off",
|
||||
led_bulk: "dot-off",
|
||||
led_float: "dot-off",
|
||||
led_inverter: "dot-off",
|
||||
led_overload: "dot-off",
|
||||
led_bat_low: "dot-off",
|
||||
led_over_temp: "dot-off"
|
||||
},
|
||||
remote_panel: defaultRemotePanelState()
|
||||
};
|
||||
}
|
||||
|
||||
function loadContent() {
|
||||
app = new Vue({
|
||||
el: "#app",
|
||||
@@ -11,33 +51,172 @@ function loadContent() {
|
||||
has_error: false,
|
||||
error_message: ""
|
||||
},
|
||||
state: {
|
||||
output_current: null,
|
||||
output_voltage: 0,
|
||||
output_frequency: 0,
|
||||
output_power: 0,
|
||||
input_current: 0,
|
||||
input_voltage: 0,
|
||||
input_frequency: 0,
|
||||
input_power: 0,
|
||||
battery_current: 0,
|
||||
battery_voltage: 0,
|
||||
battery_charge: 0,
|
||||
battery_power: 0,
|
||||
led_map: [
|
||||
{ led_mains: "dot-off" },
|
||||
{ led_absorb: "dot-off" },
|
||||
{ led_bulk: "dot-off" },
|
||||
{ led_float: "dot-off" },
|
||||
{ led_inverter: "dot-off" },
|
||||
{ led_overload: "dot-off" },
|
||||
{ led_bat_low: "dot-off" },
|
||||
{ led_over_temp: "dot-off" }
|
||||
]
|
||||
control: {
|
||||
busy: false,
|
||||
has_error: false,
|
||||
message: ""
|
||||
},
|
||||
remote_form: {
|
||||
mode: "on",
|
||||
current_limit: "",
|
||||
standby: false
|
||||
},
|
||||
state: defaultState()
|
||||
},
|
||||
methods: {
|
||||
syncRemoteFormFromState: function(remoteState) {
|
||||
if (!remoteState) {
|
||||
return;
|
||||
}
|
||||
if (remoteState.mode && remoteState.mode !== "unknown") {
|
||||
this.remote_form.mode = remoteState.mode;
|
||||
}
|
||||
if (remoteState.current_limit === null || remoteState.current_limit === undefined) {
|
||||
this.remote_form.current_limit = "";
|
||||
} else {
|
||||
this.remote_form.current_limit = String(remoteState.current_limit);
|
||||
}
|
||||
if (remoteState.standby === null || remoteState.standby === undefined) {
|
||||
this.remote_form.standby = false;
|
||||
} else {
|
||||
this.remote_form.standby = !!remoteState.standby;
|
||||
}
|
||||
},
|
||||
remoteModeLabel: function(remoteState) {
|
||||
var mode = (remoteState && remoteState.mode) || "unknown";
|
||||
if (mode === "charger_only") {
|
||||
return "Charger Only";
|
||||
}
|
||||
if (mode === "inverter_only") {
|
||||
return "Inverter Only";
|
||||
}
|
||||
if (mode === "on") {
|
||||
return "On";
|
||||
}
|
||||
if (mode === "off") {
|
||||
return "Off";
|
||||
}
|
||||
return "Unknown";
|
||||
},
|
||||
remoteStandbyLabel: function(remoteState) {
|
||||
if (!remoteState || remoteState.standby === null || remoteState.standby === undefined) {
|
||||
return "Unknown";
|
||||
}
|
||||
return remoteState.standby ? "Enabled" : "Disabled";
|
||||
},
|
||||
refreshRemoteState: function() {
|
||||
var self = this;
|
||||
fetch(getAPIURI("api/remote-panel/state"))
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) {
|
||||
throw new Error("Could not load remote panel state.");
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(payload) {
|
||||
self.state.remote_panel = payload;
|
||||
self.syncRemoteFormFromState(payload);
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.control.has_error = true;
|
||||
self.control.message = err.message;
|
||||
});
|
||||
},
|
||||
applyRemotePanelState: function() {
|
||||
var self = this;
|
||||
if (!self.state.remote_panel.writable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var body = {
|
||||
mode: self.remote_form.mode
|
||||
};
|
||||
if (self.remote_form.current_limit !== "") {
|
||||
var parsed = parseFloat(self.remote_form.current_limit);
|
||||
if (isNaN(parsed)) {
|
||||
self.control.has_error = true;
|
||||
self.control.message = "Current limit must be numeric.";
|
||||
return;
|
||||
}
|
||||
body.current_limit = parsed;
|
||||
}
|
||||
|
||||
self.control.busy = true;
|
||||
self.control.has_error = false;
|
||||
self.control.message = "";
|
||||
fetch(getAPIURI("api/remote-panel/state"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) {
|
||||
return resp.text().then(function(text) {
|
||||
throw new Error(text || "Failed to set remote panel mode/current limit.");
|
||||
});
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(payload) {
|
||||
self.state.remote_panel = payload;
|
||||
self.syncRemoteFormFromState(payload);
|
||||
self.control.has_error = false;
|
||||
self.control.message = "Remote panel state updated.";
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.control.has_error = true;
|
||||
self.control.message = err.message;
|
||||
})
|
||||
.finally(function() {
|
||||
self.control.busy = false;
|
||||
});
|
||||
},
|
||||
applyStandby: function() {
|
||||
var self = this;
|
||||
if (!self.state.remote_panel.writable) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.control.busy = true;
|
||||
self.control.has_error = false;
|
||||
self.control.message = "";
|
||||
fetch(getAPIURI("api/remote-panel/standby"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
standby: !!self.remote_form.standby
|
||||
})
|
||||
})
|
||||
.then(function(resp) {
|
||||
if (!resp.ok) {
|
||||
return resp.text().then(function(text) {
|
||||
throw new Error(text || "Failed to set standby mode.");
|
||||
});
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(payload) {
|
||||
self.state.remote_panel = payload;
|
||||
self.syncRemoteFormFromState(payload);
|
||||
self.control.has_error = false;
|
||||
self.control.message = "Standby mode updated.";
|
||||
})
|
||||
.catch(function(err) {
|
||||
self.control.has_error = true;
|
||||
self.control.message = err.message;
|
||||
})
|
||||
.finally(function() {
|
||||
self.control.busy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.refreshRemoteState();
|
||||
connect();
|
||||
}
|
||||
|
||||
@@ -61,7 +240,7 @@ function connect() {
|
||||
}
|
||||
};
|
||||
|
||||
conn.onopen = function(evt) {
|
||||
conn.onopen = function() {
|
||||
timeout = timeoutMin;
|
||||
app.error.has_error = false;
|
||||
};
|
||||
@@ -69,6 +248,9 @@ function connect() {
|
||||
conn.onmessage = function(evt) {
|
||||
var update = JSON.parse(evt.data);
|
||||
app.state = update;
|
||||
if (!app.control.busy) {
|
||||
app.syncRemoteFormFromState(update.remote_panel);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
app.error.has_error = true;
|
||||
@@ -88,3 +270,11 @@ function getURI() {
|
||||
new_uri += loc.pathname + "ws";
|
||||
return new_uri;
|
||||
}
|
||||
|
||||
function getAPIURI(path) {
|
||||
var base = window.location.pathname;
|
||||
if (base.slice(-1) !== "/") {
|
||||
base += "/";
|
||||
}
|
||||
return base + path.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user