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() {
|
||||
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(
|
||||
() => import(`@/components/panels/Entities/${this.type}`)
|
||||
() => import(`@/components/panels/Entities/${type}`)
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -31,7 +31,8 @@ export default {
|
|||
computed: {
|
||||
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="header">
|
||||
<span class="section left">
|
||||
<Icon v-bind="entitiesMeta[group.name].icon || {}"
|
||||
v-if="selector.grouping === 'type' && entitiesMeta[group.name]" />
|
||||
<Icon v-bind="entitiesMeta[typesByCategory[group.name]].icon || {}"
|
||||
v-if="selector.grouping === 'category' && entitiesMeta[typesByCategory[group.name]]" />
|
||||
<Icon :class="pluginIcons[group.name]?.class" :url="pluginIcons[group.name]?.imgUrl"
|
||||
v-else-if="selector.grouping === 'plugin' && pluginIcons[group.name]" />
|
||||
</span>
|
||||
|
||||
<span class="section center">
|
||||
<div class="title" v-text="entitiesMeta[group.name].name_plural"
|
||||
v-if="selector.grouping === 'type' && entitiesMeta[group.name]"/>
|
||||
<div class="title" v-text="group.name" v-else-if="selector.grouping === 'plugin'"/>
|
||||
<div class="title" v-text="group.name" />
|
||||
</span>
|
||||
|
||||
<span class="section right">
|
||||
|
@ -99,7 +97,7 @@ export default {
|
|||
modalEntityId: null,
|
||||
modalVisible: false,
|
||||
selector: {
|
||||
grouping: 'type',
|
||||
grouping: 'category',
|
||||
selectedEntities: {},
|
||||
},
|
||||
}
|
||||
|
@ -114,13 +112,24 @@ export default {
|
|||
return icons
|
||||
},
|
||||
|
||||
entityTypes() {
|
||||
return this.groupEntities('type')
|
||||
},
|
||||
|
||||
typesByCategory() {
|
||||
return Object.entries(meta).reduce((obj, [type, meta]) => {
|
||||
obj[meta.name_plural] = type
|
||||
return obj
|
||||
}, {})
|
||||
},
|
||||
|
||||
entityGroups() {
|
||||
return {
|
||||
'id': Object.entries(this.groupEntities('id')).reduce((obj, [id, entities]) => {
|
||||
obj[id] = entities[0]
|
||||
return obj
|
||||
}, {}),
|
||||
'type': this.groupEntities('type'),
|
||||
'category': this.groupEntities('category'),
|
||||
'plugin': this.groupEntities('plugin'),
|
||||
}
|
||||
},
|
||||
|
@ -148,6 +157,7 @@ export default {
|
|||
return Object.values(this.entities).reduce((obj, entity) => {
|
||||
const entities = obj[entity[attr]] || {}
|
||||
entities[entity.id] = entity
|
||||
|
||||
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
@ -178,11 +188,7 @@ export default {
|
|||
delete self.entityTimeouts[id]
|
||||
|
||||
self.errorEntities[id] = entity
|
||||
self.notify({
|
||||
error: true,
|
||||
title: entity.plugin,
|
||||
text: `Scan timeout for ${entity.name}`,
|
||||
})
|
||||
console.warn(`Scan timeout for ${entity.name}`)
|
||||
}, this.entityScanTimeout * 1000)
|
||||
|
||||
obj[id] = true
|
||||
|
@ -198,6 +204,7 @@ export default {
|
|||
try {
|
||||
this.entities = (await this.request('entities.get')).reduce((obj, entity) => {
|
||||
entity.name = entity?.meta?.name_override || entity.name
|
||||
entity.category = meta[entity.type].name_plural
|
||||
entity.meta = {
|
||||
...(meta[entity.type] || {}),
|
||||
...(entity.meta || {}),
|
||||
|
@ -225,6 +232,7 @@ export default {
|
|||
},
|
||||
|
||||
onEntityInput(entity) {
|
||||
entity.category = meta[entity.type].name_plural
|
||||
this.entities[entity.id] = entity
|
||||
this.clearEntityTimeouts(entity.id)
|
||||
if (this.loadingEntities[entity.id])
|
||||
|
@ -247,6 +255,7 @@ export default {
|
|||
else
|
||||
entity.name = event.entity?.name || this.entities[entityId]?.name
|
||||
|
||||
entity.category = meta[entity.type].name_plural
|
||||
entity.meta = {
|
||||
...(meta[event.entity.type] || {}),
|
||||
...(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: {
|
||||
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) {
|
||||
|
|
|
@ -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": {
|
||||
"name": "Entity",
|
||||
"name_plural": "Entities",
|
||||
"battery": {
|
||||
"name": "Battery",
|
||||
"name_plural": "Batteries",
|
||||
"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": {
|
||||
"name": "Switch",
|
||||
"name_plural": "Switches",
|
||||
"dimmer": {
|
||||
"name": "Dimmer",
|
||||
"name_plural": "Dimmers",
|
||||
"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": {
|
||||
"name": "Dimmer",
|
||||
"name_plural": "Dimmers",
|
||||
"link_quality": {
|
||||
"name": "Link Quality",
|
||||
"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": {
|
||||
"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))
|
||||
|
||||
exposes = (device.get('definition', {}) or {}).get('exposes', [])
|
||||
client.publish(
|
||||
self.base_topic + '/' + name + '/get',
|
||||
json.dumps(self._plugin.build_device_get_request(exposes)),
|
||||
)
|
||||
payload = self._plugin.build_device_get_request(exposes)
|
||||
if payload:
|
||||
client.publish(
|
||||
self.base_topic + '/' + name + '/get',
|
||||
json.dumps(payload),
|
||||
)
|
||||
|
||||
devices_copy = [*self._devices.keys()]
|
||||
for name in devices_copy:
|
||||
|
|
|
@ -259,6 +259,9 @@ class EntitiesEngine(Thread):
|
|||
session.add_all(entities)
|
||||
session.commit()
|
||||
|
||||
for e in entities:
|
||||
session.expunge(e)
|
||||
|
||||
with self._entities_cache_lock:
|
||||
for entity in entities:
|
||||
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
|
||||
|
||||
|
@ -20,3 +20,23 @@ if not entity_types_registry.get('Switch'):
|
|||
entity_types_registry['Switch'] = Switch
|
||||
else:
|
||||
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
|
||||
import decimal
|
||||
import datetime
|
||||
import logging
|
||||
import inspect
|
||||
|
@ -19,7 +20,7 @@ class JSONAble(ABC):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Message(object):
|
||||
class Message:
|
||||
"""
|
||||
Message generic class
|
||||
"""
|
||||
|
@ -38,6 +39,8 @@ class Message(object):
|
|||
return int(obj)
|
||||
if isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return float(obj)
|
||||
if callable(obj):
|
||||
return '<function at {}.{}>'.format(obj.__module__, obj.__name__)
|
||||
|
||||
|
@ -45,9 +48,7 @@ class Message(object):
|
|||
|
||||
@staticmethod
|
||||
def parse_datetime(obj):
|
||||
if isinstance(obj, datetime.datetime) or \
|
||||
isinstance(obj, datetime.date) or \
|
||||
isinstance(obj, datetime.time):
|
||||
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
|
||||
return obj.isoformat()
|
||||
|
||||
def default(self, obj):
|
||||
|
@ -68,8 +69,11 @@ class Message(object):
|
|||
try:
|
||||
return super().default(obj)
|
||||
except Exception as e:
|
||||
logger.warning('Could not serialize object type {}: {}: {}'.format(
|
||||
type(obj), str(e), obj))
|
||||
logger.warning(
|
||||
'Could not serialize object type {}: {}: {}'.format(
|
||||
type(obj), str(e), obj
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, timestamp=None, *_, **__):
|
||||
self.timestamp = timestamp or time.time()
|
||||
|
@ -80,12 +84,15 @@ class Message(object):
|
|||
the message into a UTF-8 JSON string
|
||||
"""
|
||||
|
||||
return json.dumps({
|
||||
attr: getattr(self, attr)
|
||||
for attr in self.__dir__()
|
||||
if (attr != '_timestamp' or not attr.startswith('_'))
|
||||
and not inspect.ismethod(getattr(self, attr))
|
||||
}, cls=self.Encoder).replace('\n', ' ')
|
||||
return json.dumps(
|
||||
{
|
||||
attr: getattr(self, attr)
|
||||
for attr in self.__dir__()
|
||||
if (attr != '_timestamp' or not attr.startswith('_'))
|
||||
and not inspect.ismethod(getattr(self, attr))
|
||||
},
|
||||
cls=self.Encoder,
|
||||
).replace('\n', ' ')
|
||||
|
||||
def __bytes__(self):
|
||||
"""
|
||||
|
@ -105,7 +112,7 @@ class Message(object):
|
|||
|
||||
if isinstance(msg, cls):
|
||||
msg = str(msg)
|
||||
if isinstance(msg, bytes) or isinstance(msg, bytearray):
|
||||
if isinstance(msg, (bytes, bytearray)):
|
||||
msg = msg.decode('utf-8')
|
||||
if isinstance(msg, str):
|
||||
try:
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
import json
|
||||
import re
|
||||
import threading
|
||||
|
||||
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.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.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.response import Response
|
||||
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]
|
||||
"""
|
||||
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):
|
||||
from platypush.entities.switches import Switch
|
||||
|
||||
compatible_entities = []
|
||||
for dev in devices:
|
||||
if not dev:
|
||||
continue
|
||||
|
||||
converted_entity = None
|
||||
dev_def = dev.get("definition") or {}
|
||||
dev_info = {
|
||||
"type": dev.get("type"),
|
||||
|
@ -185,68 +194,80 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
|
||||
light_info = self._get_light_meta(dev)
|
||||
switch_info = self._get_switch_meta(dev)
|
||||
sensors = self._get_sensors(dev)
|
||||
enum_switches = self._get_enum_switches(dev)
|
||||
|
||||
if light_info:
|
||||
converted_entity = Light(
|
||||
id=dev['ieee_address'],
|
||||
name=dev.get('friendly_name'),
|
||||
on=dev.get('state', {}).get('state') == switch_info.get('value_on'),
|
||||
brightness_min=light_info.get('brightness_min'),
|
||||
brightness_max=light_info.get('brightness_max'),
|
||||
temperature_min=light_info.get('temperature_min'),
|
||||
temperature_max=light_info.get('temperature_max'),
|
||||
hue_min=light_info.get('hue_min'),
|
||||
hue_max=light_info.get('hue_max'),
|
||||
saturation_min=light_info.get('saturation_min'),
|
||||
saturation_max=light_info.get('saturation_max'),
|
||||
brightness=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('brightness_name', 'brightness'))
|
||||
),
|
||||
temperature=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('temperature_name', 'temperature'))
|
||||
),
|
||||
hue=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('hue_name', 'hue'))
|
||||
),
|
||||
saturation=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('saturation_name', 'saturation'))
|
||||
),
|
||||
x=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('x_name', 'x'))
|
||||
),
|
||||
y=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('y_name', 'y'))
|
||||
),
|
||||
description=dev_def.get('description'),
|
||||
data=dev_info,
|
||||
compatible_entities.append(
|
||||
Light(
|
||||
id=f'{dev["ieee_address"]}:light',
|
||||
name=dev.get('friendly_name'),
|
||||
on=dev.get('state', {}).get('state')
|
||||
== switch_info.get('value_on'),
|
||||
brightness_min=light_info.get('brightness_min'),
|
||||
brightness_max=light_info.get('brightness_max'),
|
||||
temperature_min=light_info.get('temperature_min'),
|
||||
temperature_max=light_info.get('temperature_max'),
|
||||
hue_min=light_info.get('hue_min'),
|
||||
hue_max=light_info.get('hue_max'),
|
||||
saturation_min=light_info.get('saturation_min'),
|
||||
saturation_max=light_info.get('saturation_max'),
|
||||
brightness=(
|
||||
dev.get('state', {}).get(
|
||||
light_info.get('brightness_name', 'brightness')
|
||||
)
|
||||
),
|
||||
temperature=(
|
||||
dev.get('state', {}).get(
|
||||
light_info.get('temperature_name', 'temperature')
|
||||
)
|
||||
),
|
||||
hue=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('hue_name', 'hue'))
|
||||
),
|
||||
saturation=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('saturation_name', 'saturation'))
|
||||
),
|
||||
x=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('x_name', 'x'))
|
||||
),
|
||||
y=(
|
||||
dev.get('state', {})
|
||||
.get('color', {})
|
||||
.get(light_info.get('y_name', 'y'))
|
||||
),
|
||||
description=dev_def.get('description'),
|
||||
data=dev_info,
|
||||
)
|
||||
)
|
||||
elif switch_info and dev.get('state', {}).get('state') is not None:
|
||||
converted_entity = Switch(
|
||||
id=dev['ieee_address'],
|
||||
name=dev.get('friendly_name'),
|
||||
state=dev.get('state', {}).get('state') == switch_info['value_on'],
|
||||
description=dev_def.get("description"),
|
||||
data=dev_info,
|
||||
compatible_entities.append(
|
||||
Switch(
|
||||
id=f'{dev["ieee_address"]}:switch',
|
||||
name=dev.get('friendly_name'),
|
||||
state=dev.get('state', {}).get('state')
|
||||
== 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:
|
||||
compatible_entities.append(converted_entity)
|
||||
if sensors:
|
||||
compatible_entities += sensors
|
||||
if enum_switches:
|
||||
compatible_entities += enum_switches
|
||||
|
||||
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')
|
||||
client = None
|
||||
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')
|
||||
return info
|
||||
finally:
|
||||
try:
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
if client:
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
'Error on MQTT client disconnection: {}'.format(str(e))
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def _topic(self, topic):
|
||||
return self.base_topic + '/' + topic
|
||||
|
||||
@staticmethod
|
||||
def _parse_response(response: Union[dict, Response]) -> dict:
|
||||
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'
|
||||
)
|
||||
return response
|
||||
return response # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
@action
|
||||
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
|
||||
def permit_join(
|
||||
|
@ -520,12 +543,13 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
"""
|
||||
if timeout:
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/permit_join'),
|
||||
msg={'value': permit, 'time': timeout},
|
||||
reply_topic=self._topic('bridge/response/permit_join'),
|
||||
**self._mqtt_args(**kwargs),
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
return self.publish(
|
||||
|
@ -560,7 +584,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/config/log_level'),
|
||||
msg={'value': 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.
|
||||
|
||||
:param device: Display name of the device.
|
||||
:param device: Display name or IEEE address of the device.
|
||||
:param option: Option name.
|
||||
:param value: New value.
|
||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/options'),
|
||||
reply_topic=self._topic('bridge/response/device/options'),
|
||||
msg={
|
||||
|
@ -606,7 +630,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/remove'),
|
||||
msg={'id': device, 'force': force},
|
||||
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).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/ban'),
|
||||
reply_topic=self._topic('bridge/response/device/ban'),
|
||||
msg={'id': device},
|
||||
|
@ -643,7 +667,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/whitelist'),
|
||||
reply_topic=self._topic('bridge/response/device/whitelist'),
|
||||
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')
|
||||
return
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
devices = self.devices().output
|
||||
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
||||
assert not [
|
||||
dev for dev in devices if dev.get('friendly_name') == 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(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/rename'),
|
||||
msg=req,
|
||||
reply_topic=self._topic('bridge/response/device/rename'),
|
||||
|
@ -694,9 +717,16 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
|
||||
@staticmethod
|
||||
def build_device_get_request(values: List[Dict[str, Any]]) -> dict:
|
||||
def extract_value(value: dict, root: dict):
|
||||
if not value.get('access', 1) & 0x1:
|
||||
# Property not readable
|
||||
def extract_value(value: dict, root: dict, depth: int = 0):
|
||||
for feature in value.get('features', []):
|
||||
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
|
||||
|
||||
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 = root[value['property']]
|
||||
|
||||
for feature in value['features']:
|
||||
extract_value(feature, root)
|
||||
|
||||
ret = {}
|
||||
for value in values:
|
||||
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, {})
|
||||
)
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
@action
|
||||
def device_get(
|
||||
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)
|
||||
device_info = self._get_device_info(device)
|
||||
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:
|
||||
properties = self.publish(
|
||||
|
@ -749,7 +775,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
reply_topic=self._topic(device),
|
||||
msg={property: ''},
|
||||
**kwargs,
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
assert property in properties, f'No such property: {property}'
|
||||
return {property: properties[property]}
|
||||
|
@ -774,7 +800,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
reply_topic=self._topic(device),
|
||||
msg=self.build_device_get_request(exposes),
|
||||
**kwargs,
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
if device_info:
|
||||
self.publish_entities( # type: ignore
|
||||
|
@ -816,13 +842,15 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
kwargs = self._mqtt_args(**kwargs)
|
||||
|
||||
if not devices:
|
||||
devices = {
|
||||
device['friendly_name'] or device['ieee_address']
|
||||
for device in self.devices(**kwargs).output
|
||||
}
|
||||
devices = list(
|
||||
{
|
||||
device['friendly_name'] or device['ieee_address']
|
||||
for device in self.devices(**kwargs).output # type: ignore[reportGeneralTypeIssues]
|
||||
}
|
||||
)
|
||||
|
||||
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 = {}
|
||||
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)
|
||||
|
||||
# noinspection PyShadowingBuiltins,DuplicatedCode
|
||||
@action
|
||||
def device_set(
|
||||
self,
|
||||
|
@ -880,22 +907,63 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
msg = (values or {}).copy()
|
||||
reply_topic = self._topic(device)
|
||||
|
||||
if property:
|
||||
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(
|
||||
topic=self._topic(device + '/set'),
|
||||
reply_topic=self._topic(device),
|
||||
reply_topic=reply_topic,
|
||||
msg=msg,
|
||||
**self._mqtt_args(**kwargs),
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
if property:
|
||||
if property and reply_topic:
|
||||
assert property in properties, 'No such property: ' + property
|
||||
return {property: properties[property]}
|
||||
|
||||
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
|
||||
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(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/ota_update/check'),
|
||||
reply_topic=self._topic('bridge/response/device/ota_update/check'),
|
||||
msg={'id': device},
|
||||
|
@ -941,7 +1009,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/ota_update/update'),
|
||||
reply_topic=self._topic('bridge/response/device/ota_update/update'),
|
||||
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``
|
||||
(default: query the default configured device).
|
||||
"""
|
||||
return self._get_network_info(**kwargs).get('groups')
|
||||
return self._get_network_info(**kwargs).get('groups', [])
|
||||
|
||||
@action
|
||||
def info(self, **kwargs) -> dict:
|
||||
|
@ -1113,7 +1181,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
)
|
||||
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/group/add'),
|
||||
reply_topic=self._topic('bridge/response/group/add'),
|
||||
msg=payload,
|
||||
|
@ -1142,7 +1210,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
reply_topic=self._topic(group),
|
||||
msg=msg,
|
||||
**self._mqtt_args(**kwargs),
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
if 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),
|
||||
msg={property: value},
|
||||
**self._mqtt_args(**kwargs),
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
|
||||
if 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')
|
||||
return
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
groups = {group.get('friendly_name'): group for group in self.groups().output}
|
||||
groups = {
|
||||
group.get('friendly_name'): group
|
||||
for group in self.groups().output # type: ignore[reportGeneralTypeIssues]
|
||||
}
|
||||
|
||||
assert (
|
||||
name not in groups
|
||||
), 'A group named {} already exists on the network'.format(name)
|
||||
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/group/rename'),
|
||||
reply_topic=self._topic('bridge/response/group/rename'),
|
||||
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).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/group/remove'),
|
||||
reply_topic=self._topic('bridge/response/group/remove'),
|
||||
msg=name,
|
||||
|
@ -1235,7 +1306,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/group/members/add'),
|
||||
reply_topic=self._topic('bridge/response/group/members/add'),
|
||||
msg={
|
||||
|
@ -1258,7 +1329,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic(
|
||||
'bridge/request/group/members/remove{}'.format(
|
||||
'_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).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/bind'),
|
||||
reply_topic=self._topic('bridge/response/device/bind'),
|
||||
msg={'from': source, 'to': target},
|
||||
|
@ -1315,7 +1386,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
(default: query the default configured device).
|
||||
"""
|
||||
return self._parse_response(
|
||||
self.publish(
|
||||
self.publish( # type: ignore[reportGeneralTypeIssues]
|
||||
topic=self._topic('bridge/request/device/unbind'),
|
||||
reply_topic=self._topic('bridge/response/device/unbind'),
|
||||
msg={'from': source, 'to': target},
|
||||
|
@ -1324,54 +1395,55 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
)
|
||||
|
||||
@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
|
||||
binary property.
|
||||
"""
|
||||
switch_info = self._get_switch_info(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(
|
||||
device, switch_info['property'], switch_info['value_on']
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
return self._properties_to_switch(
|
||||
device=device, props=props, switch_info=switch_info
|
||||
)
|
||||
|
||||
@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
|
||||
writable binary property.
|
||||
"""
|
||||
switch_info = self._get_switch_info(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(
|
||||
device, switch_info['property'], switch_info['value_off']
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
return self._properties_to_switch(
|
||||
device=device, props=props, switch_info=switch_info
|
||||
)
|
||||
|
||||
@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
|
||||
writable binary property.
|
||||
Implements :meth:`platypush.plugins.switch.plugin.SwitchPlugin.toggle`
|
||||
and toggles a Zigbee device with a writable binary property.
|
||||
"""
|
||||
switch_info = self._get_switch_info(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(
|
||||
device, switch_info['property'], switch_info['value_toggle']
|
||||
).output
|
||||
).output # type: ignore[reportGeneralTypeIssues]
|
||||
return self._properties_to_switch(
|
||||
device=device, props=props, switch_info=switch_info
|
||||
)
|
||||
|
||||
def _get_switch_info(self, device: str):
|
||||
device = self._ieee_address(device)
|
||||
switches_info = self._get_switches_info()
|
||||
info = switches_info.get(device)
|
||||
if info:
|
||||
|
@ -1392,7 +1464,44 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
}
|
||||
|
||||
@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', [])
|
||||
for exposed in exposes:
|
||||
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_off': feature['value_off'],
|
||||
'value_toggle': feature.get('value_toggle', None),
|
||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _get_light_meta(device_info: dict) -> dict:
|
||||
@classmethod
|
||||
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', [])
|
||||
for exposed in exposes:
|
||||
if exposed.get('type') == 'light':
|
||||
|
@ -1438,8 +1646,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
'value_off': feature['value_off'],
|
||||
'state_name': feature['name'],
|
||||
'value_toggle': feature.get('value_toggle', None),
|
||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
elif (
|
||||
feature.get('property') == 'brightness'
|
||||
|
@ -1451,8 +1659,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
'brightness_name': feature['name'],
|
||||
'brightness_min': feature['value_min'],
|
||||
'brightness_max': feature['value_max'],
|
||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
elif (
|
||||
feature.get('property') == 'color_temp'
|
||||
|
@ -1464,8 +1672,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
'temperature_name': feature['name'],
|
||||
'temperature_min': feature['value_min'],
|
||||
'temperature_max': feature['value_max'],
|
||||
'is_read_only': not bool(feature.get('access', 0) & 2),
|
||||
'is_write_only': not bool(feature.get('access', 0) & 1),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
elif (
|
||||
feature.get('property') == 'color'
|
||||
|
@ -1481,12 +1689,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
'hue_max': color_feature.get(
|
||||
'value_max', 65535
|
||||
),
|
||||
'is_read_only': not bool(
|
||||
feature.get('access', 0) & 2
|
||||
),
|
||||
'is_write_only': not bool(
|
||||
feature.get('access', 0) & 1
|
||||
),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
)
|
||||
elif color_feature.get('property') == 'saturation':
|
||||
|
@ -1499,12 +1703,8 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
'saturation_max': color_feature.get(
|
||||
'value_max', 255
|
||||
),
|
||||
'is_read_only': not bool(
|
||||
feature.get('access', 0) & 2
|
||||
),
|
||||
'is_write_only': not bool(
|
||||
feature.get('access', 0) & 1
|
||||
),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
)
|
||||
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_min': color_feature.get('value_min', 0.0),
|
||||
'x_max': color_feature.get('value_max', 1.0),
|
||||
'is_read_only': not bool(
|
||||
feature.get('access', 0) & 2
|
||||
),
|
||||
'is_write_only': not bool(
|
||||
feature.get('access', 0) & 1
|
||||
),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
)
|
||||
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_min': color_feature.get('value_min', 0),
|
||||
'y_max': color_feature.get('value_max', 255),
|
||||
'is_read_only': not bool(
|
||||
feature.get('access', 0) & 2
|
||||
),
|
||||
'is_write_only': not bool(
|
||||
feature.get('access', 0) & 1
|
||||
),
|
||||
'is_read_only': cls._is_read_only(feature),
|
||||
'is_write_only': cls._is_write_only(feature),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1548,8 +1740,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
return {}
|
||||
|
||||
def _get_switches_info(self) -> dict:
|
||||
# noinspection PyUnresolvedReferences
|
||||
devices = self.devices().output
|
||||
devices = self.devices().output # type: ignore[reportGeneralTypeIssues]
|
||||
switches_info = {}
|
||||
|
||||
for device in devices:
|
||||
|
@ -1558,7 +1749,7 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
continue
|
||||
|
||||
switches_info[
|
||||
device.get('friendly_name', device.get('ieee_address'))
|
||||
device.get('friendly_name', device['ieee_address'] + ':switch')
|
||||
] = 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``).
|
||||
"""
|
||||
switches_info = self._get_switches_info()
|
||||
# noinspection PyUnresolvedReferences
|
||||
return [
|
||||
self._properties_to_switch(
|
||||
device=name, props=switch, switch_info=switches_info[name]
|
||||
)
|
||||
for name, switch in self.devices_get(
|
||||
list(switches_info.keys())
|
||||
).output.items()
|
||||
).output.items() # type: ignore[reportGeneralTypeIssues]
|
||||
]
|
||||
|
||||
@action
|
||||
|
@ -1587,10 +1777,11 @@ class ZigbeeMqttPlugin(MqttPlugin): # lgtm [py/missing-call-to-init]
|
|||
Set the state for one or more Zigbee lights.
|
||||
"""
|
||||
lights = [lights] if isinstance(lights, str) else lights
|
||||
lights = [self._ieee_address(t) for t in lights]
|
||||
devices = [
|
||||
dev
|
||||
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:
|
||||
|
|
|
@ -2,13 +2,14 @@ from abc import ABC, abstractmethod
|
|||
from typing import Any, Dict, Optional, List, Union
|
||||
|
||||
from platypush.entities import manages
|
||||
from platypush.entities.batteries import Battery
|
||||
from platypush.entities.dimmers import Dimmer
|
||||
from platypush.entities.lights import Light
|
||||
from platypush.entities.switches import Switch
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
@manages(Dimmer, Light, Switch)
|
||||
@manages(Battery, Dimmer, Light, Switch)
|
||||
class ZwaveBasePlugin(Plugin, ABC):
|
||||
"""
|
||||
Base class for Z-Wave plugins.
|
||||
|
|
|
@ -6,6 +6,7 @@ from datetime import datetime
|
|||
from threading import Timer
|
||||
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.switches import Switch
|
||||
from platypush.message.event.zwave import ZwaveNodeRenamedEvent, ZwaveNodeEvent
|
||||
|
@ -482,6 +483,14 @@ class ZwaveMqttPlugin(MqttPlugin, ZwaveBasePlugin):
|
|||
value, 'switch_multilevel', 'switch_toggle_multilevel'
|
||||
) 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:
|
||||
if value['id'].endswith('-targetValue'):
|
||||
current_value_id = '-'.join(value['id'].split('-')[:-1] + ['currentValue'])
|
||||
|
@ -525,6 +534,12 @@ class ZwaveMqttPlugin(MqttPlugin, ZwaveBasePlugin):
|
|||
entity_args['value'] = value['data']
|
||||
entity_args['min'] = value['min']
|
||||
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):
|
||||
entity_type = Switch
|
||||
entity_args['state'] = value['data']
|
||||
|
|
Loading…
Reference in a new issue