Set time and time zone
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bbctrl",
|
"name": "bbctrl",
|
||||||
"version": "1.0.10b5",
|
"version": "1.0.10b6",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bbctrl",
|
"name": "bbctrl",
|
||||||
"version": "1.0.10b5",
|
"version": "1.0.10b6",
|
||||||
"license": "GPL-3.0+",
|
"license": "GPL-3.0+",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bbctrl",
|
"name": "bbctrl",
|
||||||
"version": "1.0.10b5",
|
"version": "1.0.10b6",
|
||||||
"homepage": "https://onefinitycnc.com/",
|
"homepage": "https://onefinitycnc.com/",
|
||||||
"repository": "https://github.com/OneFinityCNC/onefinity",
|
"repository": "https://github.com/OneFinityCNC/onefinity",
|
||||||
"license": "GPL-3.0+",
|
"license": "GPL-3.0+",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ const api = require("./api");
|
|||||||
const cookie = require("./cookie")("bbctrl-");
|
const cookie = require("./cookie")("bbctrl-");
|
||||||
const Sock = require("./sock");
|
const Sock = require("./sock");
|
||||||
|
|
||||||
SvelteComponents.initNetworkInfo();
|
|
||||||
SvelteComponents.createComponent("DialogHost",
|
SvelteComponents.createComponent("DialogHost",
|
||||||
document.getElementById("svelte-dialog-host")
|
document.getElementById("svelte-dialog-host")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module.exports = {
|
|||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
current_time: "",
|
||||||
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
|
mach_units: this.$root.state.metric ? "METRIC" : "IMPERIAL",
|
||||||
mdi: '',
|
mdi: '',
|
||||||
last_file: undefined,
|
last_file: undefined,
|
||||||
@@ -253,6 +254,10 @@ module.exports = {
|
|||||||
ready: function () {
|
ready: function () {
|
||||||
this.load();
|
this.load();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.current_time = new Date().toLocaleTimeString();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
SvelteComponents.registerControllerMethods({
|
SvelteComponents.registerControllerMethods({
|
||||||
stop: (...args) => this.stop(...args),
|
stop: (...args) => this.stop(...args),
|
||||||
send: (...args) => this.send(...args),
|
send: (...args) => this.send(...args),
|
||||||
@@ -527,6 +532,10 @@ module.exports = {
|
|||||||
|
|
||||||
showProbeDialog: function (probeType) {
|
showProbeDialog: function (probeType) {
|
||||||
SvelteComponents.showDialog("Probe", { probeType });
|
SvelteComponents.showDialog("Probe", { probeType });
|
||||||
|
},
|
||||||
|
|
||||||
|
showSetTimeDialog: function () {
|
||||||
|
SvelteComponents.showDialog("SetTime");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -265,6 +265,13 @@ script#control-view-template(type="text/x-template")
|
|||||||
|
|
||||||
td
|
td
|
||||||
table.info
|
table.info
|
||||||
|
tr
|
||||||
|
th Current Time
|
||||||
|
td
|
||||||
|
span {{current_time}}
|
||||||
|
button.pure-button(@click="showSetTimeDialog", style="height: 28px; padding: 0px 10px")
|
||||||
|
.fa.fa-cog
|
||||||
|
|
||||||
tr
|
tr
|
||||||
th Remaining
|
th Remaining
|
||||||
td(title="Total run time (days:hours:mins:secs)").
|
td(title="Total run time (days:hours:mins:secs)").
|
||||||
@@ -273,12 +280,7 @@ script#control-view-template(type="text/x-template")
|
|||||||
tr
|
tr
|
||||||
th ETA
|
th ETA
|
||||||
td.eta {{eta}}
|
td.eta {{eta}}
|
||||||
tr
|
|
||||||
th Line
|
|
||||||
td
|
|
||||||
| {{0 <= state.line ? state.line : 0 | number}}
|
|
||||||
span(v-if="toolpath.lines")
|
|
||||||
| of {{toolpath.lines | number}}
|
|
||||||
tr
|
tr
|
||||||
th Progress
|
th Progress
|
||||||
td.progress
|
td.progress
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import traceback
|
import traceback
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import bbctrl
|
import bbctrl
|
||||||
|
import iw_parse
|
||||||
|
from tornado import gen
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
|
||||||
|
def call_get_output(cmd):
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||||
|
s = p.communicate()[0].decode('utf-8')
|
||||||
|
if p.returncode:
|
||||||
|
raise HTTPError(400, 'Command failed')
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class UploadChangeHandler(FileSystemEventHandler):
|
class UploadChangeHandler(FileSystemEventHandler):
|
||||||
def __init__(self, state):
|
def __init__(self, state):
|
||||||
self.state = state
|
self.state = state
|
||||||
@@ -64,6 +76,36 @@ class State(object):
|
|||||||
self), self.ctrl.get_upload(), recursive=True)
|
self), self.ctrl.get_upload(), recursive=True)
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|
||||||
|
self._updateNetworkInfo()
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def _updateNetworkInfo(self):
|
||||||
|
try:
|
||||||
|
ipAddresses = call_get_output(['hostname', '-I']).split()
|
||||||
|
except:
|
||||||
|
ipAddresses = ""
|
||||||
|
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
|
try:
|
||||||
|
wifi = json.loads(call_get_output(['config-wifi', '-j']))
|
||||||
|
except:
|
||||||
|
wifi = {'enabled': False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
|
||||||
|
wifi['networks'] = iw_parse.get_parsed_cells(lines)
|
||||||
|
except:
|
||||||
|
wifi['networks'] = []
|
||||||
|
|
||||||
|
self.set('networkInfo', {
|
||||||
|
'ipAddresses': ipAddresses,
|
||||||
|
'hostname': hostname,
|
||||||
|
'wifi': wifi
|
||||||
|
})
|
||||||
|
|
||||||
|
self.timeout = self.ctrl.ioloop.call_later(5, self._updateNetworkInfo)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
# Unhome all motors
|
# Unhome all motors
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
@@ -170,8 +212,11 @@ class State(object):
|
|||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def has(self, name): return self.resolve(name) in self.vars
|
def has(self, name):
|
||||||
def set_callback(self, name, cb): self.callbacks[self.resolve(name)] = cb
|
return self.resolve(name) in self.vars
|
||||||
|
|
||||||
|
def set_callback(self, name, cb):
|
||||||
|
self.callbacks[self.resolve(name)] = cb
|
||||||
|
|
||||||
def set(self, name, value):
|
def set(self, name, value):
|
||||||
name = self.resolve(name)
|
name = self.resolve(name)
|
||||||
@@ -233,7 +278,8 @@ class State(object):
|
|||||||
self.listeners.append(listener)
|
self.listeners.append(listener)
|
||||||
listener(self.vars)
|
listener(self.vars)
|
||||||
|
|
||||||
def remove_listener(self, listener): self.listeners.remove(listener)
|
def remove_listener(self, listener):
|
||||||
|
self.listeners.remove(listener)
|
||||||
|
|
||||||
def set_machine_vars(self, vars):
|
def set_machine_vars(self, vars):
|
||||||
# Record all machine vars, indexed or otherwise
|
# Record all machine vars, indexed or otherwise
|
||||||
@@ -284,7 +330,8 @@ class State(object):
|
|||||||
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):
|
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):
|
||||||
return motor
|
return motor
|
||||||
|
|
||||||
def is_axis_homed(self, axis): return self.get('%s_homed' % axis, False)
|
def is_axis_homed(self, axis):
|
||||||
|
return self.get('%s_homed' % axis, False)
|
||||||
|
|
||||||
def is_axis_enabled(self, axis):
|
def is_axis_enabled(self, axis):
|
||||||
motor = self.find_motor(axis)
|
motor = self.find_motor(axis)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
|
||||||
import tornado
|
import tornado
|
||||||
import sockjs.tornado
|
import sockjs.tornado
|
||||||
import datetime
|
import datetime
|
||||||
@@ -10,7 +9,6 @@ from tornado.web import HTTPError
|
|||||||
from tornado import gen
|
from tornado import gen
|
||||||
|
|
||||||
import bbctrl
|
import bbctrl
|
||||||
import iw_parse
|
|
||||||
|
|
||||||
|
|
||||||
def call_get_output(cmd):
|
def call_get_output(cmd):
|
||||||
@@ -101,31 +99,6 @@ class HostnameHandler(bbctrl.APIHandler):
|
|||||||
|
|
||||||
|
|
||||||
class NetworkHandler(bbctrl.APIHandler):
|
class NetworkHandler(bbctrl.APIHandler):
|
||||||
def get(self):
|
|
||||||
try:
|
|
||||||
ipAddresses = call_get_output(['hostname', '-I']).split()
|
|
||||||
except:
|
|
||||||
ipAddresses = ""
|
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
|
|
||||||
try:
|
|
||||||
wifi = json.loads(call_get_output(['config-wifi', '-j']))
|
|
||||||
except:
|
|
||||||
wifi = {'enabled': False}
|
|
||||||
|
|
||||||
try:
|
|
||||||
lines = iw_parse.call_iwlist().decode("utf-8").split("\n")
|
|
||||||
wifi['networks'] = iw_parse.get_parsed_cells(lines)
|
|
||||||
except:
|
|
||||||
wifi['networks'] = []
|
|
||||||
|
|
||||||
self.write_json({
|
|
||||||
'ipAddresses': ipAddresses,
|
|
||||||
'hostname': hostname,
|
|
||||||
'wifi': wifi
|
|
||||||
})
|
|
||||||
|
|
||||||
def put(self):
|
def put(self):
|
||||||
if self.get_ctrl().args.demo:
|
if self.get_ctrl().args.demo:
|
||||||
raise HTTPError(400, 'Cannot configure WiFi in demo mode')
|
raise HTTPError(400, 'Cannot configure WiFi in demo mode')
|
||||||
@@ -383,13 +356,34 @@ class ScreenRotationHandler(bbctrl.APIHandler):
|
|||||||
text = config.read()
|
text = config.read()
|
||||||
text = transformationMatrixPattern.sub(r'\1\2\3\5', text)
|
text = transformationMatrixPattern.sub(r'\1\2\3\5', text)
|
||||||
if rotated:
|
if rotated:
|
||||||
text = matchIsTouchscreenPattern.sub(r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
|
text = matchIsTouchscreenPattern.sub(
|
||||||
|
r'\1\2\3\2Option "TransformationMatrix" "-1 0 1 0 -1 1 0 0 1"\1\4', text)
|
||||||
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf", 'wt') as config:
|
with open("/usr/share/X11/xorg.conf.d/40-libinput.conf", 'wt') as config:
|
||||||
config.write(text)
|
config.write(text)
|
||||||
|
|
||||||
subprocess.run('reboot')
|
subprocess.run('reboot')
|
||||||
|
|
||||||
|
|
||||||
|
class TimeHandler(bbctrl.APIHandler):
|
||||||
|
def get(self):
|
||||||
|
timeinfo = call_get_output(['timedatectl'])
|
||||||
|
timezones = call_get_output(
|
||||||
|
['timedatectl', 'list-timezones', '--no-pager'])
|
||||||
|
self.get_log('TimeHandler').info(
|
||||||
|
'Time stuff: {}, {}'.format(timeinfo, timezones))
|
||||||
|
|
||||||
|
self.write_json({
|
||||||
|
'timeinfo': timeinfo,
|
||||||
|
'timezones': timezones
|
||||||
|
})
|
||||||
|
|
||||||
|
def put_ok(self):
|
||||||
|
datetime = self.json['datetime']
|
||||||
|
timezone = self.json['timezone']
|
||||||
|
subprocess.Popen(['timedatectl', 'set-time', datetime])
|
||||||
|
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
|
||||||
|
|
||||||
|
|
||||||
# Base class for Web Socket connections
|
# Base class for Web Socket connections
|
||||||
class ClientConnection(object):
|
class ClientConnection(object):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
@@ -526,6 +520,7 @@ class Web(tornado.web.Application):
|
|||||||
(r'/api/jog', JogHandler),
|
(r'/api/jog', JogHandler),
|
||||||
(r'/api/video', bbctrl.VideoHandler),
|
(r'/api/video', bbctrl.VideoHandler),
|
||||||
(r'/api/screen-rotation', ScreenRotationHandler),
|
(r'/api/screen-rotation', ScreenRotationHandler),
|
||||||
|
(r'/api/time', TimeHandler),
|
||||||
(r'/(.*)', StaticFileHandler,
|
(r'/(.*)', StaticFileHandler,
|
||||||
{'path': bbctrl.get_resource('http/'),
|
{'path': bbctrl.get_resource('http/'),
|
||||||
'default_filename': 'index.html'}),
|
'default_filename': 'index.html'}),
|
||||||
@@ -545,7 +540,8 @@ class Web(tornado.web.Application):
|
|||||||
|
|
||||||
print('Listening on http://%s:%d/' % (args.addr, args.port))
|
print('Listening on http://%s:%d/' % (args.addr, args.port))
|
||||||
|
|
||||||
def opened(self, ctrl): ctrl.clear_timeout()
|
def opened(self, ctrl):
|
||||||
|
ctrl.clear_timeout()
|
||||||
|
|
||||||
def closed(self, ctrl):
|
def closed(self, ctrl):
|
||||||
# Time out clients in demo mode
|
# Time out clients in demo mode
|
||||||
|
|||||||
859
src/svelte-components/package-lock.json
generated
859
src/svelte-components/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,12 @@
|
|||||||
"node-sass": "^7.0.1",
|
"node-sass": "^7.0.1",
|
||||||
"polyfill-object.fromentries": "^1.0.1",
|
"polyfill-object.fromentries": "^1.0.1",
|
||||||
"smui-theme": "^6.0.0-beta.16",
|
"smui-theme": "^6.0.0-beta.16",
|
||||||
|
"string.prototype.matchall": "^4.0.7",
|
||||||
"svelte": "^3.48.0",
|
"svelte": "^3.48.0",
|
||||||
"svelte-check": "^2.8.0",
|
"svelte-check": "^2.8.0",
|
||||||
"svelte-material-ui": "^6.0.0-beta.16",
|
"svelte-material-ui": "^6.0.0-beta.16",
|
||||||
"svelte-preprocess": "^4.10.7",
|
"svelte-preprocess": "^4.10.7",
|
||||||
|
"svelte-tiny-virtual-list": "^2.0.5",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"vite": "^2.9.13"
|
"vite": "^2.9.13"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
|
import ProbeDialog from "$dialogs/ProbeDialog.svelte";
|
||||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||||
import UploadDialog from "$dialogs/UploadDialog.svelte";
|
import UploadDialog from "$dialogs/UploadDialog.svelte";
|
||||||
|
import SetTimeDialog from "./SetTimeDialog.svelte";
|
||||||
|
|
||||||
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
|
const HomeMachineDialogProps = writable<HomeMachineDialogPropsType>();
|
||||||
type HomeMachineDialogPropsType = {
|
type HomeMachineDialogPropsType = {
|
||||||
@@ -29,6 +30,11 @@
|
|||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SetTimeDialogProps = writable<SetTimeDialogPropsType>();
|
||||||
|
type SetTimeDialogPropsType = {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function showDialog(
|
export function showDialog(
|
||||||
dialog: "HomeMachine",
|
dialog: "HomeMachine",
|
||||||
props: Omit<HomeMachineDialogPropsType, "open">
|
props: Omit<HomeMachineDialogPropsType, "open">
|
||||||
@@ -49,6 +55,11 @@
|
|||||||
props: Omit<UploadDialogPropsType, "open">
|
props: Omit<UploadDialogPropsType, "open">
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function showDialog(
|
||||||
|
dialog: "SetTime",
|
||||||
|
props: Omit<SetTimeDialogPropsType, "open">
|
||||||
|
);
|
||||||
|
|
||||||
export function showDialog(dialog: string, props: any) {
|
export function showDialog(dialog: string, props: any) {
|
||||||
switch (dialog) {
|
switch (dialog) {
|
||||||
case "HomeMachine":
|
case "HomeMachine":
|
||||||
@@ -67,6 +78,10 @@
|
|||||||
UploadDialogProps.set({ ...props, open: true });
|
UploadDialogProps.set({ ...props, open: true });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "SetTime":
|
||||||
|
SetTimeDialogProps.set({ ...props, open: true });
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown dialog '${dialog}`);
|
throw new Error(`Unknown dialog '${dialog}`);
|
||||||
}
|
}
|
||||||
@@ -77,3 +92,4 @@
|
|||||||
<ProbeDialog {...$ProbeDialogProps} />
|
<ProbeDialog {...$ProbeDialogProps} />
|
||||||
<ScreenRotationDialog {...$ScreenRotationDialogProps} />
|
<ScreenRotationDialog {...$ScreenRotationDialogProps} />
|
||||||
<UploadDialog {...$UploadDialogProps} />
|
<UploadDialog {...$UploadDialogProps} />
|
||||||
|
<SetTimeDialog {...$SetTimeDialogProps} />
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
<script type="ts" context="module">
|
<script type="ts">
|
||||||
import { get, writable, type Writable } from "svelte/store";
|
import DimensionInput from "$components/DimensionInput.svelte";
|
||||||
|
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||||
|
import Button, { Label } from "@smui/button";
|
||||||
|
import { waitForChange } from "$lib/StoreHelpers";
|
||||||
|
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
||||||
|
import { Config } from "$lib/ConfigStore";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import {
|
||||||
|
probingActive,
|
||||||
|
probeContacted,
|
||||||
|
probingComplete,
|
||||||
|
probingFailed,
|
||||||
|
probingStarted,
|
||||||
|
} from "$lib/ControllerState";
|
||||||
|
|
||||||
type Step =
|
type Step =
|
||||||
| "None"
|
| "None"
|
||||||
@@ -19,49 +32,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelled = writable(false);
|
const cancelled = writable(false);
|
||||||
const probingActive = writable(false);
|
|
||||||
const probeContacted = writable(false);
|
|
||||||
const probingStarted = writable(false);
|
|
||||||
const probingFailed = writable(false);
|
|
||||||
const probingComplete = writable(false);
|
|
||||||
const userAcknowledged = writable(false);
|
const userAcknowledged = writable(false);
|
||||||
|
|
||||||
export function handleControllerStateUpdate(state: Record<string, any>) {
|
|
||||||
if (!get(probingActive)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case state.pw === 0:
|
|
||||||
probeContacted.set(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case state.log?.msg === "Switch not found":
|
|
||||||
probingFailed.set(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case state.cycle !== "idle":
|
|
||||||
probingStarted.set(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case state.cycle === "idle":
|
|
||||||
if (get(probingStarted)) {
|
|
||||||
probingStarted.set(false);
|
|
||||||
probingComplete.set(true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="ts">
|
|
||||||
import DimensionInput from "$components/DimensionInput.svelte";
|
|
||||||
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
|
||||||
import Button, { Label } from "@smui/button";
|
|
||||||
import { waitForChange } from "$lib/StoreHelpers";
|
|
||||||
import { ControllerMethods } from "$lib/RegisterControllerMethods";
|
|
||||||
import { Config } from "$lib/ConfigStore";
|
|
||||||
|
|
||||||
const cutterDiameterOptions = [
|
const cutterDiameterOptions = [
|
||||||
{ value: 0.5, label: '1/2 "', metric: false },
|
{ value: 0.5, label: '1/2 "', metric: false },
|
||||||
{ value: 0.25, label: '1/4 "', metric: false },
|
{ value: 0.25, label: '1/4 "', metric: false },
|
||||||
|
|||||||
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
240
src/svelte-components/src/dialogs/SetTimeDialog.svelte
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Dialog, { Title, Content, Actions } from "@smui/dialog";
|
||||||
|
import Button, { Label } from "@smui/button";
|
||||||
|
import TextField from "@smui/textfield";
|
||||||
|
import CircularProgress from "@smui/circular-progress";
|
||||||
|
import VirtualList from "svelte-tiny-virtual-list";
|
||||||
|
import * as api from "$lib/api";
|
||||||
|
|
||||||
|
const itemHeight = 35;
|
||||||
|
|
||||||
|
type Timezone = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
let value = "";
|
||||||
|
let wasOpen = false;
|
||||||
|
let loading = true;
|
||||||
|
let timezones: Timezone[] = [];
|
||||||
|
let currentTimezoneIndex: number;
|
||||||
|
let selectedTimezoneIndex: number;
|
||||||
|
let networkTimeSynchronized: boolean;
|
||||||
|
|
||||||
|
$: if (open != wasOpen) {
|
||||||
|
if (!wasOpen) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
wasOpen = open;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
const result = await api.GET("time");
|
||||||
|
|
||||||
|
parseTimezones(result.timezones);
|
||||||
|
parseTimeinfo(result.timeinfo);
|
||||||
|
value = getDateTimeValueString();
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimeinfo(str: string) {
|
||||||
|
const matches = Array.from(str.matchAll(/\s*([^:]+):\s+(.+)/gm));
|
||||||
|
|
||||||
|
let currentTimezoneValue;
|
||||||
|
for (const match of matches) {
|
||||||
|
let [, label, value] = match;
|
||||||
|
|
||||||
|
switch (label) {
|
||||||
|
case "Time zone":
|
||||||
|
currentTimezoneValue = value.split(" ")[0];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "NTP synchronized":
|
||||||
|
networkTimeSynchronized = value === "yes";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTimezoneIndex = timezones.findIndex(
|
||||||
|
(tz) => tz.value === currentTimezoneValue
|
||||||
|
);
|
||||||
|
selectedTimezoneIndex = currentTimezoneIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTimezones(str: string) {
|
||||||
|
const matches = Array.from(str.matchAll(/\s*(\S+)\s*/gm));
|
||||||
|
|
||||||
|
timezones = [];
|
||||||
|
for (let [, value] of matches) {
|
||||||
|
timezones.push({
|
||||||
|
label: value.replace(/_/g, " "),
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort alphabetically, but with the current timezone at the top of the list
|
||||||
|
timezones.sort((a, b) => {
|
||||||
|
switch (true) {
|
||||||
|
case a.value === "UTC":
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
case b.value === "UTC":
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return a.value.localeCompare(b.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateTimeValueString() {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const year = date.getFullYear().toString().padStart(2, "0");
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
const hour = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hour}:${minute}:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirm() {
|
||||||
|
const date = new Date(value);
|
||||||
|
const year = date.getFullYear().toString().padStart(2, "0");
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
const hour = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minute = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
await api.PUT("time", {
|
||||||
|
datetime: `${year}-${month}-${day} ${hour}:${minute}:00`,
|
||||||
|
timezone: timezones[selectedTimezoneIndex].value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
bind:open
|
||||||
|
scrimClickAction=""
|
||||||
|
aria-labelledby="set-time-dialog-title"
|
||||||
|
aria-describedby="set-time-dialog-content"
|
||||||
|
>
|
||||||
|
<Title id="set-time-dialog-title">Adjust Clock & Timezone</Title>
|
||||||
|
|
||||||
|
<Content id="set-time-dialog-content">
|
||||||
|
{#if loading}
|
||||||
|
<div style="display: flex; justify-content: center">
|
||||||
|
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#if networkTimeSynchronized}
|
||||||
|
<p>
|
||||||
|
Because this controller is connected to the internet, the time is
|
||||||
|
managed automatically, and cannot be manually set.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
Because this controller is not connected to the internet, you can
|
||||||
|
manually set the time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Note: any time the controller is turned off, the time will need to be
|
||||||
|
reset. If you connect the controller to the internet, the time will be
|
||||||
|
managed automatically.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Label>Date & Time</Label>
|
||||||
|
<TextField
|
||||||
|
bind:value
|
||||||
|
label="Time"
|
||||||
|
type="datetime-local"
|
||||||
|
variant="filled"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To display your local time correctly, the controller must know what
|
||||||
|
timezone it is in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="timezones-container" style="margin-top: 20px;">
|
||||||
|
<Label>Select your timezone</Label>
|
||||||
|
<VirtualList
|
||||||
|
width="100%"
|
||||||
|
height={itemHeight * 6}
|
||||||
|
itemCount={timezones.length}
|
||||||
|
itemSize={itemHeight}
|
||||||
|
scrollToIndex={currentTimezoneIndex}
|
||||||
|
scrollToAlignment="center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="item"
|
||||||
|
let:index
|
||||||
|
let:style
|
||||||
|
{style}
|
||||||
|
class="timezone"
|
||||||
|
class:selected={index === selectedTimezoneIndex}
|
||||||
|
on:click={() => (selectedTimezoneIndex = index)}
|
||||||
|
>
|
||||||
|
{timezones[index].label}
|
||||||
|
</div>
|
||||||
|
</VirtualList>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<Actions>
|
||||||
|
<Button>
|
||||||
|
<Label>Cancel</Label>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
defaultAction
|
||||||
|
disabled={selectedTimezoneIndex === -1}
|
||||||
|
on:click={onConfirm}
|
||||||
|
>
|
||||||
|
<Label>Confirm</Label>
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "sass:color";
|
||||||
|
|
||||||
|
$primary: #0078e7;
|
||||||
|
$very-dark: #555;
|
||||||
|
$text: #777;
|
||||||
|
$grey: #bbb;
|
||||||
|
$light: #ddd;
|
||||||
|
|
||||||
|
.timezones-container {
|
||||||
|
:global {
|
||||||
|
.virtual-list-wrapper {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone {
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: $primary;
|
||||||
|
background-color: color.adjust($primary, $lightness: 50%);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
36
src/svelte-components/src/lib/ControllerState.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
import { processNetworkInfo } from "./NetworkInfo";
|
||||||
|
|
||||||
|
export const networkInfo = writable({});
|
||||||
|
|
||||||
|
export const probingActive = writable(false);
|
||||||
|
export const probeContacted = writable(false);
|
||||||
|
export const probingStarted = writable(false);
|
||||||
|
export const probingFailed = writable(false);
|
||||||
|
export const probingComplete = writable(false);
|
||||||
|
|
||||||
|
export function handleControllerStateUpdate(state: Record<string, any>) {
|
||||||
|
if (state.networkInfo) {
|
||||||
|
processNetworkInfo(state.networkInfo);
|
||||||
|
delete state.networkInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(probingActive)) {
|
||||||
|
if (state.pw === 0) {
|
||||||
|
probeContacted.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.log?.msg === "Switch not found") {
|
||||||
|
probingFailed.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.cycle !== "idle") {
|
||||||
|
probingStarted.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.cycle === "idle" && get(probingStarted)) {
|
||||||
|
probingStarted.set(false);
|
||||||
|
probingComplete.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { readable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import * as api from "$lib/api";
|
|
||||||
|
|
||||||
export type WifiNetwork = {
|
export type WifiNetwork = {
|
||||||
Quality: string;
|
Quality: string;
|
||||||
@@ -34,21 +33,16 @@ const empty: NetworkInfo = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
export const networkInfo = writable<NetworkInfo>(empty);
|
||||||
getNetworkInfo();
|
|
||||||
const networkInfoIntervalId = setInterval(getNetworkInfo, 5000);
|
|
||||||
|
|
||||||
async function getNetworkInfo() {
|
export function processNetworkInfo(rawNetworkInfo: NetworkInfo) {
|
||||||
|
const now = Date.now();
|
||||||
const networksByName: Record<string, WifiNetwork> = {}
|
const networksByName: Record<string, WifiNetwork> = {}
|
||||||
|
|
||||||
try {
|
for (let network of rawNetworkInfo.wifi.networks) {
|
||||||
const networkInfo: NetworkInfo = await api.GET("network");
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
for (let network of networkInfo.wifi.networks) {
|
|
||||||
if (network.Name) {
|
if (network.Name) {
|
||||||
network.lastSeen = now;
|
network.lastSeen = now;
|
||||||
network.active = networkInfo.wifi.ssid === network.Name;
|
network.active = rawNetworkInfo.wifi.ssid === network.Name;
|
||||||
networksByName[network.Name] = network;
|
networksByName[network.Name] = network;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,11 +53,11 @@ export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
networkInfo.set({
|
||||||
ipAddresses: networkInfo.ipAddresses,
|
ipAddresses: rawNetworkInfo.ipAddresses,
|
||||||
hostname: networkInfo.hostname,
|
hostname: rawNetworkInfo.hostname,
|
||||||
wifi: {
|
wifi: {
|
||||||
ssid: networkInfo.wifi.ssid,
|
ssid: rawNetworkInfo.wifi.ssid,
|
||||||
networks: Object.values(networksByName).sort((a, b) => {
|
networks: Object.values(networksByName).sort((a, b) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case a.active:
|
case a.active:
|
||||||
@@ -78,16 +72,4 @@ export const networkInfo = readable<NetworkInfo>(empty, (set) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.debug("Failed to fetch network info", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(networkInfoIntervalId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function init() {
|
|
||||||
return networkInfo.subscribe(() => ({}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'polyfill-object.fromentries';
|
import 'polyfill-object.fromentries';
|
||||||
|
import matchAll from "string.prototype.matchall";
|
||||||
|
|
||||||
|
matchAll.shim();
|
||||||
|
|
||||||
import AdminNetworkView from '$components/AdminNetworkView.svelte';
|
import AdminNetworkView from '$components/AdminNetworkView.svelte';
|
||||||
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
||||||
import Devmode from "$components/Devmode.svelte";
|
import Devmode from "$components/Devmode.svelte";
|
||||||
import { handleConfigUpdate } from '$lib/ConfigStore';
|
import { handleConfigUpdate } from '$lib/ConfigStore';
|
||||||
import { init as initNetworkInfo } from '$lib/NetworkInfo';
|
import { handleControllerStateUpdate } from "$lib/ControllerState";
|
||||||
import { handleControllerStateUpdate } from "$dialogs/ProbeDialog.svelte";
|
|
||||||
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
|
import { registerControllerMethods } from "$lib/RegisterControllerMethods";
|
||||||
|
|
||||||
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
|
export function createComponent(component: string, target: HTMLElement, props: Record<string, any>) {
|
||||||
@@ -17,7 +19,7 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
|||||||
return new DialogHost({ target, props });
|
return new DialogHost({ target, props });
|
||||||
|
|
||||||
case "Devmode":
|
case "Devmode":
|
||||||
return new Devmode({target, props});
|
return new Devmode({ target, props });
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown component");
|
throw new Error("Unknown component");
|
||||||
@@ -25,7 +27,6 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
initNetworkInfo,
|
|
||||||
showDialog,
|
showDialog,
|
||||||
handleControllerStateUpdate,
|
handleControllerStateUpdate,
|
||||||
handleConfigUpdate,
|
handleConfigUpdate,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
minify: false,
|
||||||
target: "chrome60",
|
target: "chrome60",
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/main.ts'),
|
entry: resolve(__dirname, 'src/main.ts'),
|
||||||
|
|||||||
Reference in New Issue
Block a user