Zigbee web panel migration [WIP]
This commit is contained in:
parent
b8979040da
commit
86e6ffd18d
11 changed files with 1243 additions and 88 deletions
160
platypush/backend/http/webapp/package-lock.json
generated
160
platypush/backend/http/webapp/package-lock.json
generated
|
@ -1745,6 +1745,16 @@
|
|||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||
|
@ -1771,6 +1781,34 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
|
||||
|
@ -1792,6 +1830,25 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -1856,6 +1913,16 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -1872,6 +1939,18 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -11741,87 +11820,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-yR+BS90EOXTNieasf8ce9J3TFCpm2DGqoqdbtiwQ33hon3FyIznLX7sKavAq1VmfBnOeV6It0Htg4aniv8ph1g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "4.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.3.tgz",
|
||||
|
|
BIN
platypush/backend/http/webapp/public/icons/z-wave.png
Normal file
BIN
platypush/backend/http/webapp/public/icons/z-wave.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
5
platypush/backend/http/webapp/public/icons/zigbee.svg
Normal file
5
platypush/backend/http/webapp/public/icons/zigbee.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<style>.color {fill: #5f7869}</style>
|
||||
<title>Zigbee icon</title>
|
||||
<path class="color" d="M11.988 0a11.85 11.85 0 00-8.617 3.696c7.02-.875 11.401-.583 13.289-.34 3.752.583 3.558 3.404 3.558 3.404L8.237 19.112c2.299.22 6.897.366 13.796-.631a11.86 11.86 0 001.912-6.469C23.945 5.374 18.595 0 11.988 0zm.232 4.31c-2.451-.014-5.772.146-9.963.723C.854 7.003.055 9.41.055 12.012.055 18.626 5.38 24 11.988 24c3.63 0 6.85-1.63 9.053-4.182-7.286.948-11.813.631-13.75.388-3.775-.56-3.557-3.404-3.557-3.404L15.691 4.474a38.635 38.635 0 00-3.471-.163Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 632 B |
|
@ -26,6 +26,9 @@
|
|||
},
|
||||
"rtorrent": {
|
||||
"class": "fa fa-magnet"
|
||||
},
|
||||
"zigbee.mqtt": {
|
||||
"imgUrl": "/icons/zigbee.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<a :href="`/#${name}`">
|
||||
<span class="icon">
|
||||
<i :class="icons[name].class" v-if="icons[name]?.class" />
|
||||
<img :src="icons[name].imgUrl" v-else-if="icons[name]?.imgUrl" alt="name"/>
|
||||
<i class="fas fa-puzzle-piece" v-else />
|
||||
</span>
|
||||
<span class="name" v-if="!collapsed" v-text="name" />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="row item" @click="clicked">
|
||||
<div class="col-1 icon">
|
||||
<i :class="iconClass" v-if="iconClass" />
|
||||
<div class="col-1 icon" v-if="iconClass">
|
||||
<i :class="iconClass" />
|
||||
</div>
|
||||
<div class="col-11 text" v-text="text" />
|
||||
<div class="text" :class="{'col-11': iconClass != null}" v-text="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
@change="$emit('input', $event)" @mouseup="$emit('mouseup', $event)" @input="$emit('input', $event)"
|
||||
@mousedown="$emit('mousedown', $event)" @touch="$emit('input', $event)"
|
||||
@touchstart="$emit('mousedown', $event)" @touchend="$emit('mouseup', $event)">
|
||||
|
||||
<span class="label" v-if="withLabel" v-text="value" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
@ -25,6 +27,11 @@ export default {
|
|||
type: Array,
|
||||
default: () => [0, 100],
|
||||
},
|
||||
|
||||
withLabel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -62,14 +69,21 @@ export default {
|
|||
&[disabled] {
|
||||
&::-webkit-progress-value,
|
||||
&::-moz-range-progress {
|
||||
background: none;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb,
|
||||
&::-moz-range-thumb {
|
||||
display: none;
|
||||
width: 0;
|
||||
opacity: .4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
.label {
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,498 @@
|
|||
<template>
|
||||
<div class="item device" :class="{selected: selected}">
|
||||
<Loading v-if="loading" />
|
||||
<div class="row name header vertical-center" :class="{selected: selected}"
|
||||
v-text="device.friendly_name || device.ieee_address" @click="$emit('select')" />
|
||||
|
||||
<div class="params" v-if="selected">
|
||||
<div class="row">
|
||||
<div class="param-name">Name</div>
|
||||
<div class="param-value">
|
||||
<div class="name-edit" :class="{hidden: !editName}">
|
||||
<form @submit.prevent="rename">
|
||||
<label>
|
||||
<input type="text" name="name" ref="name" :value="device.friendly_name">
|
||||
</label>
|
||||
|
||||
<span class="buttons">
|
||||
<button type="button" class="btn btn-default" @click="editName = false">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-default">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="name-edit" :class="{hidden: editName}">
|
||||
<span v-text="device.friendly_name"></span>
|
||||
<span class="buttons">
|
||||
<button type="button" class="btn btn-default" @click="editName = true">
|
||||
<i class="fa fa-edit"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">IEEE Address</div>
|
||||
<div class="param-value" v-text="device.ieee_address"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.network_address">
|
||||
<div class="param-name">Network Address</div>
|
||||
<div class="param-value" v-text="device.network_address"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Type</div>
|
||||
<div class="param-value" v-text="device.type"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.definition?.vendor">
|
||||
<div class="param-name">Vendor</div>
|
||||
<div class="param-value">
|
||||
{{ device.definition.vendor }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.definition?.model">
|
||||
<div class="param-name">Model</div>
|
||||
<div class="param-value">
|
||||
{{ device.definition.model }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.model_id">
|
||||
<div class="param-name">Model ID</div>
|
||||
<div class="param-value">
|
||||
{{ device.model_id }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.definition?.description">
|
||||
<div class="param-name">Description</div>
|
||||
<div class="param-value">
|
||||
{{ device.definition.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.software_build_id">
|
||||
<div class="param-name">Software Build ID</div>
|
||||
<div class="param-value">
|
||||
{{ device.software_build_id }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.definition?.date_code">
|
||||
<div class="param-name">Date Code</div>
|
||||
<div class="param-value">
|
||||
{{ device.definition.date_code }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="device.power_source">
|
||||
<div class="param-name">Power Source</div>
|
||||
<div class="param-value">
|
||||
{{ device.power_source }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section values" v-if="Object.keys(displayedValues).length">
|
||||
<div class="header">
|
||||
<div class="title">Values</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row value" v-for="(value, property) in displayedValues" :key="property">
|
||||
<div class="param-name">
|
||||
{{ value.description }}
|
||||
<span class="text" v-if="value.value?.x != null && value.value?.y != null">Color (XY coordinates)</span>
|
||||
<span class="text" v-else-if="value.value?.hue != null && value.value?.saturation != null">Color (Hue/saturation)</span>
|
||||
<span class="name" v-text="value.property" v-if="value.property" />
|
||||
<span class="unit" v-text="value.unit" v-if="value.unit" />
|
||||
</div>
|
||||
|
||||
<div class="param-value">
|
||||
<ToggleSwitch :value="value.value_on != null ? value.value === value.value_on : !!value.value"
|
||||
v-if="value.type === 'binary'" @input="setValue(value, $event)" />
|
||||
|
||||
<Slider :with-label="true" :range="[value.value_min, value.value_max]" :value="value.value"
|
||||
:disabled="!value.writable" @change="setValue(value, $event)"
|
||||
v-else-if="value.type === 'numeric' && value.value_min != null && value.value_max != null" />
|
||||
|
||||
<label v-else-if="value.type === 'numeric' && (value.value_min == null || value.value_max == null)">
|
||||
<input type="number" :with-label="true" :value="value.value" :disabled="!value.writable"
|
||||
@change="setValue(value, $event)" />
|
||||
</label>
|
||||
|
||||
<label v-else-if="value.type === 'enum'">
|
||||
<select :value="value.readable && value.value != null ? value.value : ''"
|
||||
@change="setValue(value, $event)">
|
||||
<option v-if="!value.readable" />
|
||||
<option v-for="option in value.values" :key="option" :value="option" v-text="option"
|
||||
:selected="value.readable && value.value === option" :disabled="!value.writable" />
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label v-else-if="(value.value?.x != null && value.value?.y != null) || (value.value?.hue != null && value.value?.saturation != null)">
|
||||
<!-- <input type="color" :value="rgbColor" @change.stop="onColorSelect" />-->
|
||||
</label>
|
||||
|
||||
<label v-else>
|
||||
<input type="text" :disabled="!value.writable" :value="value.value" @change="setValue(value, $event)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section actions">
|
||||
<div class="header">
|
||||
<div class="title">Actions</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<!-- <div class="row" @click="removeDevice(false)">-->
|
||||
<!-- <div class="param-name">Remove Device</div>-->
|
||||
<!-- <div class="param-value">-->
|
||||
<!-- <i class="fa fa-trash"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="row error" @click="removeDevice(true)">-->
|
||||
<!-- <div class="param-name">Force Remove Device</div>-->
|
||||
<!-- <div class="param-value">-->
|
||||
<!-- <i class="fa fa-trash"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="row" @click="banDevice">-->
|
||||
<!-- <div class="param-name">Ban Device</div>-->
|
||||
<!-- <div class="param-value">-->
|
||||
<!-- <i class="fa fa-ban"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- <div class="row" @click="whitelistDevice">-->
|
||||
<!-- <div class="param-name">Whitelist Device</div>-->
|
||||
<!-- <div class="param-value">-->
|
||||
<!-- <i class="fa fa-list"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Loading from "@/components/Loading";
|
||||
import Slider from "@/components/elements/Slider";
|
||||
import ToggleSwitch from "@/components/elements/ToggleSwitch";
|
||||
import Utils from "@/Utils";
|
||||
// import {ColorConverter} from "@/components/panels/Light/color";
|
||||
|
||||
export default {
|
||||
name: "Device",
|
||||
components: {ToggleSwitch, Slider, Loading},
|
||||
mixins: [Utils],
|
||||
emits: ['select', 'rename'],
|
||||
|
||||
props: {
|
||||
device: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editName: false,
|
||||
loading: false,
|
||||
status: {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
values() {
|
||||
if (!this.device.definition?.exposes)
|
||||
return {}
|
||||
|
||||
const extractValues = (values) => {
|
||||
const extractValue = (value, root) => {
|
||||
if (!value.features) {
|
||||
if (value.property)
|
||||
root[value.property] = value
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (value.property) {
|
||||
root[value.property] = root[value.property] || {}
|
||||
root = root[value.property]
|
||||
}
|
||||
|
||||
for (const feature of value.features)
|
||||
extractValue(feature, root)
|
||||
}
|
||||
|
||||
const ret = {}
|
||||
for (const value of values)
|
||||
extractValue(value, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
return extractValues(this.device.definition.exposes)
|
||||
},
|
||||
|
||||
displayedValues() {
|
||||
const ret = {}
|
||||
const mergeValues = (obj, [key, value]) => {
|
||||
if (key in this.status)
|
||||
value = {
|
||||
...value,
|
||||
value: this.status[key]
|
||||
}
|
||||
|
||||
if (value.access != null) {
|
||||
value.readable = !!(value.access & 0x1)
|
||||
value.writable = !!(value.access & 0x2)
|
||||
delete value.access
|
||||
}
|
||||
|
||||
obj[key] = value
|
||||
Object.entries(value).filter((v) => v[1] instanceof Object).reduce(mergeValues, obj[key])
|
||||
return obj
|
||||
}
|
||||
|
||||
Object.entries(this.values).reduce(mergeValues, ret)
|
||||
return ret
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
try {
|
||||
this.status = await this.request('zigbee.mqtt.device_get',
|
||||
{device: this.device.friendly_name || this.device.ieee_address})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async rename() {
|
||||
const name = (this.$refs.name.value || '').trim()
|
||||
if (!name.length || name === this.device.friendly_name)
|
||||
return
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.device_rename', {
|
||||
device: this.device.friendly_name?.length ? this.device.friendly_name : this.device.ieee_address,
|
||||
name: name,
|
||||
})
|
||||
|
||||
this.$emit('rename', {name: this.device.friendly_name, newName: name});
|
||||
} finally {
|
||||
this.editName = false
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async setValue(value, event) {
|
||||
const request = {
|
||||
device: this.device.friendly_name || this.device.ieee_address,
|
||||
property: value.property,
|
||||
value: null,
|
||||
}
|
||||
|
||||
switch (value.type) {
|
||||
case 'binary':
|
||||
if (value.value_toggle) {
|
||||
request.value = value.value_toggle
|
||||
} else if (value.value_on && value.value_off) {
|
||||
request.value = value.value === value.value_on ? value.value_off : value.value_on
|
||||
} else {
|
||||
request.value = !value.value
|
||||
}
|
||||
break
|
||||
|
||||
case 'numeric':
|
||||
request.value = parseFloat(event.target.value)
|
||||
break
|
||||
|
||||
case 'enum':
|
||||
if (event.target.value?.length) {
|
||||
request.value = event.target.value
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (request.value == null)
|
||||
return
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.device_set', request)
|
||||
await this.refresh()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.refresh()
|
||||
this.$watch(() => this.selected, (newValue) => {
|
||||
if (newValue)
|
||||
this.refresh()
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
|
||||
.item {
|
||||
&.selected {
|
||||
box-shadow: $selected-item-box-shadow;
|
||||
}
|
||||
|
||||
.name.header {
|
||||
padding: 1em !important;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
|
||||
&.selected {
|
||||
border-radius: 1.5em;
|
||||
}
|
||||
&.selected {
|
||||
background: $selected-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
|
||||
&.selected {
|
||||
background: $selected-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: $item-border;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: 1.5em 1.5em 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 1.5em 1.5em;
|
||||
}
|
||||
|
||||
.params {
|
||||
.section {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
.param-name {
|
||||
display: inline-block;
|
||||
|
||||
.name {
|
||||
font-family: monospace;
|
||||
font-size: .8em;
|
||||
text-transform: unset;
|
||||
padding: 0;
|
||||
|
||||
&:before {
|
||||
content: '[';
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ']';
|
||||
}
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: .8em;
|
||||
&:before {
|
||||
content: ' [unit: ';
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.param-value {
|
||||
label {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0 .5em;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.name-edit {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items:center;
|
||||
justify-content: right;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-flex;
|
||||
justify-content: right;
|
||||
margin: 0 0 0 .5em;
|
||||
}
|
||||
|
||||
form {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,521 @@
|
|||
<template>
|
||||
<div class="zigbee-container">
|
||||
<Loading v-if="loading" />
|
||||
|
||||
<!-- Include group modal -->
|
||||
|
||||
<div class="view-options">
|
||||
<div class="view-selector col-s-8 col-m-9 col-l-10">
|
||||
<label>
|
||||
<select :value="selected.view" @change="this.selected.view = $event.target.value">
|
||||
<option v-for="(enabled, view) in views"
|
||||
v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
|
||||
:key="view" :selected="enabled" :value="view">
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn btn-default" title="Add Group" v-if="selected.view === 'groups'"
|
||||
:disabled="loading" @click="addGroup">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<Dropdown ref="networkCommandsDropdown" icon-class="fa fa-cog" title="Network commands">
|
||||
<DropdownItem text="Start Network" :disabled="loading" @click="startNetwork" />
|
||||
<DropdownItem text="Stop Network" :disabled="loading" @click="stopNetwork" />
|
||||
<DropdownItem text="Permit Join" :disabled="loading" @click="permitJoin(true)" />
|
||||
<DropdownItem text="Reset" :disabled="loading" @click="reset" />
|
||||
<DropdownItem text="Factory Reset" :disabled="loading" @click="factoryReset" />
|
||||
</Dropdown>
|
||||
|
||||
<button class="btn btn-default" title="Refresh network" :disabled="loading" @click="refresh">
|
||||
<i class="fa fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="view-container">
|
||||
<div class="view devices" v-if="selected.view === 'devices'">
|
||||
<div class="no-items" v-if="!Object.keys(devices).length">
|
||||
<div class="loading" v-if="loading">Loading devices...</div>
|
||||
<div class="empty" v-else>No devices found on the network</div>
|
||||
</div>
|
||||
|
||||
<ZigbeeDevice v-for="(device, id) in devices" :key="id"
|
||||
:device="device" :selected="selected.deviceId === id"
|
||||
@select="selected.deviceId = selected.deviceId === id ? null : id"
|
||||
@rename="refreshDevices" />
|
||||
|
||||
<!-- <dropdown ref="addToGroupDropdown" :items="addToGroupDropdownItems"></dropdown>-->
|
||||
</div>
|
||||
|
||||
<div class="view groups" v-else-if="selected.view === 'groups'">
|
||||
<div class="no-items" v-if="!Object.keys(groups).length">
|
||||
<div class="loading" v-if="loading">Loading groups...</div>
|
||||
<div class="empty" v-else>No groups available on the network</div>
|
||||
</div>
|
||||
|
||||
<!-- <zigbee-group-->
|
||||
<!-- v-for="(group, groupId) in groups"-->
|
||||
<!-- :key="groupId"-->
|
||||
<!-- :group="group"-->
|
||||
<!-- :selected="selected.groupId === groupId"-->
|
||||
<!-- :bus="bus">-->
|
||||
<!-- </zigbee-group>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "../../elements/Dropdown"
|
||||
import DropdownItem from "@/components/elements/DropdownItem"
|
||||
import Loading from "@/components/Loading"
|
||||
import Utils from "@/Utils"
|
||||
|
||||
import ZigbeeDevice from "@/components/panels/ZigbeeMqtt/Device";
|
||||
|
||||
export default {
|
||||
name: "ZigbeeMqtt",
|
||||
components: {Dropdown, DropdownItem, Loading, ZigbeeDevice},
|
||||
mixins: [Utils],
|
||||
|
||||
data() {
|
||||
return {
|
||||
status: {},
|
||||
devices: {},
|
||||
groups: {},
|
||||
loading: false,
|
||||
selected: {
|
||||
view: 'devices',
|
||||
deviceId: undefined,
|
||||
groupId: undefined,
|
||||
},
|
||||
views: {
|
||||
devices: true,
|
||||
groups: true,
|
||||
},
|
||||
modal: {
|
||||
group: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// addToGroupDropdownItems: function() {
|
||||
// const self = this
|
||||
// return Object.values(this.groups).filter((group) => {
|
||||
// return !group.values || !group.values.length || !(this.selected.valueId in this.scene.values)
|
||||
// }).map((group) => {
|
||||
// return {
|
||||
// text: group.name,
|
||||
// disabled: this.loading,
|
||||
// click: async function () {
|
||||
// if (!self.selected.valueId) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.loading = true
|
||||
// await this.request('zwave.scene_add_value', {
|
||||
// id_on_network: self.selected.valueId,
|
||||
// scene_id: group.scene_id,
|
||||
// })
|
||||
//
|
||||
// self.loading = false
|
||||
// self.refresh()
|
||||
// },
|
||||
// }
|
||||
// })
|
||||
// },
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshDevices() {
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.devices = (await this.request('zigbee.mqtt.devices')).reduce((devices, device) => {
|
||||
if (device.friendly_name in this.devices) {
|
||||
device = {
|
||||
values: this.devices[device.friendly_name].values || {},
|
||||
...this.devices[device.friendly_name],
|
||||
}
|
||||
}
|
||||
|
||||
devices[device.friendly_name] = device
|
||||
return devices
|
||||
}, {})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async refreshGroups() {
|
||||
this.loading = true
|
||||
this.groups = (await this.request('zigbee.mqtt.groups')).reduce((groups, group) => {
|
||||
groups[group.id] = group
|
||||
return groups
|
||||
}, {})
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.refreshDevices()
|
||||
this.refreshGroups()
|
||||
},
|
||||
|
||||
updateProperties(device, props) {
|
||||
this.devices[device].values = props
|
||||
},
|
||||
|
||||
async addGroup() {
|
||||
const name = prompt('Group name')
|
||||
if (!(name && name.length)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.group_add', {name: name})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
await this.refreshGroups()
|
||||
},
|
||||
|
||||
async startNetwork() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.start_network')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async stopNetwork() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.stop_network')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async permitJoin(permit) {
|
||||
let seconds = prompt('Join allow period in seconds (type 0 for no time limits)', '60')
|
||||
if (!seconds) {
|
||||
return
|
||||
}
|
||||
|
||||
seconds = parseInt(seconds)
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.request('zigbee.mqtt.permit_join', {permit: !!permit, timeout: seconds || null})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async reset() {
|
||||
if (!confirm('Are you sure that you want to reset the device?')) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.reset')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async factoryReset() {
|
||||
if (!confirm('Are you SURE that you want to do a device factory reset?')) {
|
||||
if (!confirm('Are you REALLY sure? ALL network information and custom firmware will be lost!!'))
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
await this.request('zigbee.mqtt.factory_reset')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// openAddToGroupDropdown(event) {
|
||||
// this.selected.valueId = event.valueId
|
||||
// openDropdown(this.$refs.addToGroupDropdown)
|
||||
// },
|
||||
|
||||
async addToGroup(device, group) {
|
||||
this.loading = true
|
||||
await this.request('zigbee.mqtt.group_add_device', {
|
||||
device: device,
|
||||
group: group,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
const self = this
|
||||
|
||||
setTimeout(() => {
|
||||
self.refresh()
|
||||
self.refreshGroups()
|
||||
}, 100)
|
||||
},
|
||||
|
||||
async removeNodeFromGroup(event) {
|
||||
if (!confirm('Are you sure that you want to remove this value from the group?')) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
await this.request('zigbee.mqtt.group_remove_device', {
|
||||
group: event.group,
|
||||
device: event.device,
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
// const self = this
|
||||
// this.bus.$on('refresh', this.refresh)
|
||||
// this.bus.$on('refreshDevices', this.refreshDevices)
|
||||
// this.bus.$on('refreshGroups', this.refreshGroups)
|
||||
// this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true})
|
||||
// this.bus.$on('openAddToGroupDropdown', this.openAddToGroupDropdown)
|
||||
// this.bus.$on('removeFromGroup', this.removeNodeFromGroup)
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'WARNING: The controller is now offline',
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttOfflineEvent')
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'The controller is now online',
|
||||
// iconClass: 'fas fa-check',
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttOfflineEvent')
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'Failed to remove the device',
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedFailedEvent')
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'Failed to add the group',
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedFailedEvent')
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'Failed to remove the group',
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedFailedEvent')
|
||||
//
|
||||
// registerEventHandler(() => {
|
||||
// createNotification({
|
||||
// text: 'Failed to remove the devices from the group',
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllFailedEvent')
|
||||
//
|
||||
// registerEventHandler((event) => {
|
||||
// createNotification({
|
||||
// text: 'Unhandled Zigbee error: ' + (event.error || '[Unknown error]'),
|
||||
// error: true,
|
||||
// })
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttErrorEvent')
|
||||
//
|
||||
// registerEventHandler((event) => {
|
||||
// self.updateProperties(event.device, event.properties)
|
||||
// }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent')
|
||||
//
|
||||
// registerEventHandler(this.refresh,
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePairingEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceConnectedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBannedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceWhitelistedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRenamedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBindEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceUnbindEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedEvent',
|
||||
// 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllEvent',
|
||||
// )
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.refresh()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "common";
|
||||
|
||||
.zigbee-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
|
||||
.no-items {
|
||||
padding: 2em;
|
||||
font-size: 1.5em;
|
||||
color: $no-items-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.view-options {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: $header-height;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
background: $header-bg;
|
||||
border-bottom: $default-border-2;
|
||||
box-shadow: $border-shadow-bottom;
|
||||
|
||||
.view-selector {
|
||||
display: inline-flex;
|
||||
padding-left: .5em;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 .75em;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.item {
|
||||
padding: .5em 2em .5em .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-container {
|
||||
width: 100%;
|
||||
height: calc(100% - #{$header-height});
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.view {
|
||||
min-width: 400pt;
|
||||
max-width: 750pt;
|
||||
height: max-content;
|
||||
background: $view-bg;
|
||||
border: $view-border;
|
||||
border-radius: 1.5em;
|
||||
box-shadow: $view-box-shadow;
|
||||
}
|
||||
|
||||
.params {
|
||||
background: $params-bg;
|
||||
padding-bottom: 1em;
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 1em;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
border-bottom: $param-section-header-border;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.btn-value-name-edit {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
.section {
|
||||
.header {
|
||||
background: none;
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.network-info {
|
||||
min-width: 600pt;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
.device, .group {
|
||||
.actions {
|
||||
.row {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
input[type=text] {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,102 @@
|
|||
@import "vars";
|
||||
|
||||
.zigbee-container {
|
||||
.view {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 1em;
|
||||
padding: .3em;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: $param-even-row-bg;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: $param-odd-row-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
.param-name {
|
||||
display: inline-flex;
|
||||
width: 40%;
|
||||
margin-left: 1%;
|
||||
vertical-align: top;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
display: inline-flex;
|
||||
width: 58%;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
|
||||
.value-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value-data {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: .8em;
|
||||
margin-left: 1em;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.numeric {
|
||||
input.slider {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
background: none;
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.value-min, .value-max {
|
||||
width: 50%;
|
||||
font-size: .85em;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.value-min {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.value-max {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
$view-bg: white;
|
||||
$view-border: 1px solid #d8d8d8;
|
||||
$view-box-shadow: 1px 2px 2px #ccc;
|
||||
$item-border: 1px solid #dddddd;
|
||||
$no-items-color: #555555;
|
||||
$params-bg: white;
|
||||
$param-even-row-bg: #f0f0f0;
|
||||
$param-odd-row-bg: white;
|
||||
$param-section-header-border: 1px solid #e8e8e8;
|
||||
$selected-item-box-shadow: 0 2px 4px 0 #bbb;
|
||||
$error-color: #aa0000;
|
||||
$header-bg: #f8f8f8;
|
||||
$header-height: 3.5em;
|
Loading…
Reference in a new issue