frontend is fucked
This commit is contained in:
parent
dbf511f27c
commit
ba0b5cdf5c
@ -1,119 +1,85 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<div class="p-grid">
|
||||
<!-- Devices Panel -->
|
||||
<div class="p-col-12 p-md-3">
|
||||
<Panel header="Devices">
|
||||
<div class="p-grid p-m-2">
|
||||
<div class="p-col-12 p-md-3">
|
||||
<div class="p-panel p-shadow-3">
|
||||
<div class="p-panel-header">Devices</div>
|
||||
<div class="p-panel-content">
|
||||
<ul>
|
||||
<li v-for="(device, id) in devices" :key="id">
|
||||
<Button
|
||||
:label="id + ' — ' + device.status"
|
||||
:class="{'p-button-outlined': selected !== id}"
|
||||
<button
|
||||
class="p-button p-button-text"
|
||||
@click="selectDevice(id)"
|
||||
/>
|
||||
>
|
||||
{{ id }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Properties & Commands Panel -->
|
||||
<div class="p-col-12 p-md-9" v-if="selected">
|
||||
<Panel :header="'Properties — ' + selected" class="mb-3">
|
||||
<Tree :value="treeData" />
|
||||
</Panel>
|
||||
|
||||
<Panel :header="'Commands — ' + selected">
|
||||
<Button
|
||||
v-for="(cmd, name) in devices[selected].commands"
|
||||
:key="name"
|
||||
class="p-m-2"
|
||||
:label="name"
|
||||
@click="sendCommand(name)"
|
||||
/>
|
||||
</Panel>
|
||||
<div class="p-col-12 p-md-9" v-if="selected">
|
||||
<div class="p-panel p-shadow-3">
|
||||
<div class="p-panel-header">Properties</div>
|
||||
<div class="p-panel-content">
|
||||
<PropertyTree :properties="devices[selected].property" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MQTTService from './services/mqtt'
|
||||
import { reactive } from 'vue'
|
||||
import MQTTService from './services/mqtt.js'
|
||||
import PropertyTree from './PropertyTree.vue'
|
||||
|
||||
export default {
|
||||
components: { PropertyTree },
|
||||
data() {
|
||||
return {
|
||||
mqtt: null,
|
||||
devices: {}, // { deviceId: {status, properties, commands} }
|
||||
devices: reactive({}),
|
||||
selected: null,
|
||||
treeData: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.mqtt = new MQTTService()
|
||||
|
||||
// Device status
|
||||
this.mqtt.subscribe('asset/+/status', (payload, topic) => {
|
||||
const id = topic.split('/')[1]
|
||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
||||
this.devices[id].status = payload
|
||||
})
|
||||
|
||||
// Device properties
|
||||
this.mqtt.subscribe('asset/+/property/#', (payload, topic) => {
|
||||
const parts = topic.split('/')
|
||||
const id = parts[1]
|
||||
const keyPath = parts.slice(3)
|
||||
|
||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
||||
|
||||
let target = this.devices[id].properties
|
||||
keyPath.forEach((k, idx) => {
|
||||
if (idx === keyPath.length - 1) {
|
||||
try { target[k] = JSON.parse(payload) } catch(e) { target[k] = payload }
|
||||
} else {
|
||||
if (!target[k] || typeof target[k] !== 'object') this.$set(target, k, {})
|
||||
target = target[k]
|
||||
}
|
||||
})
|
||||
|
||||
if (this.selected) this.buildTree()
|
||||
})
|
||||
|
||||
// Device commands
|
||||
this.mqtt.subscribe('asset/+/command/#', (payload, topic) => {
|
||||
const id = topic.split('/')[1]
|
||||
const cmdName = topic.split('/')[3]
|
||||
|
||||
if (!this.devices[id]) this.$set(this.devices, id, {status: 'OFFLINE', properties: {}, commands: {}})
|
||||
try { this.$set(this.devices[id].commands, cmdName, JSON.parse(payload)) }
|
||||
catch(e) { this.$set(this.devices[id].commands, cmdName, { raw: payload }) }
|
||||
this.mqtt.client.on('connect', () => {
|
||||
console.log('Connected to MQTT broker')
|
||||
this.mqtt.subscribe('asset/+/status', this.handleStatus)
|
||||
this.mqtt.subscribe('asset/+/property/#', this.handleProperty)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
selectDevice(id) {
|
||||
this.selected = id
|
||||
this.buildTree()
|
||||
},
|
||||
sendCommand(cmdName) {
|
||||
const topic = `asset/${this.selected}/command/${cmdName}`
|
||||
this.mqtt.publish(topic, JSON.stringify({args:{}}))
|
||||
|
||||
handleStatus(payload, topic) {
|
||||
const deviceId = topic.split('/')[1]
|
||||
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
|
||||
this.devices[deviceId].status = payload
|
||||
},
|
||||
buildTree() {
|
||||
const buildNode = (obj, label) => {
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
return { label, children: Object.entries(obj).map(([k,v]) => buildNode(v,k)) }
|
||||
|
||||
handleProperty(payload, topic) {
|
||||
const parts = topic.split('/')
|
||||
const deviceId = parts[1]
|
||||
const propertyPath = parts.slice(3) // after asset/device/property/...
|
||||
if (!this.devices[deviceId]) this.devices[deviceId] = { property: {}, status: null }
|
||||
|
||||
// traverse/create tree
|
||||
let node = this.devices[deviceId].property
|
||||
propertyPath.forEach((p, i) => {
|
||||
if (i === propertyPath.length - 1) {
|
||||
// leaf
|
||||
node[p] = { value: payload, updated: Date.now() } // flash on update
|
||||
} else {
|
||||
return { label: `${label}: ${obj}` }
|
||||
if (!node[p]) node[p] = {}
|
||||
node = node[p]
|
||||
}
|
||||
}
|
||||
this.treeData = Object.entries(this.devices[this.selected].properties).map(([k,v]) => buildNode(v,k))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.p-m-2 { margin: .5rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.mb-3 { margin-bottom: 1rem; }
|
||||
</style>
|
||||
70
dashboard/src/PropertyTree.vue
Normal file
70
dashboard/src/PropertyTree.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<Tree
|
||||
:value="treeData"
|
||||
:expansion-keys="expansionKeys"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span
|
||||
:style="{
|
||||
backgroundColor: recentlyUpdated(node) ? '#fffa90' : 'transparent',
|
||||
padding: '0 2px',
|
||||
borderRadius: '2px'
|
||||
}"
|
||||
>
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tree from 'primevue/tree'
|
||||
|
||||
export default {
|
||||
name: 'PropertyTree',
|
||||
components: { Tree },
|
||||
props: {
|
||||
properties: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
treeData: [],
|
||||
expansionKeys: {},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
properties: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.treeData = this.buildTree(this.properties)
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildTree(obj, path = []) {
|
||||
return Object.entries(obj).map(([key, val]) => {
|
||||
if (val && typeof val === 'object' && 'value' in val) {
|
||||
return {
|
||||
key: [...path, key].join('/'),
|
||||
label: `${key}: ${val.value}`,
|
||||
data: val,
|
||||
}
|
||||
} else if (val && typeof val === 'object') {
|
||||
const children = this.buildTree(val, [...path, key])
|
||||
this.expansionKeys[[...path, key].join('/')] = true
|
||||
return {
|
||||
key: [...path, key].join('/'),
|
||||
label: key,
|
||||
children,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
recentlyUpdated(node) {
|
||||
const updated = node.data?.updated
|
||||
return updated && Date.now() - updated < 500
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,19 +1,24 @@
|
||||
import mqtt from 'mqtt'
|
||||
|
||||
export default class MQTTService {
|
||||
constructor(brokerUrl = 'ws://localhost:8083/mqtt', clientId = null) {
|
||||
constructor(brokerUrl = 'ws://127.0.0.1:8083/mqtt', clientId = null) {
|
||||
this.clientId = clientId || 'vue-client-' + Math.random().toString(16).substr(2, 8)
|
||||
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId })
|
||||
this.subscriptions = {}
|
||||
this.subscriptions = [] // array of {topic, callback}
|
||||
|
||||
this.client.on('connect', () => console.log('Connected to MQTT broker'))
|
||||
this.client.on('message', (topic, payload) => {
|
||||
if (this.subscriptions[topic]) this.subscriptions[topic](payload.toString(), topic)
|
||||
// iterate over subscriptions and check for matches
|
||||
this.subscriptions.forEach(({topic: subTopic, callback}) => {
|
||||
if (mqttMatch(subTopic, topic)) {
|
||||
callback(payload.toString(), topic)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
subscribe(topic, callback) {
|
||||
this.subscriptions[topic] = callback
|
||||
this.subscriptions.push({topic, callback})
|
||||
this.client.subscribe(topic)
|
||||
}
|
||||
|
||||
@ -21,3 +26,10 @@ export default class MQTTService {
|
||||
this.client.publish(topic, message, options)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for MQTT wildcards
|
||||
function mqttMatch(subTopic, topic) {
|
||||
// replace MQTT wildcards with RegExp
|
||||
const regex = '^' + subTopic.replace('+', '[^/]+').replace('#', '.+') + '$'
|
||||
return new RegExp(regex).test(topic)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user