Compare commits
21 commits
486cd66885
...
00a43dd1f8
Author | SHA1 | Date | |
---|---|---|---|
00a43dd1f8 | |||
801ed05684 | |||
6454f9d018 | |||
0f19104512 | |||
5ca3c06f96 | |||
d5f8d55b4b | |||
636d1ced3a | |||
7db84acd34 | |||
02abef71e3 | |||
64513be6b8 | |||
440cd60d6e | |||
3d1a08f7af | |||
68dd09e8ae | |||
d7214c4c83 | |||
a1cf671334 | |||
78dc8416fb | |||
691d109fb7 | |||
71ccf6d04a | |||
42651e937b | |||
d61b053f72 | |||
cdacf50fc7 |
31 changed files with 1238 additions and 207 deletions
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity battery-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:icon="icon"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-8 col-m-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-3 col-m-2 buttons pull-right">
|
||||||
|
<span class="value-percent"
|
||||||
|
v-text="valuePercent + '%'"
|
||||||
|
v-if="valuePercent != null" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
|
||||||
|
const thresholds = [
|
||||||
|
{
|
||||||
|
iconClass: 'full',
|
||||||
|
color: '#157145',
|
||||||
|
value: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconClass: 'three-quarters',
|
||||||
|
color: '#94C595',
|
||||||
|
value: 0.825,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconClass: 'half',
|
||||||
|
color: '#F0B67F',
|
||||||
|
value: 0.625,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconClass: 'quarter',
|
||||||
|
color: '#FE5F55',
|
||||||
|
value: 0.375,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconClass: 'low',
|
||||||
|
color: '#CC444B',
|
||||||
|
value: 0.15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconClass: 'empty',
|
||||||
|
color: '#EC0B43',
|
||||||
|
value: 0.05,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Battery',
|
||||||
|
components: {EntityIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
valuePercent() {
|
||||||
|
if (this.value?.value == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const min = this.value.min || 0
|
||||||
|
const max = this.value.max || 100
|
||||||
|
return ((100 * this.value.value) / (max - min)).toFixed(0)
|
||||||
|
},
|
||||||
|
|
||||||
|
icon() {
|
||||||
|
const icon = {...(this.value.meta?.icon || {})}
|
||||||
|
let value = this.valuePercent
|
||||||
|
let threshold = thresholds[0]
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
value = parseFloat(value) / 100
|
||||||
|
for (const t of thresholds) {
|
||||||
|
if (value > t.value)
|
||||||
|
break
|
||||||
|
threshold = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icon['class'] = `fas fa-battery-${threshold.iconClass}`
|
||||||
|
icon['color'] = threshold.color
|
||||||
|
return icon
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
prevent(event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.battery-container {
|
||||||
|
.head {
|
||||||
|
.value-percent {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity sensor-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:icon="value.meta?.icon || {}"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-8 col-m-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-3 col-m-2 pull-right" v-if="value.value != null">
|
||||||
|
<ToggleSwitch :value="value.value" :disabled="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
import ToggleSwitch from "@/components/elements/ToggleSwitch"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BinarySensor',
|
||||||
|
components: {EntityIcon, ToggleSwitch},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.sensor-container {
|
||||||
|
.head {
|
||||||
|
.value {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
margin-left: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -26,10 +26,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.type !== 'Entity')
|
if (this.type !== 'Entity') {
|
||||||
|
const type = this.type.split('_').map((t) =>
|
||||||
|
t[0].toUpperCase() + t.slice(1)
|
||||||
|
).join('')
|
||||||
|
|
||||||
this.component = defineAsyncComponent(
|
this.component = defineAsyncComponent(
|
||||||
() => import(`@/components/panels/Entities/${this.type}`)
|
() => import(`@/components/panels/Entities/${type}`)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -31,7 +31,8 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
type() {
|
type() {
|
||||||
let entityType = (this.value.type || '')
|
let entityType = (this.value.type || '')
|
||||||
return entityType.charAt(0).toUpperCase() + entityType.slice(1)
|
return entityType.split('_').
|
||||||
|
map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity switch-container">
|
||||||
|
<div class="head" :class="{expanded: expanded}">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:icon="this.value.meta?.icon || {}"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-8 col-m-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-3 col-m-2 buttons pull-right">
|
||||||
|
<button @click.stop="expanded = !expanded" v-if="hasValues">
|
||||||
|
<i class="fas"
|
||||||
|
:class="{'fa-angle-up': expanded, 'fa-angle-down': !expanded}" />
|
||||||
|
</button>
|
||||||
|
<span class="value"
|
||||||
|
v-text="value.value"
|
||||||
|
v-if="value?.value != null" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body" v-if="expanded" @click.stop="prevent">
|
||||||
|
<div class="row">
|
||||||
|
<div class="input">
|
||||||
|
<select @input="setValue" ref="values">
|
||||||
|
<option value="" v-if="value.is_write_only" selected>--</option>
|
||||||
|
<option :value="v" v-for="v in value.values" :key="v" v-text="v" />
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EnumSwitch',
|
||||||
|
components: {EntityIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasValues() {
|
||||||
|
return !!this?.value?.values?.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
prevent(event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
async setValue(event) {
|
||||||
|
if (!event.target.value?.length)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.$emit('loading', true)
|
||||||
|
if (this.value.is_write_only) {
|
||||||
|
const self = this;
|
||||||
|
setTimeout(() => {
|
||||||
|
self.$refs.values.value = ''
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.request('entities.execute', {
|
||||||
|
id: this.value.id,
|
||||||
|
action: 'set_value',
|
||||||
|
data: event.target.value,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.$emit('loading', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
.head {
|
||||||
|
.buttons {
|
||||||
|
button {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -27,16 +27,14 @@
|
||||||
<div class="frame">
|
<div class="frame">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="section left">
|
<span class="section left">
|
||||||
<Icon v-bind="entitiesMeta[group.name].icon || {}"
|
<Icon v-bind="entitiesMeta[typesByCategory[group.name]].icon || {}"
|
||||||
v-if="selector.grouping === 'type' && entitiesMeta[group.name]" />
|
v-if="selector.grouping === 'category' && entitiesMeta[typesByCategory[group.name]]" />
|
||||||
<Icon :class="pluginIcons[group.name]?.class" :url="pluginIcons[group.name]?.imgUrl"
|
<Icon :class="pluginIcons[group.name]?.class" :url="pluginIcons[group.name]?.imgUrl"
|
||||||
v-else-if="selector.grouping === 'plugin' && pluginIcons[group.name]" />
|
v-else-if="selector.grouping === 'plugin' && pluginIcons[group.name]" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="section center">
|
<span class="section center">
|
||||||
<div class="title" v-text="entitiesMeta[group.name].name_plural"
|
<div class="title" v-text="group.name" />
|
||||||
v-if="selector.grouping === 'type' && entitiesMeta[group.name]"/>
|
|
||||||
<div class="title" v-text="group.name" v-else-if="selector.grouping === 'plugin'"/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="section right">
|
<span class="section right">
|
||||||
|
@ -99,7 +97,7 @@ export default {
|
||||||
modalEntityId: null,
|
modalEntityId: null,
|
||||||
modalVisible: false,
|
modalVisible: false,
|
||||||
selector: {
|
selector: {
|
||||||
grouping: 'type',
|
grouping: 'category',
|
||||||
selectedEntities: {},
|
selectedEntities: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -114,13 +112,24 @@ export default {
|
||||||
return icons
|
return icons
|
||||||
},
|
},
|
||||||
|
|
||||||
|
entityTypes() {
|
||||||
|
return this.groupEntities('type')
|
||||||
|
},
|
||||||
|
|
||||||
|
typesByCategory() {
|
||||||
|
return Object.entries(meta).reduce((obj, [type, meta]) => {
|
||||||
|
obj[meta.name_plural] = type
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
|
||||||
entityGroups() {
|
entityGroups() {
|
||||||
return {
|
return {
|
||||||
'id': Object.entries(this.groupEntities('id')).reduce((obj, [id, entities]) => {
|
'id': Object.entries(this.groupEntities('id')).reduce((obj, [id, entities]) => {
|
||||||
obj[id] = entities[0]
|
obj[id] = entities[0]
|
||||||
return obj
|
return obj
|
||||||
}, {}),
|
}, {}),
|
||||||
'type': this.groupEntities('type'),
|
'category': this.groupEntities('category'),
|
||||||
'plugin': this.groupEntities('plugin'),
|
'plugin': this.groupEntities('plugin'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -148,6 +157,7 @@ export default {
|
||||||
return Object.values(this.entities).reduce((obj, entity) => {
|
return Object.values(this.entities).reduce((obj, entity) => {
|
||||||
const entities = obj[entity[attr]] || {}
|
const entities = obj[entity[attr]] || {}
|
||||||
entities[entity.id] = entity
|
entities[entity.id] = entity
|
||||||
|
|
||||||
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
|
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
|
@ -178,11 +188,7 @@ export default {
|
||||||
delete self.entityTimeouts[id]
|
delete self.entityTimeouts[id]
|
||||||
|
|
||||||
self.errorEntities[id] = entity
|
self.errorEntities[id] = entity
|
||||||
self.notify({
|
console.warn(`Scan timeout for ${entity.name}`)
|
||||||
error: true,
|
|
||||||
title: entity.plugin,
|
|
||||||
text: `Scan timeout for ${entity.name}`,
|
|
||||||
})
|
|
||||||
}, this.entityScanTimeout * 1000)
|
}, this.entityScanTimeout * 1000)
|
||||||
|
|
||||||
obj[id] = true
|
obj[id] = true
|
||||||
|
@ -198,6 +204,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
this.entities = (await this.request('entities.get')).reduce((obj, entity) => {
|
this.entities = (await this.request('entities.get')).reduce((obj, entity) => {
|
||||||
entity.name = entity?.meta?.name_override || entity.name
|
entity.name = entity?.meta?.name_override || entity.name
|
||||||
|
entity.category = meta[entity.type].name_plural
|
||||||
entity.meta = {
|
entity.meta = {
|
||||||
...(meta[entity.type] || {}),
|
...(meta[entity.type] || {}),
|
||||||
...(entity.meta || {}),
|
...(entity.meta || {}),
|
||||||
|
@ -225,6 +232,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onEntityInput(entity) {
|
onEntityInput(entity) {
|
||||||
|
entity.category = meta[entity.type].name_plural
|
||||||
this.entities[entity.id] = entity
|
this.entities[entity.id] = entity
|
||||||
this.clearEntityTimeouts(entity.id)
|
this.clearEntityTimeouts(entity.id)
|
||||||
if (this.loadingEntities[entity.id])
|
if (this.loadingEntities[entity.id])
|
||||||
|
@ -247,6 +255,7 @@ export default {
|
||||||
else
|
else
|
||||||
entity.name = event.entity?.name || this.entities[entityId]?.name
|
entity.name = event.entity?.name || this.entities[entityId]?.name
|
||||||
|
|
||||||
|
entity.category = meta[entity.type].name_plural
|
||||||
entity.meta = {
|
entity.meta = {
|
||||||
...(meta[event.entity.type] || {}),
|
...(meta[event.entity.type] || {}),
|
||||||
...(this.entities[entityId]?.meta || {}),
|
...(this.entities[entityId]?.meta || {}),
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity link-quality-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:icon="value.meta?.icon || {}"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-8 col-m-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-3 col-m-2 buttons pull-right">
|
||||||
|
<span class="value-percent"
|
||||||
|
v-text="valuePercent + '%'"
|
||||||
|
v-if="valuePercent != null" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LinkQuality',
|
||||||
|
components: {EntityIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
valuePercent() {
|
||||||
|
if (this.value?.value == null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const min = this.value.min || 0
|
||||||
|
const max = this.value.max || 100
|
||||||
|
return ((100 * this.value.value) / (max - min)).toFixed(0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.link-quality-container {
|
||||||
|
.head {
|
||||||
|
.value-percent {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -98,7 +98,15 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
prettifyGroupingName(name) {
|
prettifyGroupingName(name) {
|
||||||
return name ? this.prettify(name) + 's' : ''
|
if (!name)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
name = this.prettify(name)
|
||||||
|
if (name.endsWith('y'))
|
||||||
|
name = name.slice(0, name.length-1) + 'ie'
|
||||||
|
|
||||||
|
name += 's'
|
||||||
|
return name
|
||||||
},
|
},
|
||||||
|
|
||||||
iconForGroup(group) {
|
iconForGroup(group) {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="entity sensor-container">
|
||||||
|
<div class="head">
|
||||||
|
<div class="col-1 icon">
|
||||||
|
<EntityIcon
|
||||||
|
:icon="value.meta?.icon || {}"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-8 col-m-9 label">
|
||||||
|
<div class="name" v-text="value.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-s-3 col-m-2 pull-right"
|
||||||
|
v-if="value.value != null">
|
||||||
|
<span class="unit" v-text="value.unit"
|
||||||
|
v-if="value.unit != null" />
|
||||||
|
<span class="value" v-text="value.value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import EntityMixin from "./EntityMixin"
|
||||||
|
import EntityIcon from "./EntityIcon"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Sensor',
|
||||||
|
components: {EntityIcon},
|
||||||
|
mixins: [EntityMixin],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "common";
|
||||||
|
|
||||||
|
.sensor-container {
|
||||||
|
.head {
|
||||||
|
.value {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
margin-left: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -0,0 +1 @@
|
||||||
|
Sensor.vue
|
|
@ -1,9 +1,17 @@
|
||||||
{
|
{
|
||||||
"entity": {
|
"battery": {
|
||||||
"name": "Entity",
|
"name": "Battery",
|
||||||
"name_plural": "Entities",
|
"name_plural": "Batteries",
|
||||||
"icon": {
|
"icon": {
|
||||||
"class": "fas fa-circle-question"
|
"class": "fas fa-battery-full"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"current_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-bolt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -15,11 +23,35 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"switch": {
|
"dimmer": {
|
||||||
"name": "Switch",
|
"name": "Dimmer",
|
||||||
"name_plural": "Switches",
|
"name_plural": "Dimmers",
|
||||||
"icon": {
|
"icon": {
|
||||||
"class": "fas fa-toggle-on"
|
"class": "fas fa-gauge"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"energy_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-plug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"entity": {
|
||||||
|
"name": "Entity",
|
||||||
|
"name_plural": "Entities",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-circle-question"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"humidity_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-droplet"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -31,11 +63,75 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"dimmer": {
|
"link_quality": {
|
||||||
"name": "Dimmer",
|
"name": "Link Quality",
|
||||||
"name_plural": "Dimmers",
|
"name_plural": "Link Qualities",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-tower-broadcast"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"power_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-plug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"temperature_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-temperature-half"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"enum_switch": {
|
||||||
|
"name": "Switch",
|
||||||
|
"name_plural": "Switches",
|
||||||
"icon": {
|
"icon": {
|
||||||
"class": "fas fa-gauge"
|
"class": "fas fa-gauge"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"switch": {
|
||||||
|
"name": "Switch",
|
||||||
|
"name_plural": "Switches",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-toggle-on"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"voltage_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-car-battery"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"binary_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-thermometer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"numeric_sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-thermometer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"sensor": {
|
||||||
|
"name": "Sensor",
|
||||||
|
"name_plural": "Sensors",
|
||||||
|
"icon": {
|
||||||
|
"class": "fas fa-thermometer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,10 +246,12 @@ class ZigbeeMqttBackend(MqttBackend):
|
||||||
self.bus.post(ZigbeeMqttDeviceConnectedEvent(device=name, **event_args))
|
self.bus.post(ZigbeeMqttDeviceConnectedEvent(device=name, **event_args))
|
||||||
|
|
||||||
exposes = (device.get('definition', {}) or {}).get('exposes', [])
|
exposes = (device.get('definition', {}) or {}).get('exposes', [])
|
||||||
client.publish(
|
payload = self._plugin.build_device_get_request(exposes)
|
||||||
self.base_topic + '/' + name + '/get',
|
if payload:
|
||||||
json.dumps(self._plugin.build_device_get_request(exposes)),
|
client.publish(
|
||||||
)
|
self.base_topic + '/' + name + '/get',
|
||||||
|
json.dumps(payload),
|
||||||
|
)
|
||||||
|
|
||||||
devices_copy = [*self._devices.keys()]
|
devices_copy = [*self._devices.keys()]
|
||||||
for name in devices_copy:
|
for name in devices_copy:
|
||||||
|
|
|
@ -259,6 +259,9 @@ class EntitiesEngine(Thread):
|
||||||
session.add_all(entities)
|
session.add_all(entities)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
for e in entities:
|
||||||
|
session.expunge(e)
|
||||||
|
|
||||||
with self._entities_cache_lock:
|
with self._entities_cache_lock:
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
self._cache_entities(entity, overwrite_cache=True)
|
self._cache_entities(entity, overwrite_cache=True)
|
||||||
|
|
27
platypush/entities/batteries.py
Normal file
27
platypush/entities/batteries.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from .devices import entity_types_registry
|
||||||
|
from .sensors import NumericSensor
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('Battery'):
|
||||||
|
|
||||||
|
class Battery(NumericSensor):
|
||||||
|
__tablename__ = 'battery'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, unit: str = '%', min: float = 0, max: float = 100, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(*args, min=min, max=max, unit=unit, **kwargs)
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['Battery'] = Battery
|
||||||
|
else:
|
||||||
|
Battery = entity_types_registry['Battery']
|
76
platypush/entities/electricity.py
Normal file
76
platypush/entities/electricity.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from .devices import entity_types_registry
|
||||||
|
from .sensors import NumericSensor
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('PowerSensor'):
|
||||||
|
|
||||||
|
class PowerSensor(NumericSensor):
|
||||||
|
__tablename__ = 'power_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['PowerSensor'] = PowerSensor
|
||||||
|
else:
|
||||||
|
PowerSensor = entity_types_registry['PowerSensor']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('CurrentSensor'):
|
||||||
|
|
||||||
|
class CurrentSensor(NumericSensor):
|
||||||
|
__tablename__ = 'current_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['CurrentSensor'] = CurrentSensor
|
||||||
|
else:
|
||||||
|
CurrentSensor = entity_types_registry['CurrentSensor']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('VoltageSensor'):
|
||||||
|
|
||||||
|
class VoltageSensor(NumericSensor):
|
||||||
|
__tablename__ = 'voltage_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['VoltageSensor'] = VoltageSensor
|
||||||
|
else:
|
||||||
|
VoltageSensor = entity_types_registry['VoltageSensor']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('EnergySensor'):
|
||||||
|
|
||||||
|
class EnergySensor(NumericSensor):
|
||||||
|
__tablename__ = 'energy_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['EnergySensor'] = EnergySensor
|
||||||
|
else:
|
||||||
|
EnergySensor = entity_types_registry['EnergySensor']
|
22
platypush/entities/humidity.py
Normal file
22
platypush/entities/humidity.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from .devices import entity_types_registry
|
||||||
|
from .sensors import NumericSensor
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('HumiditySensor'):
|
||||||
|
|
||||||
|
class HumiditySensor(NumericSensor):
|
||||||
|
__tablename__ = 'humidity_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['HumiditySensor'] = HumiditySensor
|
||||||
|
else:
|
||||||
|
HumiditySensor = entity_types_registry['HumiditySensor']
|
27
platypush/entities/linkquality.py
Normal file
27
platypush/entities/linkquality.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from .devices import entity_types_registry
|
||||||
|
from .sensors import NumericSensor
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('LinkQuality'):
|
||||||
|
|
||||||
|
class LinkQuality(NumericSensor):
|
||||||
|
__tablename__ = 'link_quality'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *args, unit: str = '%', min: float = 0, max: float = 100, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(*args, min=min, max=max, unit=unit, **kwargs)
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['LinkQuality'] = LinkQuality
|
||||||
|
else:
|
||||||
|
LinkQuality = entity_types_registry['LinkQuality']
|
85
platypush/entities/sensors.py
Normal file
85
platypush/entities/sensors.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey, Boolean, Numeric, String
|
||||||
|
|
||||||
|
from .devices import Device, entity_types_registry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Sensor(Device):
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('RawSensor'):
|
||||||
|
|
||||||
|
class RawSensor(Sensor):
|
||||||
|
__tablename__ = 'raw_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
value = Column(String)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['RawSensor'] = RawSensor
|
||||||
|
else:
|
||||||
|
RawSensor = entity_types_registry['RawSensor']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('NumericSensor'):
|
||||||
|
|
||||||
|
class NumericSensor(Sensor):
|
||||||
|
__tablename__ = 'numeric_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
value = Column(Numeric)
|
||||||
|
min = Column(Numeric)
|
||||||
|
max = Column(Numeric)
|
||||||
|
unit = Column(String)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['NumericSensor'] = NumericSensor
|
||||||
|
else:
|
||||||
|
NumericSensor = entity_types_registry['NumericSensor']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('BinarySensor'):
|
||||||
|
|
||||||
|
class BinarySensor(Sensor):
|
||||||
|
__tablename__ = 'binary_sensor'
|
||||||
|
|
||||||
|
def __init__(self, *args, value=None, **kwargs):
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.lower()
|
||||||
|
|
||||||
|
if value in {True, 1, '1', 't', 'true', 'on'}:
|
||||||
|
value = True
|
||||||
|
elif value in {False, 0, '0', 'f', 'false', 'off'}:
|
||||||
|
value = False
|
||||||
|
elif value is not None:
|
||||||
|
logger.warning(f'Unsupported value for BinarySensor type: {value}')
|
||||||
|
value = None
|
||||||
|
|
||||||
|
super().__init__(*args, value=value, **kwargs)
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
value = Column(Boolean)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['BinarySensor'] = BinarySensor
|
||||||
|
else:
|
||||||
|
BinarySensor = entity_types_registry['BinarySensor']
|
|
@ -1,4 +1,4 @@
|
||||||
from sqlalchemy import Column, Integer, ForeignKey, Boolean
|
from sqlalchemy import Column, Integer, ForeignKey, Boolean, String, JSON
|
||||||
|
|
||||||
from .devices import Device, entity_types_registry
|
from .devices import Device, entity_types_registry
|
||||||
|
|
||||||
|
@ -20,3 +20,23 @@ if not entity_types_registry.get('Switch'):
|
||||||
entity_types_registry['Switch'] = Switch
|
entity_types_registry['Switch'] = Switch
|
||||||
else:
|
else:
|
||||||
Switch = entity_types_registry['Switch']
|
Switch = entity_types_registry['Switch']
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('EnumSwitch'):
|
||||||
|
|
||||||
|
class EnumSwitch(Device):
|
||||||
|
__tablename__ = 'enum_switch'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
value = Column(String)
|
||||||
|
values = Column(JSON)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['EnumSwitch'] = EnumSwitch
|
||||||
|
else:
|
||||||
|
EnumSwitch = entity_types_registry['EnumSwitch']
|
||||||
|
|
22
platypush/entities/temperature.py
Normal file
22
platypush/entities/temperature.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from sqlalchemy import Column, Integer, ForeignKey
|
||||||
|
|
||||||
|
from .devices import entity_types_registry
|
||||||
|
from .sensors import NumericSensor
|
||||||
|
|
||||||
|
|
||||||
|
if not entity_types_registry.get('TemperatureSensor'):
|
||||||
|
|
||||||
|
class TemperatureSensor(NumericSensor):
|
||||||
|
__tablename__ = 'temperature_sensor'
|
||||||
|
|
||||||
|
id = Column(
|
||||||
|
Integer, ForeignKey(NumericSensor.id, ondelete='CASCADE'), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
__mapper_args__ = {
|
||||||
|
'polymorphic_identity': __tablename__,
|
||||||
|
}
|
||||||
|
|
||||||
|
entity_types_registry['TemperatureSensor'] = TemperatureSensor
|
||||||
|
else:
|
||||||
|
TemperatureSensor = entity_types_registry['TemperatureSensor']
|
|
@ -1,4 +1,5 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import decimal
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -19,7 +20,7 @@ class JSONAble(ABC):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class Message(object):
|
class Message:
|
||||||
"""
|
"""
|
||||||
Message generic class
|
Message generic class
|
||||||
"""
|
"""
|
||||||
|
@ -38,6 +39,8 @@ class Message(object):
|
||||||
return int(obj)
|
return int(obj)
|
||||||
if isinstance(obj, np.ndarray):
|
if isinstance(obj, np.ndarray):
|
||||||
return obj.tolist()
|
return obj.tolist()
|
||||||
|
if isinstance(obj, decimal.Decimal):
|
||||||
|
return float(obj)
|
||||||
if callable(obj):
|
if callable(obj):
|
||||||
return '<function at {}.{}>'.format(obj.__module__, obj.__name__)
|
return '<function at {}.{}>'.format(obj.__module__, obj.__name__)
|
||||||
|
|
||||||
|
@ -45,9 +48,7 @@ class Message(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_datetime(obj):
|
def parse_datetime(obj):
|
||||||
if isinstance(obj, datetime.datetime) or \
|
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
|
||||||
isinstance(obj, datetime.date) or \
|
|
||||||
isinstance(obj, datetime.time):
|
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
|
@ -68,8 +69,11 @@ class Message(object):
|
||||||
try:
|
try:
|
||||||
return super().default(obj)
|
return super().default(obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('Could not serialize object type {}: {}: {}'.format(
|
logger.warning(
|
||||||
type(obj), str(e), obj))
|
'Could not serialize object type {}: {}: {}'.format(
|
||||||
|
type(obj), str(e), obj
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, timestamp=None, *_, **__):
|
def __init__(self, timestamp=None, *_, **__):
|
||||||
self.timestamp = timestamp or time.time()
|
self.timestamp = timestamp or time.time()
|
||||||
|
@ -80,12 +84,15 @@ class Message(object):
|
||||||
the message into a UTF-8 JSON string
|
the message into a UTF-8 JSON string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps(
|
||||||
attr: getattr(self, attr)
|
{
|
||||||
for attr in self.__dir__()
|
attr: getattr(self, attr)
|
||||||
if (attr != '_timestamp' or not attr.startswith('_'))
|
for attr in self.__dir__()
|
||||||
and not inspect.ismethod(getattr(self, attr))
|
if (attr != '_timestamp' or not attr.startswith('_'))
|
||||||
}, cls=self.Encoder).replace('\n', ' ')
|
and not inspect.ismethod(getattr(self, attr))
|
||||||
|
},
|
||||||
|
cls=self.Encoder,
|
||||||
|
).replace('\n', ' ')
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -105,7 +112,7 @@ class Message(object):
|
||||||
|
|
||||||
if isinstance(msg, cls):
|
if isinstance(msg, cls):
|
||||||
msg = str(msg)
|
msg = str(msg)
|
||||||
if isinstance(msg, bytes) or isinstance(msg, bytearray):
|
if isinstance(msg, (bytes, bytearray)):
|
||||||
msg = msg.decode('utf-8')
|
msg = msg.decode('utf-8')
|
||||||
if isinstance(msg, str):
|
if isinstance(msg, str):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,18 +1,30 @@
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Optional, List, Any, Dict, Union
|
from typing import Optional, List, Any, Dict, Union, Tuple
|
||||||
|
|
||||||
from platypush.entities import manages
|
from platypush.entities import manages
|
||||||
|
from platypush.entities.batteries import Battery
|
||||||
|
from platypush.entities.electricity import (
|
||||||
|
CurrentSensor,
|
||||||
|
EnergySensor,
|
||||||
|
PowerSensor,
|
||||||
|
VoltageSensor,
|
||||||
|
)
|
||||||
|
from platypush.entities.humidity import HumiditySensor
|
||||||
from platypush.entities.lights import Light
|
from platypush.entities.lights import Light
|
||||||
from platypush.entities.switches import Switch
|
from platypush.entities.linkquality import LinkQuality
|
||||||
|
from platypush.entities.sensors import Sensor, BinarySensor, NumericSensor
|
||||||
|
from platypush.entities.switches import Switch, EnumSwitch
|
||||||
|
from platypush.entities.temperature import TemperatureSensor
|
||||||
from platypush.message import Mapping
|
from platypush.message import Mapping
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
from platypush.plugins.mqtt import MqttPlugin, action
|
from platypush.plugins.mqtt import MqttPlugin, action
|
||||||
|
|
||||||
|
|
||||||
@manages(Light, Switch)
|
@manages(Light, Switch, LinkQuality, Battery, Sensor)
|
||||||
class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
"""
|
"""
|
||||||
This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and
|
This plugin allows you to interact with Zigbee devices over MQTT through any Zigbee sniffer and
|
||||||
|
@ -161,14 +173,11 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
}
|
}
|
||||||
|
|
||||||
def transform_entities(self, devices):
|
def transform_entities(self, devices):
|
||||||
from platypush.entities.switches import Switch
|
|
||||||
|
|
||||||
compatible_entities = []
|
compatible_entities = []
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
if not dev:
|
if not dev:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
converted_entity = None
|
|
||||||
dev_def = dev.get("definition") or {}
|
dev_def = dev.get("definition") or {}
|
||||||
dev_info = {
|
dev_info = {
|
||||||
"type": dev.get("type"),
|
"type": dev.get("type"),
|
||||||
|
@ -185,68 +194,80 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
|
|
||||||
light_info = self._get_light_meta(dev)
|
light_info = self._get_light_meta(dev)
|
||||||
switch_info = self._get_switch_meta(dev)
|
switch_info = self._get_switch_meta(dev)
|
||||||
|
sensors = self._get_sensors(dev)
|
||||||
|
enum_switches = self._get_enum_switches(dev)
|
||||||
|
|
||||||
if light_info:
|
if light_info:
|
||||||
converted_entity = Light(
|
compatible_entities.append(
|
||||||
id=dev['ieee_address'],
|
Light(
|
||||||
name=dev.get('friendly_name'),
|
id=f'{dev["ieee_address"]}:light',
|
||||||
on=dev.get('state', {}).get('state') == switch_info.get('value_on'),
|
name=dev.get('friendly_name'),
|
||||||
brightness_min=light_info.get('brightness_min'),
|
on=dev.get('state', {}).get('state')
|
||||||
brightness_max=light_info.get('brightness_max'),
|
== switch_info.get('value_on'),
|
||||||
temperature_min=light_info.get('temperature_min'),
|
brightness_min=light_info.get('brightness_min'),
|
||||||
temperature_max=light_info.get('temperature_max'),
|
brightness_max=light_info.get('brightness_max'),
|
||||||
hue_min=light_info.get('hue_min'),
|
temperature_min=light_info.get('temperature_min'),
|
||||||
hue_max=light_info.get('hue_max'),
|
temperature_max=light_info.get('temperature_max'),
|
||||||
saturation_min=light_info.get('saturation_min'),
|
hue_min=light_info.get('hue_min'),
|
||||||
saturation_max=light_info.get('saturation_max'),
|
hue_max=light_info.get('hue_max'),
|
||||||
brightness=(
|
saturation_min=light_info.get('saturation_min'),
|
||||||
dev.get('state', {})
|
saturation_max=light_info.get('saturation_max'),
|
||||||
.get('color', {})
|
brightness=(
|
||||||
.get(light_info.get('brightness_name', 'brightness'))
|
dev.get('state', {}).get(
|
||||||
),
|
light_info.get('brightness_name', 'brightness')
|
||||||
temperature=(
|
)
|
||||||
dev.get('state', {})
|
),
|
||||||
.get('color', {})
|
temperature=(
|
||||||
.get(light_info.get('temperature_name', 'temperature'))
|
dev.get('state', {}).get(
|
||||||
),
|
light_info.get('temperature_name', 'temperature')
|
||||||
hue=(
|
)
|
||||||
dev.get('state', {})
|
),
|
||||||
.get('color', {})
|
hue=(
|
||||||
.get(light_info.get('hue_name', 'hue'))
|
dev.get('state', {})
|
||||||
),
|
.get('color', {})
|
||||||
saturation=(
|
.get(light_info.get('hue_name', 'hue'))
|
||||||
dev.get('state', {})
|
),
|
||||||
.get('color', {})
|
saturation=(
|
||||||
.get(light_info.get('saturation_name', 'saturation'))
|
dev.get('state', {})
|
||||||
),
|
.get('color', {})
|
||||||
x=(
|
.get(light_info.get('saturation_name', 'saturation'))
|
||||||
dev.get('state', {})
|
),
|
||||||
.get('color', {})
|
x=(
|
||||||
.get(light_info.get('x_name', 'x'))
|
dev.get('state', {})
|
||||||
),
|
.get('color', {})
|
||||||
y=(
|
.get(light_info.get('x_name', 'x'))
|
||||||
dev.get('state', {})
|
),
|
||||||
.get('color', {})
|
y=(
|
||||||
.get(light_info.get('y_name', 'y'))
|
dev.get('state', {})
|
||||||
),
|
.get('color', {})
|
||||||
description=dev_def.get('description'),
|
.get(light_info.get('y_name', 'y'))
|
||||||
data=dev_info,
|
),
|
||||||
|
description=dev_def.get('description'),
|
||||||
|
data=dev_info,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif switch_info and dev.get('state', {}).get('state') is not None:
|
elif switch_info and dev.get('state', {}).get('state') is not None:
|
||||||
converted_entity = Switch(
|
compatible_entities.append(
|
||||||
id=dev['ieee_address'],
|
Switch(
|
||||||
name=dev.get('friendly_name'),
|
id=f'{dev["ieee_address"]}:switch',
|
||||||
state=dev.get('state', {}).get('state') == switch_info['value_on'],
|
name=dev.get('friendly_name'),
|
||||||
description=dev_def.get("description"),
|
state=dev.get('state', {}).get('state')
|
||||||
data=dev_info,
|
== switch_info['value_on'],
|
||||||
|
description=dev_def.get("description"),
|
||||||
|
data=dev_info,
|
||||||
|
is_read_only=switch_info['is_read_only'],
|
||||||
|
is_write_only=switch_info['is_write_only'],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if converted_entity:
|
if sensors:
|
||||||
compatible_entities.append(converted_entity)
|
compatible_entities += sensors
|
||||||
|
if enum_switches:
|
||||||
|
compatible_entities += enum_switches
|
||||||
|
|
||||||
return super().transform_entities(compatible_entities) # type: ignore
|
return super().transform_entities(compatible_entities) # type: ignore
|
||||||
|
|
||||||
def _get_network_info(self, **kwargs):
|
def _get_network_info(self, **kwargs) -> dict:
|
||||||
self.logger.info('Fetching Zigbee network information')
|
self.logger.info('Fetching Zigbee network information')
|
||||||
client = None
|
client = None
|
||||||
mqtt_args = self._mqtt_args(**kwargs)
|
mqtt_args = self._mqtt_args(**kwargs)
|
||||||
|
@ -308,28 +329,30 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.info('Zigbee network configuration updated')
|
self.logger.info('Zigbee network configuration updated')
|
||||||
return info
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
client.loop_stop()
|
if client:
|
||||||
client.disconnect()
|
client.loop_stop()
|
||||||
|
client.disconnect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Error on MQTT client disconnection: {}'.format(str(e))
|
'Error on MQTT client disconnection: {}'.format(str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
def _topic(self, topic):
|
def _topic(self, topic):
|
||||||
return self.base_topic + '/' + topic
|
return self.base_topic + '/' + topic
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_response(response: Union[dict, Response]) -> dict:
|
def _parse_response(response: Union[dict, Response]) -> dict:
|
||||||
if isinstance(response, Response):
|
if isinstance(response, Response):
|
||||||
response = response.output
|
response = response.output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
assert response.get('status') != 'error', response.get(
|
assert response.get('status') != 'error', response.get( # type: ignore[reportGeneralTypeIssues]
|
||||||
'error', 'zigbee2mqtt error'
|
'error', 'zigbee2mqtt error'
|
||||||
)
|
)
|
||||||
return response
|
return response # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def devices(self, **kwargs) -> List[Dict[str, Any]]:
|
def devices(self, **kwargs) -> List[Dict[str, Any]]:
|
||||||
|
@ -503,7 +526,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
]
|
]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._get_network_info(**kwargs).get('devices')
|
return self._get_network_info(**kwargs).get('devices', {})
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def permit_join(
|
def permit_join(
|
||||||
|
@ -520,12 +543,13 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
"""
|
"""
|
||||||
if timeout:
|
if timeout:
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/permit_join'),
|
topic=self._topic('bridge/request/permit_join'),
|
||||||
msg={'value': permit, 'time': timeout},
|
msg={'value': permit, 'time': timeout},
|
||||||
reply_topic=self._topic('bridge/response/permit_join'),
|
reply_topic=self._topic('bridge/response/permit_join'),
|
||||||
**self._mqtt_args(**kwargs),
|
**self._mqtt_args(**kwargs),
|
||||||
)
|
)
|
||||||
|
or {}
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.publish(
|
return self.publish(
|
||||||
|
@ -560,7 +584,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/config/log_level'),
|
topic=self._topic('bridge/request/config/log_level'),
|
||||||
msg={'value': level},
|
msg={'value': level},
|
||||||
reply_topic=self._topic('bridge/response/config/log_level'),
|
reply_topic=self._topic('bridge/response/config/log_level'),
|
||||||
|
@ -573,14 +597,14 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
"""
|
"""
|
||||||
Change the options of a device. Options can only be changed, not added or deleted.
|
Change the options of a device. Options can only be changed, not added or deleted.
|
||||||
|
|
||||||
:param device: Display name of the device.
|
:param device: Display name or IEEE address of the device.
|
||||||
:param option: Option name.
|
:param option: Option name.
|
||||||
:param value: New value.
|
:param value: New value.
|
||||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/options'),
|
topic=self._topic('bridge/request/device/options'),
|
||||||
reply_topic=self._topic('bridge/response/device/options'),
|
reply_topic=self._topic('bridge/response/device/options'),
|
||||||
msg={
|
msg={
|
||||||
|
@ -606,7 +630,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/remove'),
|
topic=self._topic('bridge/request/device/remove'),
|
||||||
msg={'id': device, 'force': force},
|
msg={'id': device, 'force': force},
|
||||||
reply_topic=self._topic('bridge/response/device/remove'),
|
reply_topic=self._topic('bridge/response/device/remove'),
|
||||||
|
@ -624,7 +648,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/ban'),
|
topic=self._topic('bridge/request/device/ban'),
|
||||||
reply_topic=self._topic('bridge/response/device/ban'),
|
reply_topic=self._topic('bridge/response/device/ban'),
|
||||||
msg={'id': device},
|
msg={'id': device},
|
||||||
|
@ -643,7 +667,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/whitelist'),
|
topic=self._topic('bridge/request/device/whitelist'),
|
||||||
reply_topic=self._topic('bridge/response/device/whitelist'),
|
reply_topic=self._topic('bridge/response/device/whitelist'),
|
||||||
msg={'id': device},
|
msg={'id': device},
|
||||||
|
@ -666,8 +690,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
self.logger.info('Old and new name are the same: nothing to do')
|
self.logger.info('Old and new name are the same: nothing to do')
|
||||||
return
|
return
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
||||||
devices = self.devices().output
|
|
||||||
assert not [
|
assert not [
|
||||||
dev for dev in devices if dev.get('friendly_name') == name
|
dev for dev in devices if dev.get('friendly_name') == name
|
||||||
], 'A device named {} already exists on the network'.format(name)
|
], 'A device named {} already exists on the network'.format(name)
|
||||||
|
@ -684,7 +707,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
}
|
}
|
||||||
|
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/rename'),
|
topic=self._topic('bridge/request/device/rename'),
|
||||||
msg=req,
|
msg=req,
|
||||||
reply_topic=self._topic('bridge/response/device/rename'),
|
reply_topic=self._topic('bridge/response/device/rename'),
|
||||||
|
@ -694,9 +717,16 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_device_get_request(values: List[Dict[str, Any]]) -> dict:
|
def build_device_get_request(values: List[Dict[str, Any]]) -> dict:
|
||||||
def extract_value(value: dict, root: dict):
|
def extract_value(value: dict, root: dict, depth: int = 0):
|
||||||
if not value.get('access', 1) & 0x1:
|
for feature in value.get('features', []):
|
||||||
# Property not readable
|
new_root = root
|
||||||
|
if depth > 0:
|
||||||
|
new_root = root[value['property']] = root.get(value['property'], {})
|
||||||
|
|
||||||
|
extract_value(feature, new_root, depth=depth + 1)
|
||||||
|
|
||||||
|
if not value.get('access', 1) & 0x4:
|
||||||
|
# Property not readable/query-able
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'features' not in value:
|
if 'features' not in value:
|
||||||
|
@ -708,9 +738,6 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
root[value['property']] = root.get(value['property'], {})
|
root[value['property']] = root.get(value['property'], {})
|
||||||
root = root[value['property']]
|
root = root[value['property']]
|
||||||
|
|
||||||
for feature in value['features']:
|
|
||||||
extract_value(feature, root)
|
|
||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
for value in values:
|
for value in values:
|
||||||
extract_value(value, root=ret)
|
extract_value(value, root=ret)
|
||||||
|
@ -722,7 +749,6 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
device, self._info['devices_by_addr'].get(device, {})
|
device, self._info['devices_by_addr'].get(device, {})
|
||||||
)
|
)
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
@action
|
@action
|
||||||
def device_get(
|
def device_get(
|
||||||
self, device: str, property: Optional[str] = None, **kwargs
|
self, device: str, property: Optional[str] = None, **kwargs
|
||||||
|
@ -741,7 +767,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
kwargs = self._mqtt_args(**kwargs)
|
kwargs = self._mqtt_args(**kwargs)
|
||||||
device_info = self._get_device_info(device)
|
device_info = self._get_device_info(device)
|
||||||
if device_info:
|
if device_info:
|
||||||
device = device_info.get('friendly_name') or device_info['ieee_address']
|
device = device_info.get('friendly_name') or self._ieee_address(device_info)
|
||||||
|
|
||||||
if property:
|
if property:
|
||||||
properties = self.publish(
|
properties = self.publish(
|
||||||
|
@ -749,7 +775,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
reply_topic=self._topic(device),
|
reply_topic=self._topic(device),
|
||||||
msg={property: ''},
|
msg={property: ''},
|
||||||
**kwargs,
|
**kwargs,
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
assert property in properties, f'No such property: {property}'
|
assert property in properties, f'No such property: {property}'
|
||||||
return {property: properties[property]}
|
return {property: properties[property]}
|
||||||
|
@ -774,7 +800,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
reply_topic=self._topic(device),
|
reply_topic=self._topic(device),
|
||||||
msg=self.build_device_get_request(exposes),
|
msg=self.build_device_get_request(exposes),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
if device_info:
|
if device_info:
|
||||||
self.publish_entities( # type: ignore
|
self.publish_entities( # type: ignore
|
||||||
|
@ -816,13 +842,15 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
kwargs = self._mqtt_args(**kwargs)
|
kwargs = self._mqtt_args(**kwargs)
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
devices = {
|
devices = list(
|
||||||
device['friendly_name'] or device['ieee_address']
|
{
|
||||||
for device in self.devices(**kwargs).output
|
device['friendly_name'] or device['ieee_address']
|
||||||
}
|
for device in self.devices(**kwargs).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def worker(device: str, q: Queue):
|
def worker(device: str, q: Queue):
|
||||||
q.put(self.device_get(device, **kwargs).output)
|
q.put(self.device_get(device, **kwargs).output) # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
queues = {}
|
queues = {}
|
||||||
workers = {}
|
workers = {}
|
||||||
|
@ -857,7 +885,6 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
"""
|
"""
|
||||||
return self.devices_get([device] if device else None, *args, **kwargs)
|
return self.devices_get([device] if device else None, *args, **kwargs)
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins,DuplicatedCode
|
|
||||||
@action
|
@action
|
||||||
def device_set(
|
def device_set(
|
||||||
self,
|
self,
|
||||||
|
@ -880,22 +907,63 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
msg = (values or {}).copy()
|
msg = (values or {}).copy()
|
||||||
|
reply_topic = self._topic(device)
|
||||||
|
|
||||||
if property:
|
if property:
|
||||||
msg[property] = value
|
msg[property] = value
|
||||||
|
stored_property = next(
|
||||||
|
iter(
|
||||||
|
exposed
|
||||||
|
for exposed in (
|
||||||
|
self._info.get('devices_by_addr', {})
|
||||||
|
.get(device, {})
|
||||||
|
.get('definition', {})
|
||||||
|
.get('exposes', {})
|
||||||
|
)
|
||||||
|
if exposed.get('property') == property
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if stored_property and self._is_write_only(stored_property):
|
||||||
|
# Don't wait for an update from a value that is not readable
|
||||||
|
reply_topic = None
|
||||||
|
|
||||||
properties = self.publish(
|
properties = self.publish(
|
||||||
topic=self._topic(device + '/set'),
|
topic=self._topic(device + '/set'),
|
||||||
reply_topic=self._topic(device),
|
reply_topic=reply_topic,
|
||||||
msg=msg,
|
msg=msg,
|
||||||
**self._mqtt_args(**kwargs),
|
**self._mqtt_args(**kwargs),
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
if property:
|
if property and reply_topic:
|
||||||
assert property in properties, 'No such property: ' + property
|
assert property in properties, 'No such property: ' + property
|
||||||
return {property: properties[property]}
|
return {property: properties[property]}
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_value(
|
||||||
|
self, device: str, property: Optional[str] = None, data=None, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Entity-compatible way of setting a value on a node.
|
||||||
|
|
||||||
|
:param device: Device friendly name, IEEE address or internal entity ID
|
||||||
|
in ``<address>:<property>`` format.
|
||||||
|
:param property: Name of the property to set. If not specified here, it
|
||||||
|
should be specified on ``device`` in ``<address>:<property>``
|
||||||
|
format.
|
||||||
|
:param kwargs: Extra arguments to be passed to
|
||||||
|
:meth:`platypush.plugins.mqtt.MqttPlugin.publish`` (default: query
|
||||||
|
the default configured device).
|
||||||
|
"""
|
||||||
|
dev, prop = self._ieee_address(device, with_property=True)
|
||||||
|
if not property:
|
||||||
|
property = prop
|
||||||
|
|
||||||
|
self.device_set(dev, property, data, **kwargs)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def device_check_ota_updates(self, device: str, **kwargs) -> dict:
|
def device_check_ota_updates(self, device: str, **kwargs) -> dict:
|
||||||
"""
|
"""
|
||||||
|
@ -917,7 +985,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ret = self._parse_response(
|
ret = self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/ota_update/check'),
|
topic=self._topic('bridge/request/device/ota_update/check'),
|
||||||
reply_topic=self._topic('bridge/response/device/ota_update/check'),
|
reply_topic=self._topic('bridge/response/device/ota_update/check'),
|
||||||
msg={'id': device},
|
msg={'id': device},
|
||||||
|
@ -941,7 +1009,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/ota_update/update'),
|
topic=self._topic('bridge/request/device/ota_update/update'),
|
||||||
reply_topic=self._topic('bridge/response/device/ota_update/update'),
|
reply_topic=self._topic('bridge/response/device/ota_update/update'),
|
||||||
msg={'id': device},
|
msg={'id': device},
|
||||||
|
@ -957,7 +1025,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._get_network_info(**kwargs).get('groups')
|
return self._get_network_info(**kwargs).get('groups', [])
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def info(self, **kwargs) -> dict:
|
def info(self, **kwargs) -> dict:
|
||||||
|
@ -1113,7 +1181,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/group/add'),
|
topic=self._topic('bridge/request/group/add'),
|
||||||
reply_topic=self._topic('bridge/response/group/add'),
|
reply_topic=self._topic('bridge/response/group/add'),
|
||||||
msg=payload,
|
msg=payload,
|
||||||
|
@ -1142,7 +1210,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
reply_topic=self._topic(group),
|
reply_topic=self._topic(group),
|
||||||
msg=msg,
|
msg=msg,
|
||||||
**self._mqtt_args(**kwargs),
|
**self._mqtt_args(**kwargs),
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
if property:
|
if property:
|
||||||
assert property in properties, 'No such property: ' + property
|
assert property in properties, 'No such property: ' + property
|
||||||
|
@ -1169,7 +1237,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
reply_topic=self._topic(group),
|
reply_topic=self._topic(group),
|
||||||
msg={property: value},
|
msg={property: value},
|
||||||
**self._mqtt_args(**kwargs),
|
**self._mqtt_args(**kwargs),
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
|
||||||
if property:
|
if property:
|
||||||
assert property in properties, 'No such property: ' + property
|
assert property in properties, 'No such property: ' + property
|
||||||
|
@ -1191,14 +1259,17 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
self.logger.info('Old and new name are the same: nothing to do')
|
self.logger.info('Old and new name are the same: nothing to do')
|
||||||
return
|
return
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
groups = {
|
||||||
groups = {group.get('friendly_name'): group for group in self.groups().output}
|
group.get('friendly_name'): group
|
||||||
|
for group in self.groups().output # type: ignore[reportGeneralTypeIssues]
|
||||||
|
}
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
name not in groups
|
name not in groups
|
||||||
), 'A group named {} already exists on the network'.format(name)
|
), 'A group named {} already exists on the network'.format(name)
|
||||||
|
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/group/rename'),
|
topic=self._topic('bridge/request/group/rename'),
|
||||||
reply_topic=self._topic('bridge/response/group/rename'),
|
reply_topic=self._topic('bridge/response/group/rename'),
|
||||||
msg={'from': group, 'to': name} if group else name,
|
msg={'from': group, 'to': name} if group else name,
|
||||||
|
@ -1216,7 +1287,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/group/remove'),
|
topic=self._topic('bridge/request/group/remove'),
|
||||||
reply_topic=self._topic('bridge/response/group/remove'),
|
reply_topic=self._topic('bridge/response/group/remove'),
|
||||||
msg=name,
|
msg=name,
|
||||||
|
@ -1235,7 +1306,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/group/members/add'),
|
topic=self._topic('bridge/request/group/members/add'),
|
||||||
reply_topic=self._topic('bridge/response/group/members/add'),
|
reply_topic=self._topic('bridge/response/group/members/add'),
|
||||||
msg={
|
msg={
|
||||||
|
@ -1258,7 +1329,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic(
|
topic=self._topic(
|
||||||
'bridge/request/group/members/remove{}'.format(
|
'bridge/request/group/members/remove{}'.format(
|
||||||
'_all' if device is None else ''
|
'_all' if device is None else ''
|
||||||
|
@ -1294,7 +1365,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/bind'),
|
topic=self._topic('bridge/request/device/bind'),
|
||||||
reply_topic=self._topic('bridge/response/device/bind'),
|
reply_topic=self._topic('bridge/response/device/bind'),
|
||||||
msg={'from': source, 'to': target},
|
msg={'from': source, 'to': target},
|
||||||
|
@ -1315,7 +1386,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
return self._parse_response(
|
return self._parse_response(
|
||||||
self.publish(
|
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||||
topic=self._topic('bridge/request/device/unbind'),
|
topic=self._topic('bridge/request/device/unbind'),
|
||||||
reply_topic=self._topic('bridge/response/device/unbind'),
|
reply_topic=self._topic('bridge/response/device/unbind'),
|
||||||
msg={'from': source, 'to': target},
|
msg={'from': source, 'to': target},
|
||||||
|
@ -1324,54 +1395,55 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def on(self, device, *args, **kwargs) -> dict:
|
def on(self, device, *_, **__) -> dict:
|
||||||
"""
|
"""
|
||||||
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.on` and turns on a Zigbee device with a writable
|
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.on` and turns on a Zigbee device with a writable
|
||||||
binary property.
|
binary property.
|
||||||
"""
|
"""
|
||||||
switch_info = self._get_switch_info(device)
|
switch_info = self._get_switch_info(device)
|
||||||
assert switch_info, '{} is not a valid switch'.format(device)
|
assert switch_info, '{} is not a valid switch'.format(device)
|
||||||
device = switch_info.get('friendly_name') or switch_info['ieee_address']
|
device = switch_info.get('friendly_name') or self._ieee_address(switch_info)
|
||||||
props = self.device_set(
|
props = self.device_set(
|
||||||
device, switch_info['property'], switch_info['value_on']
|
device, switch_info['property'], switch_info['value_on']
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
return self._properties_to_switch(
|
return self._properties_to_switch(
|
||||||
device=device, props=props, switch_info=switch_info
|
device=device, props=props, switch_info=switch_info
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def off(self, device, *args, **kwargs) -> dict:
|
def off(self, device, *_, **__) -> dict:
|
||||||
"""
|
"""
|
||||||
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.off` and turns off a Zigbee device with a
|
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.off` and turns off a Zigbee device with a
|
||||||
writable binary property.
|
writable binary property.
|
||||||
"""
|
"""
|
||||||
switch_info = self._get_switch_info(device)
|
switch_info = self._get_switch_info(device)
|
||||||
assert switch_info, '{} is not a valid switch'.format(device)
|
assert switch_info, '{} is not a valid switch'.format(device)
|
||||||
device = switch_info.get('friendly_name') or switch_info['ieee_address']
|
device = switch_info.get('friendly_name') or self._ieee_address(switch_info)
|
||||||
props = self.device_set(
|
props = self.device_set(
|
||||||
device, switch_info['property'], switch_info['value_off']
|
device, switch_info['property'], switch_info['value_off']
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
return self._properties_to_switch(
|
return self._properties_to_switch(
|
||||||
device=device, props=props, switch_info=switch_info
|
device=device, props=props, switch_info=switch_info
|
||||||
)
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def toggle(self, device, *args, **kwargs) -> dict:
|
def toggle(self, device, *_, **__) -> dict:
|
||||||
"""
|
"""
|
||||||
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.toggle` and toggles a Zigbee device with a
|
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.toggle`
|
||||||
writable binary property.
|
and toggles a Zigbee device with a writable binary property.
|
||||||
"""
|
"""
|
||||||
switch_info = self._get_switch_info(device)
|
switch_info = self._get_switch_info(device)
|
||||||
assert switch_info, '{} is not a valid switch'.format(device)
|
assert switch_info, '{} is not a valid switch'.format(device)
|
||||||
device = switch_info.get('friendly_name') or switch_info['ieee_address']
|
device = switch_info.get('friendly_name') or self._ieee_address(switch_info)
|
||||||
props = self.device_set(
|
props = self.device_set(
|
||||||
device, switch_info['property'], switch_info['value_toggle']
|
device, switch_info['property'], switch_info['value_toggle']
|
||||||
).output
|
).output # type: ignore[reportGeneralTypeIssues]
|
||||||
return self._properties_to_switch(
|
return self._properties_to_switch(
|
||||||
device=device, props=props, switch_info=switch_info
|
device=device, props=props, switch_info=switch_info
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_switch_info(self, device: str):
|
def _get_switch_info(self, device: str):
|
||||||
|
device = self._ieee_address(device)
|
||||||
switches_info = self._get_switches_info()
|
switches_info = self._get_switches_info()
|
||||||
info = switches_info.get(device)
|
info = switches_info.get(device)
|
||||||
if info:
|
if info:
|
||||||
|
@ -1392,7 +1464,44 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_switch_meta(device_info: dict) -> dict:
|
def _is_read_only(feature: dict) -> bool:
|
||||||
|
return bool(feature.get('access', 0) & 2) == 0 and (
|
||||||
|
bool(feature.get('access', 0) & 1) == 1
|
||||||
|
or bool(feature.get('access', 0) & 4) == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_write_only(feature: dict) -> bool:
|
||||||
|
return bool(feature.get('access', 0) & 2) == 1 and (
|
||||||
|
bool(feature.get('access', 0) & 1) == 0
|
||||||
|
or bool(feature.get('access', 0) & 4) == 0
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ieee_address(
|
||||||
|
device: Union[dict, str], with_property=False
|
||||||
|
) -> Union[str, Tuple[str, Optional[str]]]:
|
||||||
|
# Entity value IDs are stored in the `<address>:<property>`
|
||||||
|
# format. Therefore, we need to split by `:` if we want to
|
||||||
|
# retrieve the original address.
|
||||||
|
if isinstance(device, dict):
|
||||||
|
dev = device['ieee_address']
|
||||||
|
else:
|
||||||
|
dev = device
|
||||||
|
|
||||||
|
# IEEE address + property format
|
||||||
|
if re.search(r'^0x[0-9a-fA-F]{16}:', dev):
|
||||||
|
parts = dev.split(':')
|
||||||
|
return (
|
||||||
|
(parts[0], parts[1] if len(parts) > 1 else None)
|
||||||
|
if with_property
|
||||||
|
else parts[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (dev, None) if with_property else dev
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_switch_meta(cls, device_info: dict) -> dict:
|
||||||
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
||||||
for exposed in exposes:
|
for exposed in exposes:
|
||||||
for feature in exposed.get('features', []):
|
for feature in exposed.get('features', []):
|
||||||
|
@ -1409,14 +1518,113 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'value_on': feature['value_on'],
|
'value_on': feature['value_on'],
|
||||||
'value_off': feature['value_off'],
|
'value_off': feature['value_off'],
|
||||||
'value_toggle': feature.get('value_toggle', None),
|
'value_toggle': feature.get('value_toggle', None),
|
||||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
'is_read_only': cls._is_read_only(feature),
|
||||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
'is_write_only': cls._is_write_only(feature),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _get_light_meta(device_info: dict) -> dict:
|
def _get_sensors(cls, device_info: dict) -> List[Sensor]:
|
||||||
|
sensors = []
|
||||||
|
exposes = [
|
||||||
|
exposed
|
||||||
|
for exposed in (device_info.get('definition', {}) or {}).get('exposes', [])
|
||||||
|
if (exposed.get('property') and cls._is_read_only(exposed))
|
||||||
|
]
|
||||||
|
|
||||||
|
for exposed in exposes:
|
||||||
|
entity_type = None
|
||||||
|
sensor_args = {
|
||||||
|
'id': f'{device_info["ieee_address"]}:{exposed["property"]}',
|
||||||
|
'name': (
|
||||||
|
device_info.get('friendly_name', '[Unnamed device]')
|
||||||
|
+ ' ['
|
||||||
|
+ exposed.get('description', '')
|
||||||
|
+ ']'
|
||||||
|
),
|
||||||
|
'value': device_info.get('state', {}).get(exposed['property']),
|
||||||
|
'description': exposed.get('description'),
|
||||||
|
'is_read_only': cls._is_read_only(exposed),
|
||||||
|
'is_write_only': cls._is_write_only(exposed),
|
||||||
|
'data': device_info,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposed.get('type') == 'numeric':
|
||||||
|
sensor_args.update(
|
||||||
|
{
|
||||||
|
'min': exposed.get('value_min'),
|
||||||
|
'max': exposed.get('value_max'),
|
||||||
|
'unit': exposed.get('unit'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if exposed.get('property') == 'battery':
|
||||||
|
entity_type = Battery
|
||||||
|
elif exposed.get('property') == 'linkquality':
|
||||||
|
entity_type = LinkQuality
|
||||||
|
elif exposed.get('property') == 'current':
|
||||||
|
entity_type = CurrentSensor
|
||||||
|
elif exposed.get('property') == 'energy':
|
||||||
|
entity_type = EnergySensor
|
||||||
|
elif exposed.get('property') == 'power':
|
||||||
|
entity_type = PowerSensor
|
||||||
|
elif exposed.get('property') == 'voltage':
|
||||||
|
entity_type = VoltageSensor
|
||||||
|
elif exposed.get('property', '').endswith('temperature'):
|
||||||
|
entity_type = TemperatureSensor
|
||||||
|
elif exposed.get('property', '').endswith('humidity'):
|
||||||
|
entity_type = HumiditySensor
|
||||||
|
elif exposed.get('type') == 'binary':
|
||||||
|
entity_type = BinarySensor
|
||||||
|
sensor_args['value'] = sensor_args['value'] == exposed.get(
|
||||||
|
'value_on', True
|
||||||
|
)
|
||||||
|
elif exposed.get('type') == 'numeric':
|
||||||
|
entity_type = NumericSensor
|
||||||
|
|
||||||
|
if entity_type:
|
||||||
|
sensors.append(entity_type(**sensor_args))
|
||||||
|
|
||||||
|
return sensors
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_enum_switches(cls, device_info: dict) -> List[Sensor]:
|
||||||
|
devices = []
|
||||||
|
exposes = [
|
||||||
|
exposed
|
||||||
|
for exposed in (device_info.get('definition', {}) or {}).get('exposes', [])
|
||||||
|
if (
|
||||||
|
exposed.get('property')
|
||||||
|
and exposed.get('access', 0) & 2
|
||||||
|
and exposed.get('type') == 'enum'
|
||||||
|
and exposed.get('values')
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
for exposed in exposes:
|
||||||
|
devices.append(
|
||||||
|
EnumSwitch(
|
||||||
|
id=f'{device_info["ieee_address"]}:{exposed["property"]}',
|
||||||
|
name=(
|
||||||
|
device_info.get('friendly_name', '[Unnamed device]')
|
||||||
|
+ ' ['
|
||||||
|
+ exposed.get('description', '')
|
||||||
|
+ ']'
|
||||||
|
),
|
||||||
|
value=device_info.get(exposed['property']),
|
||||||
|
values=exposed.get('values', []),
|
||||||
|
description=exposed.get('description'),
|
||||||
|
is_read_only=cls._is_read_only(exposed),
|
||||||
|
is_write_only=cls._is_write_only(exposed),
|
||||||
|
data=device_info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_light_meta(cls, device_info: dict) -> dict:
|
||||||
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
exposes = (device_info.get('definition', {}) or {}).get('exposes', [])
|
||||||
for exposed in exposes:
|
for exposed in exposes:
|
||||||
if exposed.get('type') == 'light':
|
if exposed.get('type') == 'light':
|
||||||
|
@ -1438,8 +1646,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'value_off': feature['value_off'],
|
'value_off': feature['value_off'],
|
||||||
'state_name': feature['name'],
|
'state_name': feature['name'],
|
||||||
'value_toggle': feature.get('value_toggle', None),
|
'value_toggle': feature.get('value_toggle', None),
|
||||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
'is_read_only': cls._is_read_only(feature),
|
||||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
'is_write_only': cls._is_write_only(feature),
|
||||||
}
|
}
|
||||||
elif (
|
elif (
|
||||||
feature.get('property') == 'brightness'
|
feature.get('property') == 'brightness'
|
||||||
|
@ -1451,8 +1659,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'brightness_name': feature['name'],
|
'brightness_name': feature['name'],
|
||||||
'brightness_min': feature['value_min'],
|
'brightness_min': feature['value_min'],
|
||||||
'brightness_max': feature['value_max'],
|
'brightness_max': feature['value_max'],
|
||||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
'is_read_only': cls._is_read_only(feature),
|
||||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
'is_write_only': cls._is_write_only(feature),
|
||||||
}
|
}
|
||||||
elif (
|
elif (
|
||||||
feature.get('property') == 'color_temp'
|
feature.get('property') == 'color_temp'
|
||||||
|
@ -1464,8 +1672,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'temperature_name': feature['name'],
|
'temperature_name': feature['name'],
|
||||||
'temperature_min': feature['value_min'],
|
'temperature_min': feature['value_min'],
|
||||||
'temperature_max': feature['value_max'],
|
'temperature_max': feature['value_max'],
|
||||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
'is_read_only': cls._is_read_only(feature),
|
||||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
'is_write_only': cls._is_write_only(feature),
|
||||||
}
|
}
|
||||||
elif (
|
elif (
|
||||||
feature.get('property') == 'color'
|
feature.get('property') == 'color'
|
||||||
|
@ -1481,12 +1689,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'hue_max': color_feature.get(
|
'hue_max': color_feature.get(
|
||||||
'value_max', 65535
|
'value_max', 65535
|
||||||
),
|
),
|
||||||
'is_read_only': not bool(
|
'is_read_only': cls._is_read_only(feature),
|
||||||
feature.get('access', 0) & 2
|
'is_write_only': cls._is_write_only(feature),
|
||||||
),
|
|
||||||
'is_write_only': not bool(
|
|
||||||
feature.get('access', 0) & 1
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif color_feature.get('property') == 'saturation':
|
elif color_feature.get('property') == 'saturation':
|
||||||
|
@ -1499,12 +1703,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'saturation_max': color_feature.get(
|
'saturation_max': color_feature.get(
|
||||||
'value_max', 255
|
'value_max', 255
|
||||||
),
|
),
|
||||||
'is_read_only': not bool(
|
'is_read_only': cls._is_read_only(feature),
|
||||||
feature.get('access', 0) & 2
|
'is_write_only': cls._is_write_only(feature),
|
||||||
),
|
|
||||||
'is_write_only': not bool(
|
|
||||||
feature.get('access', 0) & 1
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif color_feature.get('property') == 'x':
|
elif color_feature.get('property') == 'x':
|
||||||
|
@ -1513,12 +1713,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'x_name': color_feature['name'],
|
'x_name': color_feature['name'],
|
||||||
'x_min': color_feature.get('value_min', 0.0),
|
'x_min': color_feature.get('value_min', 0.0),
|
||||||
'x_max': color_feature.get('value_max', 1.0),
|
'x_max': color_feature.get('value_max', 1.0),
|
||||||
'is_read_only': not bool(
|
'is_read_only': cls._is_read_only(feature),
|
||||||
feature.get('access', 0) & 2
|
'is_write_only': cls._is_write_only(feature),
|
||||||
),
|
|
||||||
'is_write_only': not bool(
|
|
||||||
feature.get('access', 0) & 1
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif color_feature.get('property') == 'y':
|
elif color_feature.get('property') == 'y':
|
||||||
|
@ -1527,12 +1723,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
'y_name': color_feature['name'],
|
'y_name': color_feature['name'],
|
||||||
'y_min': color_feature.get('value_min', 0),
|
'y_min': color_feature.get('value_min', 0),
|
||||||
'y_max': color_feature.get('value_max', 255),
|
'y_max': color_feature.get('value_max', 255),
|
||||||
'is_read_only': not bool(
|
'is_read_only': cls._is_read_only(feature),
|
||||||
feature.get('access', 0) & 2
|
'is_write_only': cls._is_write_only(feature),
|
||||||
),
|
|
||||||
'is_write_only': not bool(
|
|
||||||
feature.get('access', 0) & 1
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1548,8 +1740,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_switches_info(self) -> dict:
|
def _get_switches_info(self) -> dict:
|
||||||
# noinspection PyUnresolvedReferences
|
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
||||||
devices = self.devices().output
|
|
||||||
switches_info = {}
|
switches_info = {}
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
|
@ -1558,7 +1749,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
switches_info[
|
switches_info[
|
||||||
device.get('friendly_name', device.get('ieee_address'))
|
device.get('friendly_name', device['ieee_address'] + ':switch')
|
||||||
] = info
|
] = info
|
||||||
|
|
||||||
return switches_info
|
return switches_info
|
||||||
|
@ -1571,14 +1762,13 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
``state`` property that can be set to ``ON`` or ``OFF``).
|
``state`` property that can be set to ``ON`` or ``OFF``).
|
||||||
"""
|
"""
|
||||||
switches_info = self._get_switches_info()
|
switches_info = self._get_switches_info()
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
return [
|
return [
|
||||||
self._properties_to_switch(
|
self._properties_to_switch(
|
||||||
device=name, props=switch, switch_info=switches_info[name]
|
device=name, props=switch, switch_info=switches_info[name]
|
||||||
)
|
)
|
||||||
for name, switch in self.devices_get(
|
for name, switch in self.devices_get(
|
||||||
list(switches_info.keys())
|
list(switches_info.keys())
|
||||||
).output.items()
|
).output.items() # type: ignore[reportGeneralTypeIssues]
|
||||||
]
|
]
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -1587,10 +1777,11 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
||||||
Set the state for one or more Zigbee lights.
|
Set the state for one or more Zigbee lights.
|
||||||
"""
|
"""
|
||||||
lights = [lights] if isinstance(lights, str) else lights
|
lights = [lights] if isinstance(lights, str) else lights
|
||||||
|
lights = [self._ieee_address(t) for t in lights]
|
||||||
devices = [
|
devices = [
|
||||||
dev
|
dev
|
||||||
for dev in self._get_network_info().get('devices', [])
|
for dev in self._get_network_info().get('devices', [])
|
||||||
if dev.get('ieee_address') in lights or dev.get('friendly_name') in lights
|
if self._ieee_address(dev) in lights or dev.get('friendly_name') in lights
|
||||||
]
|
]
|
||||||
|
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
|
|
|
@ -2,13 +2,14 @@ from abc import ABC, abstractmethod
|
||||||
from typing import Any, Dict, Optional, List, Union
|
from typing import Any, Dict, Optional, List, Union
|
||||||
|
|
||||||
from platypush.entities import manages
|
from platypush.entities import manages
|
||||||
|
from platypush.entities.batteries import Battery
|
||||||
from platypush.entities.dimmers import Dimmer
|
from platypush.entities.dimmers import Dimmer
|
||||||
from platypush.entities.lights import Light
|
from platypush.entities.lights import Light
|
||||||
from platypush.entities.switches import Switch
|
from platypush.entities.switches import Switch
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
@manages(Dimmer, Light, Switch)
|
@manages(Battery, Dimmer, Light, Switch)
|
||||||
class ZwaveBasePlugin(Plugin, ABC):
|
class ZwaveBasePlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
Base class for Z-Wave plugins.
|
Base class for Z-Wave plugins.
|
||||||
|
|
|
@ -6,6 +6,7 @@ from datetime import datetime
|
||||||
from threading import Timer
|
from threading import Timer
|
||||||
from typing import Optional, List, Any, Dict, Union, Iterable, Mapping, Callable
|
from typing import Optional, List, Any, Dict, Union, Iterable, Mapping, Callable
|
||||||
|
|
||||||
|
from platypush.entities.batteries import Battery
|
||||||
from platypush.entities.dimmers import Dimmer
|
from platypush.entities.dimmers import Dimmer
|
||||||
from platypush.entities.switches import Switch
|
from platypush.entities.switches import Switch
|
||||||
from platypush.message.event.zwave import ZwaveNodeRenamedEvent, ZwaveNodeEvent
|
from platypush.message.event.zwave import ZwaveNodeRenamedEvent, ZwaveNodeEvent
|
||||||
|
@ -482,6 +483,14 @@ class ZwaveMqttPlugin(MqttPlugin, ZwaveBasePlugin):
|
||||||
value, 'switch_multilevel', 'switch_toggle_multilevel'
|
value, 'switch_multilevel', 'switch_toggle_multilevel'
|
||||||
) and not value.get('is_read_only')
|
) and not value.get('is_read_only')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_battery(cls, value: Mapping):
|
||||||
|
return (
|
||||||
|
cls._matches_classes(value, 'battery')
|
||||||
|
and value.get('is_read_only')
|
||||||
|
and not value['id'].endswith('-isLow')
|
||||||
|
)
|
||||||
|
|
||||||
def _to_entity_args(self, value: Mapping) -> dict:
|
def _to_entity_args(self, value: Mapping) -> dict:
|
||||||
if value['id'].endswith('-targetValue'):
|
if value['id'].endswith('-targetValue'):
|
||||||
current_value_id = '-'.join(value['id'].split('-')[:-1] + ['currentValue'])
|
current_value_id = '-'.join(value['id'].split('-')[:-1] + ['currentValue'])
|
||||||
|
@ -525,6 +534,12 @@ class ZwaveMqttPlugin(MqttPlugin, ZwaveBasePlugin):
|
||||||
entity_args['value'] = value['data']
|
entity_args['value'] = value['data']
|
||||||
entity_args['min'] = value['min']
|
entity_args['min'] = value['min']
|
||||||
entity_args['max'] = value['max']
|
entity_args['max'] = value['max']
|
||||||
|
elif self._is_battery(value):
|
||||||
|
entity_type = Battery
|
||||||
|
entity_args['value'] = value['data']
|
||||||
|
entity_args['min'] = value['min']
|
||||||
|
entity_args['max'] = value['max']
|
||||||
|
entity_args['unit'] = value.get('units', '%')
|
||||||
elif self._is_switch(value):
|
elif self._is_switch(value):
|
||||||
entity_type = Switch
|
entity_type = Switch
|
||||||
entity_args['state'] = value['data']
|
entity_args['state'] = value['data']
|
||||||
|
|
Loading…
Reference in a new issue