Merge pull request 'Merge mqtt backend and plugin' (#320) from 315/merge-mqtt-backend-and-plugin into master

Reviewed-on: platypush/platypush#320
This commit is contained in:
Fabio Manganiello 2023-09-17 02:51:47 +02:00
commit 9e7b95583b
108 changed files with 2365 additions and 4407 deletions

View file

@ -56,5 +56,4 @@ Backends
platypush/backend/weather.darksky.rst
platypush/backend/weather.openweathermap.rst
platypush/backend/wiimote.rst
platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View file

@ -1,5 +0,0 @@
``zwave``
===========================
.. automodule:: platypush.backend.zwave
:members:

View file

@ -1,5 +0,0 @@
``zwave``
===========================
.. automodule:: platypush.plugins.zwave
:members:

View file

@ -149,5 +149,4 @@ Plugins
platypush/plugins/xmpp.rst
platypush/plugins/zeroconf.rst
platypush/plugins/zigbee.mqtt.rst
platypush/plugins/zwave.rst
platypush/plugins/zwave.mqtt.rst

View file

@ -4,9 +4,17 @@ from typing import Type, Optional, Union, List
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.chat.telegram import MessageEvent, CommandMessageEvent, TextMessageEvent, \
PhotoMessageEvent, VideoMessageEvent, ContactMessageEvent, DocumentMessageEvent, LocationMessageEvent, \
GroupChatCreatedEvent
from platypush.message.event.chat.telegram import (
MessageEvent,
CommandMessageEvent,
TextMessageEvent,
PhotoMessageEvent,
VideoMessageEvent,
ContactMessageEvent,
DocumentMessageEvent,
LocationMessageEvent,
GroupChatCreatedEvent,
)
from platypush.plugins.chat.telegram import ChatTelegramPlugin
@ -23,7 +31,7 @@ class ChatTelegramBackend(Backend):
* :class:`platypush.message.event.chat.telegram.ContactMessageEvent` when a contact is received.
* :class:`platypush.message.event.chat.telegram.DocumentMessageEvent` when a document is received.
* :class:`platypush.message.event.chat.telegram.CommandMessageEvent` when a command message is received.
* :class:`platypush.message.event.chat.telegram.GroupCreatedEvent` when the bot is invited to a new group.
* :class:`platypush.message.event.chat.telegram.GroupChatCreatedEvent` when the bot is invited to a new group.
Requires:
@ -31,7 +39,9 @@ class ChatTelegramBackend(Backend):
"""
def __init__(self, authorized_chat_ids: Optional[List[Union[str, int]]] = None, **kwargs):
def __init__(
self, authorized_chat_ids: Optional[List[Union[str, int]]] = None, **kwargs
):
"""
:param authorized_chat_ids: Optional list of chat_id/user_id which are authorized to send messages to
the bot. If nothing is specified then no restrictions are applied.
@ -39,40 +49,52 @@ class ChatTelegramBackend(Backend):
super().__init__(**kwargs)
self.authorized_chat_ids = set(authorized_chat_ids or [])
self._plugin: ChatTelegramPlugin = get_plugin('chat.telegram')
self._plugin: ChatTelegramPlugin = get_plugin('chat.telegram') # type: ignore
def _authorize(self, msg):
if not self.authorized_chat_ids:
return
if msg.chat.type == 'private' and msg.chat.id not in self.authorized_chat_ids:
self.logger.info('Received message from unauthorized chat_id {}'.format(msg.chat.id))
self._plugin.send_message(chat_id=msg.chat.id, text='You are not allowed to send messages to this bot')
self.logger.info(
'Received message from unauthorized chat_id %s', msg.chat.id
)
self._plugin.send_message(
chat_id=msg.chat.id,
text='You are not allowed to send messages to this bot',
)
raise PermissionError
def _msg_hook(self, cls: Type[MessageEvent]):
# noinspection PyUnusedLocal
def hook(update, context):
def hook(update, _):
msg = update.effective_message
try:
self._authorize(msg)
self.bus.post(cls(chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output))
self.bus.post(
cls(
chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
except PermissionError:
pass
return hook
def _group_hook(self):
# noinspection PyUnusedLocal
def hook(update, context):
msg = update.effective_message
if msg.group_chat_created:
self.bus.post(GroupChatCreatedEvent(chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output))
self.bus.post(
GroupChatCreatedEvent(
chat_id=update.effective_chat.id,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
elif msg.photo:
self._msg_hook(PhotoMessageEvent)(update, context)
elif msg.video:
@ -92,27 +114,33 @@ class ChatTelegramBackend(Backend):
return hook
def _command_hook(self):
# noinspection PyUnusedLocal
def hook(update, context):
def hook(update, _):
msg = update.effective_message
m = re.match('\s*/([0-9a-zA-Z_-]+)\s*(.*)', msg.text)
m = re.match(r'\s*/([0-9a-zA-Z_-]+)\s*(.*)', msg.text)
if not m:
self.logger.warning('Invalid command: %s', msg.text)
return
cmd = m.group(1).lower()
args = [arg for arg in re.split('\s+', m.group(2)) if len(arg)]
args = [arg for arg in re.split(r'\s+', m.group(2)) if len(arg)]
try:
self._authorize(msg)
self.bus.post(CommandMessageEvent(chat_id=update.effective_chat.id,
command=cmd,
cmdargs=args,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output))
self.bus.post(
CommandMessageEvent(
chat_id=update.effective_chat.id,
command=cmd,
cmdargs=args,
message=self._plugin.parse_msg(msg).output,
user=self._plugin.parse_user(update.effective_user).output,
)
)
except PermissionError:
pass
return hook
def run(self):
# noinspection PyPackageRequirements
from telegram.ext import MessageHandler, Filters
super().run()
@ -120,12 +148,24 @@ class ChatTelegramBackend(Backend):
dispatcher = telegram.dispatcher
dispatcher.add_handler(MessageHandler(Filters.group, self._group_hook()))
dispatcher.add_handler(MessageHandler(Filters.text, self._msg_hook(TextMessageEvent)))
dispatcher.add_handler(MessageHandler(Filters.photo, self._msg_hook(PhotoMessageEvent)))
dispatcher.add_handler(MessageHandler(Filters.video, self._msg_hook(VideoMessageEvent)))
dispatcher.add_handler(MessageHandler(Filters.contact, self._msg_hook(ContactMessageEvent)))
dispatcher.add_handler(MessageHandler(Filters.location, self._msg_hook(LocationMessageEvent)))
dispatcher.add_handler(MessageHandler(Filters.document, self._msg_hook(DocumentMessageEvent)))
dispatcher.add_handler(
MessageHandler(Filters.text, self._msg_hook(TextMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.photo, self._msg_hook(PhotoMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.video, self._msg_hook(VideoMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.contact, self._msg_hook(ContactMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.location, self._msg_hook(LocationMessageEvent))
)
dispatcher.add_handler(
MessageHandler(Filters.document, self._msg_hook(DocumentMessageEvent))
)
dispatcher.add_handler(MessageHandler(Filters.command, self._command_hook()))
self.logger.info('Initialized Telegram backend')

View file

@ -5,7 +5,7 @@ manifest:
platypush.message.event.chat.telegram.ContactMessageEvent: when a contact is received.
platypush.message.event.chat.telegram.DocumentMessageEvent: when a document is
received.
platypush.message.event.chat.telegram.GroupCreatedEvent: when the bot is invited
platypush.message.event.chat.telegram.GroupChatCreatedEvent: when the bot is invited
to a new group.
platypush.message.event.chat.telegram.LocationMessageEvent: when a location is
received.

View file

@ -4,7 +4,7 @@ from .auth import (
authenticate_user_pass,
get_auth_status,
)
from .bus import bus, get_message_response, send_message, send_request
from .bus import bus, send_message, send_request
from .logger import logger
from .routes import (
get_http_port,
@ -25,7 +25,6 @@ __all__ = [
'get_http_port',
'get_ip_or_hostname',
'get_local_base_url',
'get_message_response',
'get_remote_base_url',
'get_routes',
'get_streaming_routes',

View file

@ -1,11 +1,9 @@
from redis import Redis
from platypush.bus.redis import RedisBus
from platypush.config import Config
from platypush.context import get_backend
from platypush.message import Message
from platypush.message.request import Request
from platypush.utils import get_redis_conf, get_redis_queue_name_by_message
from platypush.utils import get_redis_conf, get_message_response
from .logger import logger
@ -67,24 +65,3 @@ def send_request(action, wait_for_response=True, **kwargs):
msg['args'] = kwargs
return send_message(msg, wait_for_response=wait_for_response)
def get_message_response(msg):
"""
Get the response to the given message.
:param msg: The message to get the response for.
:return: The response to the given message.
"""
redis = Redis(**bus().redis_args)
redis_queue = get_redis_queue_name_by_message(msg)
if not redis_queue:
return None
response = redis.blpop(redis_queue, timeout=60)
if response and len(response) > 1:
response = Message.build(response[1])
else:
response = None
return response

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.d5110853.js"></script><script defer="defer" src="/static/js/app.2b500cfd.js"></script><link href="/static/css/chunk-vendors.d510eff2.css" rel="stylesheet"><link href="/static/css/app.86fe5b1c.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.d5110853.js"></script><script defer="defer" src="/static/js/app.3c7ccc2c.js"></script><link href="/static/css/chunk-vendors.d510eff2.css" rel="stylesheet"><link href="/static/css/app.86fe5b1c.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[169],{169:function(t,e,n){n.r(e),n.d(e,{default:function(){return x}});var l=n(6252),a=n(3577);const i={class:"entity sensor-container"},s={class:"head"},u={class:"icon"},o={class:"label"},r=["textContent"],c=["textContent"];function d(t,e,n,d,v,p){const f=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",i,[(0,l._)("div",s,[(0,l._)("div",u,[(0,l.Wm)(f,{entity:t.value,loading:t.loading,error:t.error},null,8,["entity","loading","error"])]),(0,l._)("div",o,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(t.value.name)},null,8,r)]),(0,l._)("div",{class:"value-container",textContent:(0,a.zw)(p.displayValue)},null,8,c)])])}var v=n(847),p=n(4967),f={name:"PercentSensor",components:{EntityIcon:p["default"]},mixins:[v["default"]],computed:{displayValue(){if(null==this.value.value)return null;let t=100*this.value.value;return(t.toString()==t.toFixed(0)?t.toFixed(0):t.toFixed(1))+"%"}}},y=n(3744);const h=(0,y.Z)(f,[["render",d],["__scopeId","data-v-1b6c81c2"]]);var x=h}}]);
//# sourceMappingURL=169.ebdd7044.js.map
//# sourceMappingURL=169.02caaaba.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/169.ebdd7044.js","mappings":"8LACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,6GANfC,EAAAA,EAAAA,IAYM,MAZNC,EAYM,EAXJC,EAAAA,EAAAA,GAUM,MAVNC,EAUM,EATJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAAqD,OAAhDH,MAAM,kB,aAAkBc,EAAAA,EAAAA,IAAQE,EAAaC,e,qCASxD,GACEF,KAAM,gBACNG,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YACTC,SAAU,CACRL,YAAAA,GACE,GAAwB,MAApBM,KAAKb,MAAMA,MACb,OAAO,KAGT,IAAIc,EAAY,IAAMD,KAAKb,MAAMA,MACjC,OACEc,EAAUC,YAAcD,EAAUE,QAAQ,GACxCF,EAAUE,QAAQ,GAAKF,EAAUE,QAAQ,IACzC,GACN,I,UC5BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/PercentSensor.vue","webpack://platypush/./src/components/panels/Entities/PercentSensor.vue?1f84"],"sourcesContent":["<template>\n <div class=\"entity sensor-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\" v-text=\"displayValue\" />\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'PercentSensor',\n components: {EntityIcon},\n mixins: [EntityMixin],\n computed: {\n displayValue() {\n if (this.value.value == null) {\n return null\n }\n\n let normValue = 100 * this.value.value\n return (\n normValue.toString() == normValue.toFixed(0)\n ? normValue.toFixed(0) : normValue.toFixed(1)\n ) + '%'\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./PercentSensor.vue?vue&type=template&id=1b6c81c2&scoped=true\"\nimport script from \"./PercentSensor.vue?vue&type=script&lang=js\"\nexport * from \"./PercentSensor.vue?vue&type=script&lang=js\"\n\nimport \"./PercentSensor.vue?vue&type=style&index=0&id=1b6c81c2&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-1b6c81c2\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","$options","displayValue","components","EntityIcon","mixins","EntityMixin","computed","this","normValue","toString","toFixed","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/169.02caaaba.js","mappings":"8LACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,6GANfC,EAAAA,EAAAA,IAYM,MAZNC,EAYM,EAXJC,EAAAA,EAAAA,GAUM,MAVNC,EAUM,EATJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAAqD,OAAhDH,MAAM,kB,aAAkBc,EAAAA,EAAAA,IAAQE,EAAaC,e,qCASxD,GACEF,KAAM,gBACNG,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YACTC,SAAU,CACRL,YAAAA,GACE,GAAwB,MAApBM,KAAKb,MAAMA,MACb,OAAO,KAGT,IAAIc,EAAY,IAAMD,KAAKb,MAAMA,MACjC,OACEc,EAAUC,YAAcD,EAAUE,QAAQ,GACxCF,EAAUE,QAAQ,GAAKF,EAAUE,QAAQ,IACzC,GACN,I,UC5BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/PercentSensor.vue","webpack://platypush/./src/components/panels/Entities/PercentSensor.vue?1f84"],"sourcesContent":["<template>\n <div class=\"entity sensor-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\" v-text=\"displayValue\" />\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'PercentSensor',\n components: {EntityIcon},\n mixins: [EntityMixin],\n computed: {\n displayValue() {\n if (this.value.value == null) {\n return null\n }\n\n let normValue = 100 * this.value.value\n return (\n normValue.toString() == normValue.toFixed(0)\n ? normValue.toFixed(0) : normValue.toFixed(1)\n ) + '%'\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./PercentSensor.vue?vue&type=template&id=1b6c81c2&scoped=true\"\nimport script from \"./PercentSensor.vue?vue&type=script&lang=js\"\nexport * from \"./PercentSensor.vue?vue&type=script&lang=js\"\n\nimport \"./PercentSensor.vue?vue&type=style&index=0&id=1b6c81c2&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-1b6c81c2\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","$options","displayValue","components","EntityIcon","mixins","EntityMixin","computed","this","normValue","toString","toFixed","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[2217],{2217:function(n,t,e){e.r(t),e.d(t,{default:function(){return y}});var a=e(6252),l=e(3577);const s={class:"entity cpu-times-container"},i={class:"head"},c={class:"col-1 icon"},o={class:"col-11 label"},r=["textContent"];function u(n,t,e,u,d,p){const v=(0,a.up)("EntityIcon");return(0,a.wg)(),(0,a.iD)("div",s,[(0,a._)("div",i,[(0,a._)("div",c,[(0,a.Wm)(v,{entity:n.value,loading:n.loading,error:n.error},null,8,["entity","loading","error"])]),(0,a._)("div",o,[(0,a._)("div",{class:"name",textContent:(0,l.zw)(n.value.name)},null,8,r)])])])}var d=e(847),p=e(4967),v={name:"CpuTimes",components:{EntityIcon:p["default"]},mixins:[d["default"]]},f=e(3744);const m=(0,f.Z)(v,[["render",u],["__scopeId","data-v-4667e342"]]);var y=m}}]);
//# sourceMappingURL=2217.6b927594.js.map
//# sourceMappingURL=2217.9116c837.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/2217.6b927594.js","mappings":"gMACOA,MAAM,8B,GACJA,MAAM,Q,GACJA,MAAM,c,GAONA,MAAM,gB,2FATfC,EAAAA,EAAAA,IAaM,MAbNC,EAaM,EAZJC,EAAAA,EAAAA,GAWM,MAXNC,EAWM,EAVJD,EAAAA,EAAAA,GAKM,MALNE,EAKM,EAJJC,EAAAA,EAAAA,IAGmBC,EAAA,CAFhBC,OAAQC,EAAAC,MACRC,QAASF,EAAAE,QACTC,MAAOH,EAAAG,O,wCAGZT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,uCAUxC,GACEA,KAAM,WACNC,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UCjBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/CpuTimes.vue","webpack://platypush/./src/components/panels/Entities/CpuTimes.vue?1fa2"],"sourcesContent":["<template>\n <div class=\"entity cpu-times-container\">\n <div class=\"head\">\n <div class=\"col-1 icon\">\n <EntityIcon\n :entity=\"value\"\n :loading=\"loading\"\n :error=\"error\" />\n </div>\n\n <div class=\"col-11 label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'CpuTimes',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./CpuTimes.vue?vue&type=template&id=4667e342&scoped=true\"\nimport script from \"./CpuTimes.vue?vue&type=script&lang=js\"\nexport * from \"./CpuTimes.vue?vue&type=script&lang=js\"\n\nimport \"./CpuTimes.vue?vue&type=style&index=0&id=4667e342&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-4667e342\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/2217.9116c837.js","mappings":"gMACOA,MAAM,8B,GACJA,MAAM,Q,GACJA,MAAM,c,GAONA,MAAM,gB,2FATfC,EAAAA,EAAAA,IAaM,MAbNC,EAaM,EAZJC,EAAAA,EAAAA,GAWM,MAXNC,EAWM,EAVJD,EAAAA,EAAAA,GAKM,MALNE,EAKM,EAJJC,EAAAA,EAAAA,IAGmBC,EAAA,CAFhBC,OAAQC,EAAAC,MACRC,QAASF,EAAAE,QACTC,MAAOH,EAAAG,O,wCAGZT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,uCAUxC,GACEA,KAAM,WACNC,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UCjBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/CpuTimes.vue","webpack://platypush/./src/components/panels/Entities/CpuTimes.vue?1fa2"],"sourcesContent":["<template>\n <div class=\"entity cpu-times-container\">\n <div class=\"head\">\n <div class=\"col-1 icon\">\n <EntityIcon\n :entity=\"value\"\n :loading=\"loading\"\n :error=\"error\" />\n </div>\n\n <div class=\"col-11 label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'CpuTimes',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./CpuTimes.vue?vue&type=template&id=4667e342&scoped=true\"\nimport script from \"./CpuTimes.vue?vue&type=script&lang=js\"\nexport * from \"./CpuTimes.vue?vue&type=script&lang=js\"\n\nimport \"./CpuTimes.vue?vue&type=style&index=0&id=4667e342&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-4667e342\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[2460],{2460:function(n,t,e){e.r(t),e.d(t,{default:function(){return C}});var a=e(6252),l=e(3577);const c={class:"entity cpu-container"},s={class:"head"},o={class:"col-1 icon"},u={class:"label"},i=["textContent"],r={class:"value-container"},d=["textContent"];function v(n,t,e,v,p,f){const _=(0,a.up)("EntityIcon");return(0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",s,[(0,a._)("div",o,[(0,a.Wm)(_,{entity:n.value,loading:n.loading,error:n.error},null,8,["entity","loading","error"])]),(0,a._)("div",u,[(0,a._)("div",{class:"name",textContent:(0,l.zw)(n.value.name)},null,8,i)]),(0,a._)("div",r,[(0,a._)("div",{class:"value",textContent:(0,l.zw)(Math.round(100*n.value.percent,1)+"%")},null,8,d)])])])}var p=e(847),f=e(4967),_={name:"Cpu",components:{EntityIcon:f["default"]},mixins:[p["default"]]},h=e(3744);const y=(0,h.Z)(_,[["render",v],["__scopeId","data-v-d3cf6cca"]]);var C=y}}]);
//# sourceMappingURL=2460.567e73f6.js.map
//# sourceMappingURL=2460.6a8718df.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/2460.567e73f6.js","mappings":"gMACOA,MAAM,wB,GACJA,MAAM,Q,GACJA,MAAM,c,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAcM,MAdNC,EAcM,EAbJC,EAAAA,EAAAA,GAYM,MAZNC,EAYM,EAXJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAEM,MAFNa,EAEM,EADJb,EAAAA,EAAAA,GAAuE,OAAlEH,MAAM,Q,aAAQc,EAAAA,EAAAA,IAAQG,KAAKC,MAAsB,IAAhBT,EAAAC,MAAMS,QAAe,GAAK,M,uCAUxE,GACEJ,KAAM,MACNK,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UClBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Cpu.vue","webpack://platypush/./src/components/panels/Entities/Cpu.vue?2542"],"sourcesContent":["<template>\n <div class=\"entity cpu-container\">\n <div class=\"head\">\n <div class=\"col-1 icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <div class=\"value\" v-text=\"Math.round(value.percent * 100, 1) + '%'\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Cpu',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Cpu.vue?vue&type=template&id=d3cf6cca&scoped=true\"\nimport script from \"./Cpu.vue?vue&type=script&lang=js\"\nexport * from \"./Cpu.vue?vue&type=script&lang=js\"\n\nimport \"./Cpu.vue?vue&type=style&index=0&id=d3cf6cca&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-d3cf6cca\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","Math","round","percent","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/2460.6a8718df.js","mappings":"gMACOA,MAAM,wB,GACJA,MAAM,Q,GACJA,MAAM,c,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAcM,MAdNC,EAcM,EAbJC,EAAAA,EAAAA,GAYM,MAZNC,EAYM,EAXJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAEM,MAFNa,EAEM,EADJb,EAAAA,EAAAA,GAAuE,OAAlEH,MAAM,Q,aAAQc,EAAAA,EAAAA,IAAQG,KAAKC,MAAsB,IAAhBT,EAAAC,MAAMS,QAAe,GAAK,M,uCAUxE,GACEJ,KAAM,MACNK,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UClBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Cpu.vue","webpack://platypush/./src/components/panels/Entities/Cpu.vue?2542"],"sourcesContent":["<template>\n <div class=\"entity cpu-container\">\n <div class=\"head\">\n <div class=\"col-1 icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <div class=\"value\" v-text=\"Math.round(value.percent * 100, 1) + '%'\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Cpu',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Cpu.vue?vue&type=template&id=d3cf6cca&scoped=true\"\nimport script from \"./Cpu.vue?vue&type=script&lang=js\"\nexport * from \"./Cpu.vue?vue&type=script&lang=js\"\n\nimport \"./Cpu.vue?vue&type=style&index=0&id=d3cf6cca&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-d3cf6cca\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","Math","round","percent","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[2893,6362],{2893:function(e,t,n){n.r(t),n.d(t,{default:function(){return w}});var l=n(6252),a=n(3577);const u={class:"entity sensor-container"},s={class:"head"},i={class:"icon"},o={class:"label"},c=["textContent"],r={key:0,class:"value-container"},v=["textContent"],d=["textContent"];function p(e,t,n,p,y,m){const f=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",u,[(0,l._)("div",s,[(0,l._)("div",i,[(0,l.Wm)(f,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,l._)("div",o,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(e.value.name)},null,8,c)]),null!=e.value.value?((0,l.wg)(),(0,l.iD)("div",r,[null!=e.value.unit?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"unit",textContent:(0,a.zw)(e.value.unit)},null,8,v)):(0,l.kq)("",!0),(0,l._)("span",{class:"value",textContent:(0,a.zw)(m.displayValue(e.value.value))},null,8,d)])):(0,l.kq)("",!0)])])}var y=n(4967),m=n(6362),f={name:"EnumSensor",components:{EntityIcon:y["default"]},mixins:[m["default"]],methods:{displayValue(e){return this.value?.values&&"object"===typeof this.value.values&&this.value.values[e]||e}}},_=n(3744);const h=(0,_.Z)(f,[["render",p],["__scopeId","data-v-159d46fc"]]);var w=h},6362:function(e,t,n){n.r(t),n.d(t,{default:function(){return w}});var l=n(6252),a=n(3577);const u={class:"entity sensor-container"},s={class:"head"},i={class:"icon"},o={class:"label"},c=["textContent"],r={key:0,class:"value-container"},v=["textContent"],d=["textContent"];function p(e,t,n,p,y,m){const f=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",u,[(0,l._)("div",s,[(0,l._)("div",i,[(0,l.Wm)(f,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,l._)("div",o,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(e.value.name)},null,8,c)]),null!=m.computedValue?((0,l.wg)(),(0,l.iD)("div",r,[(0,l._)("span",{class:"value",textContent:(0,a.zw)(m.computedValue)},null,8,v),null!=e.value.unit?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"unit",textContent:(0,a.zw)(e.value.unit)},null,8,d)):(0,l.kq)("",!0)])):(0,l.kq)("",!0)])])}var y=n(847),m=n(4967),f={name:"Sensor",components:{EntityIcon:m["default"]},mixins:[y["default"]],computed:{computedValue(){return null!=this.value.value?this.value.value:this.value._value}}},_=n(3744);const h=(0,_.Z)(f,[["render",p],["__scopeId","data-v-3b38610c"]]);var w=h}}]);
//# sourceMappingURL=2893.519a1554.js.map
//# sourceMappingURL=2893.55e3bcf7.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[3368],{3368:function(e,l,t){t.r(l),t.d(l,{default:function(){return b}});var a=t(6252),s=t(3577),n=t(9963);const i={class:"entity switch-container"},u={class:"icon"},o={class:"label"},c=["textContent"],v={class:"value-container"},d=["textContent"],r={class:"row"},p={class:"input"},h=["disabled"],y={key:0,value:"",selected:""},g=["value","selected","textContent"];function w(e,l,t,w,f,_){const k=(0,a.up)("EntityIcon");return(0,a.wg)(),(0,a.iD)("div",i,[(0,a._)("div",{class:(0,s.C_)(["head",{collapsed:e.collapsed}])},[(0,a._)("div",u,[(0,a.Wm)(k,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,a._)("div",o,[(0,a._)("div",{class:"name",textContent:(0,s.zw)(e.value.name)},null,8,c)]),(0,a._)("div",v,[null!=e.value?.value?((0,a.wg)(),(0,a.iD)("span",{key:0,class:"value",textContent:(0,s.zw)(e.value.values[e.value.value]||e.value.value)},null,8,d)):(0,a.kq)("",!0),_.hasValues?((0,a.wg)(),(0,a.iD)("button",{key:1,onClick:l[0]||(l[0]=(0,n.iM)((l=>e.collapsed=!e.collapsed),["stop"]))},[(0,a._)("i",{class:(0,s.C_)(["fas",{"fa-angle-up":!e.collapsed,"fa-angle-down":e.collapsed}])},null,2)])):(0,a.kq)("",!0)])],2),e.collapsed?(0,a.kq)("",!0):((0,a.wg)(),(0,a.iD)("div",{key:0,class:"body",onClick:l[2]||(l[2]=(0,n.iM)(((...e)=>_.prevent&&_.prevent(...e)),["stop"]))},[(0,a._)("div",r,[(0,a._)("div",p,[(0,a._)("select",{onInput:l[1]||(l[1]=(...e)=>_.setValue&&_.setValue(...e)),ref:"values",disabled:e.loading},[e.value.is_write_only?((0,a.wg)(),(0,a.iD)("option",y,"--")):(0,a.kq)("",!0),((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)(_.displayValues,((l,t)=>((0,a.wg)(),(0,a.iD)("option",{value:t,selected:t==e.value.value,key:t,textContent:(0,s.zw)(l)},null,8,g)))),128))],40,h)])])]))])}var f=t(847),_=t(4967),k={name:"EnumSwitch",components:{EntityIcon:_["default"]},mixins:[f["default"]],computed:{hasValues(){return!!Object.values(this?.value?.values||{}).length},displayValues(){return this.value?.values instanceof Array?this.value.values.reduce(((e,l)=>(e[l]=l,e)),{}):this.value?.values||{}}},methods:{prevent(e){return e.stopPropagation(),!1},async setValue(e){if(e.target.value?.length){if(this.$emit("loading",!0),this.value.is_write_only){const e=this;setTimeout((()=>{e.$refs.values.value=""}),1e3)}try{await this.request("entities.execute",{id:this.value.id,action:"set",value:e.target.value})}finally{this.$emit("loading",!1)}}}}},m=t(3744);const C=(0,m.Z)(k,[["render",w],["__scopeId","data-v-043593ec"]]);var b=C}}]);
//# sourceMappingURL=3368.cb04738a.js.map
//# sourceMappingURL=3368.eda50aa5.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[3559],{3559:function(e,n,t){t.r(n),t.d(n,{default:function(){return k}});var l=t(6252),a=t(3577);const u={class:"entity link-quality-container"},i={class:"head"},s={class:"icon"},r={class:"label"},c=["textContent"],o={class:"value-container"},v=["textContent"];function d(e,n,t,d,p,f){const h=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",u,[(0,l._)("div",i,[(0,l._)("div",s,[(0,l.Wm)(h,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,l._)("div",r,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(e.value.name)},null,8,c)]),(0,l._)("div",o,[null!=f.valuePercent?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"value",textContent:(0,a.zw)(f.valuePercent+"%")},null,8,v)):(0,l.kq)("",!0)])])])}var p=t(847),f=t(4967),h={name:"LinkQuality",components:{EntityIcon:f["default"]},mixins:[p["default"]],computed:{valuePercent(){if(null==this.value?.value)return null;const e=this.value.min||0,n=this.value.max||100;return(100*this.value.value/(n-e)).toFixed(0)}}},y=t(3744);const m=(0,y.Z)(h,[["render",d],["__scopeId","data-v-66f207d9"]]);var k=m}}]);
//# sourceMappingURL=3559.df95d103.js.map
//# sourceMappingURL=3559.c2592048.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/3559.df95d103.js","mappings":"gMACOA,MAAM,iC,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAgBM,MAhBNC,EAgBM,EAfJC,EAAAA,EAAAA,GAcM,MAdNC,EAcM,EAbJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAIM,MAJNa,EAIM,CADoB,MAAhBC,EAAAC,eAAY,WAFpBjB,EAAAA,EAAAA,IAEgC,Q,MAF1BD,MAAM,Q,aACVc,EAAAA,EAAAA,IAAQG,EAAmBC,aAAJ,M,wDAWjC,GACEH,KAAM,cACNI,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRL,YAAAA,GACE,GAAyB,MAArBM,KAAKd,OAAOA,MACd,OAAO,KAET,MAAMe,EAAMD,KAAKd,MAAMe,KAAO,EACxBC,EAAMF,KAAKd,MAAMgB,KAAO,IAC9B,OAAS,IAAMF,KAAKd,MAAMA,OAAUgB,EAAMD,IAAME,QAAQ,EAC1D,I,UC9BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/LinkQuality.vue","webpack://platypush/./src/components/panels/Entities/LinkQuality.vue?19d2"],"sourcesContent":["<template>\n <div class=\"entity link-quality-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <span class=\"value\"\n v-text=\"valuePercent + '%'\"\n v-if=\"valuePercent != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'LinkQuality',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n valuePercent() {\n if (this.value?.value == null)\n return null\n\n const min = this.value.min || 0\n const max = this.value.max || 100\n return ((100 * this.value.value) / (max - min)).toFixed(0)\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./LinkQuality.vue?vue&type=template&id=66f207d9&scoped=true\"\nimport script from \"./LinkQuality.vue?vue&type=script&lang=js\"\nexport * from \"./LinkQuality.vue?vue&type=script&lang=js\"\n\nimport \"./LinkQuality.vue?vue&type=style&index=0&id=66f207d9&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-66f207d9\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","$options","valuePercent","components","EntityIcon","mixins","EntityMixin","computed","this","min","max","toFixed","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/3559.c2592048.js","mappings":"gMACOA,MAAM,iC,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAgBM,MAhBNC,EAgBM,EAfJC,EAAAA,EAAAA,GAcM,MAdNC,EAcM,EAbJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,aAGlCZ,EAAAA,EAAAA,GAIM,MAJNa,EAIM,CADoB,MAAhBC,EAAAC,eAAY,WAFpBjB,EAAAA,EAAAA,IAEgC,Q,MAF1BD,MAAM,Q,aACVc,EAAAA,EAAAA,IAAQG,EAAmBC,aAAJ,M,wDAWjC,GACEH,KAAM,cACNI,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRL,YAAAA,GACE,GAAyB,MAArBM,KAAKd,OAAOA,MACd,OAAO,KAET,MAAMe,EAAMD,KAAKd,MAAMe,KAAO,EACxBC,EAAMF,KAAKd,MAAMgB,KAAO,IAC9B,OAAS,IAAMF,KAAKd,MAAMA,OAAUgB,EAAMD,IAAME,QAAQ,EAC1D,I,UC9BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/LinkQuality.vue","webpack://platypush/./src/components/panels/Entities/LinkQuality.vue?19d2"],"sourcesContent":["<template>\n <div class=\"entity link-quality-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <span class=\"value\"\n v-text=\"valuePercent + '%'\"\n v-if=\"valuePercent != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'LinkQuality',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n valuePercent() {\n if (this.value?.value == null)\n return null\n\n const min = this.value.min || 0\n const max = this.value.max || 100\n return ((100 * this.value.value) / (max - min)).toFixed(0)\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./LinkQuality.vue?vue&type=template&id=66f207d9&scoped=true\"\nimport script from \"./LinkQuality.vue?vue&type=script&lang=js\"\nexport * from \"./LinkQuality.vue?vue&type=script&lang=js\"\n\nimport \"./LinkQuality.vue?vue&type=style&index=0&id=66f207d9&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-66f207d9\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","$options","valuePercent","components","EntityIcon","mixins","EntityMixin","computed","this","min","max","toFixed","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[3835],{3405:function(e,t,n){n.d(t,{Z:function(){return h}});var a=n(6252),i=n(3577),l=n(9963);const o=e=>((0,a.dD)("data-v-a6396ae8"),e=e(),(0,a.Cn)(),e),s=["checked"],c=o((()=>(0,a._)("div",{class:"switch"},[(0,a._)("div",{class:"dot"})],-1))),d={class:"label"};function u(e,t,n,o,u,r){return(0,a.wg)(),(0,a.iD)("div",{class:(0,i.C_)(["power-switch",{disabled:n.disabled}]),onClick:t[0]||(t[0]=(0,l.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,a._)("input",{type:"checkbox",checked:n.value},null,8,s),(0,a._)("label",null,[c,(0,a._)("span",d,[(0,a.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},v=n(3744);const p=(0,v.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var h=p},3835:function(e,t,n){n.r(t),n.d(t,{default:function(){return m}});var a=n(6252),i=n(3577),l=n(9963);const o={class:"entity device-container"},s={class:"head"},c={class:"icon"},d={class:"label"},u=["textContent"];function r(e,t,n,r,v,p){const h=(0,a.up)("EntityIcon"),f=(0,a.up)("ToggleSwitch");return(0,a.wg)(),(0,a.iD)("div",o,[(0,a._)("div",s,[(0,a._)("div",c,[(0,a.Wm)(h,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,a._)("div",d,[(0,a._)("div",{class:"name",textContent:(0,i.zw)(e.value.name)},null,8,u)]),(0,a._)("div",{class:(0,i.C_)(["value-container",{"with-children":e.value?.children_ids?.length}])},[(0,a.Wm)(f,{value:e.value.connected,disabled:e.loading,onInput:p.connect,onClick:t[0]||(t[0]=(0,l.iM)((()=>{}),["stop"]))},null,8,["value","disabled","onInput"])],2)])])}var v=n(847),p=n(4967),h=n(3405),f={name:"BluetoothDevice",components:{EntityIcon:p["default"],ToggleSwitch:h.Z},mixins:[v["default"]],methods:{async connect(e){e.stopPropagation(),this.$emit("loading",!0);const t="bluetooth."+(this.value.connected?"disconnect":"connect");try{await this.request(t,{device:this.value.address})}finally{this.$emit("loading",!1)}}}},_=n(3744);const g=(0,_.Z)(f,[["render",r],["__scopeId","data-v-6aff1eff"]]);var m=g}}]);
//# sourceMappingURL=3835.11129165.js.map
//# sourceMappingURL=3835.667ba911.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[5329],{5329:function(e,l,a){a.r(l),a.d(l,{default:function(){return le}});var s=a(6252),t=a(3577),n=a(9963);const i=e=>((0,s.dD)("data-v-d7813182"),e=e(),(0,s.Cn)(),e),v={class:"icon"},c={class:"label"},d=["textContent"],u={class:"value-and-toggler"},o=["textContent"],r={key:0,class:"body children attributes fade-in"},_={key:0,class:"child"},C=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Total")],-1))),m={class:"value"},k=["textContent"],h={key:1,class:"child"},p=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Available")],-1))),w={class:"value"},x=["textContent"],f={key:2,class:"child"},y=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Used")],-1))),z={class:"value"},b=["textContent"],g={key:3,class:"child"},D=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Free")],-1))),S={class:"value"},q=["textContent"],I={key:4,class:"child"},M=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Active")],-1))),A={class:"value"},E=["textContent"],B={key:5,class:"child"},F=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Inactive")],-1))),T={class:"value"},U=["textContent"],W={key:6,class:"child"},Z=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Buffers")],-1))),j={class:"value"},G=["textContent"],H={key:7,class:"child"},J=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Cached")],-1))),K={class:"value"},L=["textContent"],N={key:8,class:"child"},O=i((()=>(0,s._)("div",{class:"label"},[(0,s._)("div",{class:"name"},"Shared")],-1))),P={class:"value"},Q=["textContent"];function R(e,l,a,i,R,V){const X=(0,s.up)("EntityIcon");return(0,s.wg)(),(0,s.iD)("div",{class:(0,t.C_)(["entity memory-stats-container",{expanded:!R.isCollapsed}])},[(0,s._)("div",{class:"head",onClick:l[1]||(l[1]=(0,n.iM)((e=>R.isCollapsed=!R.isCollapsed),["stop"]))},[(0,s._)("div",v,[(0,s.Wm)(X,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,s._)("div",c,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.value.name)},null,8,d)]),(0,s._)("div",u,[(0,s._)("div",{class:"value",textContent:(0,t.zw)(Math.round(100*e.value.percent,1)+"%")},null,8,o),(0,s._)("div",{class:"collapse-toggler",onClick:l[0]||(l[0]=(0,n.iM)((e=>R.isCollapsed=!R.isCollapsed),["stop"]))},[(0,s._)("i",{class:(0,t.C_)(["fas",{"fa-chevron-down":R.isCollapsed,"fa-chevron-up":!R.isCollapsed}])},null,2)])])]),R.isCollapsed?(0,s.kq)("",!0):((0,s.wg)(),(0,s.iD)("div",r,[null!=e.value.total?((0,s.wg)(),(0,s.iD)("div",_,[C,(0,s._)("div",m,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.total))},null,8,k)])])):(0,s.kq)("",!0),null!=e.value.available?((0,s.wg)(),(0,s.iD)("div",h,[p,(0,s._)("div",w,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.available))},null,8,x)])])):(0,s.kq)("",!0),null!=e.value.used?((0,s.wg)(),(0,s.iD)("div",f,[y,(0,s._)("div",z,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.used))},null,8,b)])])):(0,s.kq)("",!0),null!=e.value.free?((0,s.wg)(),(0,s.iD)("div",g,[D,(0,s._)("div",S,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.free))},null,8,q)])])):(0,s.kq)("",!0),null!=e.value.active?((0,s.wg)(),(0,s.iD)("div",I,[M,(0,s._)("div",A,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.active))},null,8,E)])])):(0,s.kq)("",!0),null!=e.value.inactive?((0,s.wg)(),(0,s.iD)("div",B,[F,(0,s._)("div",T,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.inactive))},null,8,U)])])):(0,s.kq)("",!0),null!=e.value.buffers?((0,s.wg)(),(0,s.iD)("div",W,[Z,(0,s._)("div",j,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.buffers))},null,8,G)])])):(0,s.kq)("",!0),null!=e.value.cached?((0,s.wg)(),(0,s.iD)("div",H,[J,(0,s._)("div",K,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.cached))},null,8,L)])])):(0,s.kq)("",!0),null!=e.value.shared?((0,s.wg)(),(0,s.iD)("div",N,[O,(0,s._)("div",P,[(0,s._)("div",{class:"name",textContent:(0,t.zw)(e.convertSize(e.value.shared))},null,8,Q)])])):(0,s.kq)("",!0)]))],2)}var V=a(847),X=a(4967),Y={name:"MemoryStats",components:{EntityIcon:X["default"]},mixins:[V["default"]],data(){return{isCollapsed:!0}}},$=a(3744);const ee=(0,$.Z)(Y,[["render",R],["__scopeId","data-v-d7813182"]]);var le=ee}}]);
//# sourceMappingURL=5329.444a9cf1.js.map
//# sourceMappingURL=5329.114966f2.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[6362],{6362:function(t,e,n){n.r(e),n.d(e,{default:function(){return w}});var l=n(6252),a=n(3577);const u={class:"entity sensor-container"},s={class:"head"},i={class:"icon"},o={class:"label"},c=["textContent"],r={key:0,class:"value-container"},v=["textContent"],d=["textContent"];function p(t,e,n,p,m,h){const y=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",u,[(0,l._)("div",s,[(0,l._)("div",i,[(0,l.Wm)(y,{entity:t.value,loading:t.loading,error:t.error},null,8,["entity","loading","error"])]),(0,l._)("div",o,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(t.value.name)},null,8,c)]),null!=h.computedValue?((0,l.wg)(),(0,l.iD)("div",r,[(0,l._)("span",{class:"value",textContent:(0,a.zw)(h.computedValue)},null,8,v),null!=t.value.unit?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"unit",textContent:(0,a.zw)(t.value.unit)},null,8,d)):(0,l.kq)("",!0)])):(0,l.kq)("",!0)])])}var m=n(847),h=n(4967),y={name:"Sensor",components:{EntityIcon:h["default"]},mixins:[m["default"]],computed:{computedValue(){return null!=this.value.value?this.value.value:this.value._value}}},f=n(3744);const k=(0,f.Z)(y,[["render",p],["__scopeId","data-v-3b38610c"]]);var w=k}}]);
//# sourceMappingURL=6362.95da0eb4.js.map
//# sourceMappingURL=6362.c4de72d9.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/6362.95da0eb4.js","mappings":"gMACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,2BAINA,MAAM,mB,6GAVfC,EAAAA,EAAAA,IAiBM,MAjBNC,EAiBM,EAhBJC,EAAAA,EAAAA,GAeM,MAfNC,EAeM,EAdJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,YAIP,MAAjBC,EAAAC,gBAAa,WADvBhB,EAAAA,EAAAA,IAKM,MALNiB,EAKM,EAHJf,EAAAA,EAAAA,GAA6C,QAAvCH,MAAM,Q,aAAQc,EAAAA,EAAAA,IAAQE,EAAcC,gB,UAEpB,MAAdR,EAAAC,MAAMS,OAAI,WADlBlB,EAAAA,EAAAA,IAC8B,Q,MADxBD,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALS,O,yEAWzC,GACEJ,KAAM,SACNK,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRP,aAAAA,GACE,OAAwB,MAApBQ,KAAKf,MAAMA,MACNe,KAAKf,MAAMA,MACbe,KAAKf,MAAMgB,MACpB,I,UC5BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Sensor.vue","webpack://platypush/./src/components/panels/Entities/Sensor.vue?60a5"],"sourcesContent":["<template>\n <div class=\"entity sensor-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\"\n v-if=\"computedValue != null\">\n <span class=\"value\" v-text=\"computedValue\" />\n <span class=\"unit\" v-text=\"value.unit\"\n v-if=\"value.unit != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Sensor',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n computedValue() {\n if (this.value.value != null)\n return this.value.value\n return this.value._value\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Sensor.vue?vue&type=template&id=3b38610c&scoped=true\"\nimport script from \"./Sensor.vue?vue&type=script&lang=js\"\nexport * from \"./Sensor.vue?vue&type=script&lang=js\"\n\nimport \"./Sensor.vue?vue&type=style&index=0&id=3b38610c&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-3b38610c\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","$options","computedValue","_hoisted_6","unit","components","EntityIcon","mixins","EntityMixin","computed","this","_value","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/6362.c4de72d9.js","mappings":"gMACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,2BAINA,MAAM,mB,6GAVfC,EAAAA,EAAAA,IAiBM,MAjBNC,EAiBM,EAhBJC,EAAAA,EAAAA,GAeM,MAfNC,EAeM,EAdJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAAgEC,EAAA,CAAnDC,OAAQC,EAAAC,MAAQC,QAASF,EAAAE,QAAUC,MAAOH,EAAAG,O,wCAGzDT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,YAIP,MAAjBC,EAAAC,gBAAa,WADvBhB,EAAAA,EAAAA,IAKM,MALNiB,EAKM,EAHJf,EAAAA,EAAAA,GAA6C,QAAvCH,MAAM,Q,aAAQc,EAAAA,EAAAA,IAAQE,EAAcC,gB,UAEpB,MAAdR,EAAAC,MAAMS,OAAI,WADlBlB,EAAAA,EAAAA,IAC8B,Q,MADxBD,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALS,O,yEAWzC,GACEJ,KAAM,SACNK,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRP,aAAAA,GACE,OAAwB,MAApBQ,KAAKf,MAAMA,MACNe,KAAKf,MAAMA,MACbe,KAAKf,MAAMgB,MACpB,I,UC5BJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Sensor.vue","webpack://platypush/./src/components/panels/Entities/Sensor.vue?60a5"],"sourcesContent":["<template>\n <div class=\"entity sensor-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\"\n v-if=\"computedValue != null\">\n <span class=\"value\" v-text=\"computedValue\" />\n <span class=\"unit\" v-text=\"value.unit\"\n v-if=\"value.unit != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Sensor',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n computedValue() {\n if (this.value.value != null)\n return this.value.value\n return this.value._value\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Sensor.vue?vue&type=template&id=3b38610c&scoped=true\"\nimport script from \"./Sensor.vue?vue&type=script&lang=js\"\nexport * from \"./Sensor.vue?vue&type=script&lang=js\"\n\nimport \"./Sensor.vue?vue&type=style&index=0&id=3b38610c&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-3b38610c\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","$options","computedValue","_hoisted_6","unit","components","EntityIcon","mixins","EntityMixin","computed","this","_value","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[7523],{4358:function(e,t,a){a.d(t,{Z:function(){return w}});var l=a(6252),n=a(3577),s=a(9963);const i={class:"slider-wrapper"},u=["textContent"],r=["textContent"],o={class:"slider-container"},d=["min","max","step","disabled","value"],p={class:"track-inner",ref:"track"},c={class:"thumb",ref:"thumb"},v=["textContent"];function h(e,t,a,h,g,f){return(0,l.wg)(),(0,l.iD)("label",i,[a.withRange?((0,l.wg)(),(0,l.iD)("span",{key:0,class:(0,n.C_)(["range-labels",{"with-label":a.withLabel}])},[a.withRange?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"label left",textContent:(0,n.zw)(a.range[0])},null,8,u)):(0,l.kq)("",!0),a.withRange?((0,l.wg)(),(0,l.iD)("span",{key:1,class:"label right",textContent:(0,n.zw)(a.range[1])},null,8,r)):(0,l.kq)("",!0)],2)):(0,l.kq)("",!0),(0,l._)("span",o,[(0,l._)("input",{class:(0,n.C_)(["slider",{"with-label":a.withLabel}]),type:"range",min:a.range[0],max:a.range[1],step:a.step,disabled:a.disabled,value:a.value,ref:"range",onInput:t[0]||(t[0]=(0,s.iM)(((...e)=>f.onUpdate&&f.onUpdate(...e)),["stop"])),onChange:t[1]||(t[1]=(0,s.iM)(((...e)=>f.onUpdate&&f.onUpdate(...e)),["stop"]))},null,42,d),(0,l._)("div",{class:(0,n.C_)(["track",{"with-label":a.withLabel}])},[(0,l._)("div",p,null,512)],2),(0,l._)("div",c,null,512),a.withLabel?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"label",textContent:(0,n.zw)(a.value),ref:"label"},null,8,v)):(0,l.kq)("",!0)])])}var g={name:"Slider",emits:["input","change","mouseup","mousedown","touchstart","touchend","keyup","keydown"],props:{value:{type:Number},disabled:{type:Boolean,default:!1},range:{type:Array,default:()=>[0,100]},step:{type:Number,default:1},withLabel:{type:Boolean,default:!1},withRange:{type:Boolean,default:!1}},methods:{onUpdate(e){this.update(e.target.value),this.$emit(e.type,{...e,target:{...e.target,value:this.$refs.range.value}})},update(e){const t=this.$refs.range.clientWidth,a=(e-this.range[0])/(this.range[1]-this.range[0]),l=a*t,n=this.$refs.thumb;n.style.left=l-n.clientWidth/2+"px",this.$refs.thumb.style.transform=`translate(-${a}%, -50%)`,this.$refs.track.style.width=`${l}px`}},mounted(){null!=this.value&&this.update(this.value),this.$watch((()=>this.value),(e=>this.update(e)))}},f=a(3744);const m=(0,f.Z)(g,[["render",h],["__scopeId","data-v-4b38623f"]]);var w=m},7523:function(e,t,a){a.r(t),a.d(t,{default:function(){return V}});var l=a(6252),n=a(3577),s=a(9963);const i={class:"entity dimmer-container"},u={class:"icon"},r={class:"label"},o=["textContent"],d={class:"value-container pull-right"},p=["textContent"],c={class:"row"},v={key:0,class:"input"},h={class:"col-10"},g={class:"col-2 value"},f=["value"],m={key:1,class:"input"},w={class:"col-12 value"},y=["value"];function b(e,t,a,b,_,k){const C=(0,l.up)("EntityIcon"),x=(0,l.up)("Slider");return(0,l.wg)(),(0,l.iD)("div",i,[(0,l._)("div",{class:(0,n.C_)(["head",{collapsed:e.collapsed}])},[(0,l._)("div",u,[(0,l.Wm)(C,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,l._)("div",r,[(0,l._)("div",{class:"name",textContent:(0,n.zw)(e.value.name)},null,8,o)]),(0,l._)("div",d,[null!=k.parsedValue?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"value",textContent:(0,n.zw)(k.parsedValue)},null,8,p)):(0,l.kq)("",!0),(0,l._)("button",{onClick:t[0]||(t[0]=(0,s.iM)((t=>e.collapsed=!e.collapsed),["stop"]))},[(0,l._)("i",{class:(0,n.C_)(["fas",{"fa-angle-up":!e.collapsed,"fa-angle-down":e.collapsed}])},null,2)])])],2),e.collapsed?(0,l.kq)("",!0):((0,l.wg)(),(0,l.iD)("div",{key:0,class:"body",onClick:t[3]||(t[3]=(0,s.iM)(((...e)=>k.prevent&&k.prevent(...e)),["stop"]))},[(0,l._)("div",c,[null!=e.value?.min&&null!=e.value?.max?((0,l.wg)(),(0,l.iD)("div",v,[(0,l._)("div",h,[(0,l.Wm)(x,{range:[e.value.min,e.value.max],"with-range":"",value:e.value.value,onInput:k.setValue},null,8,["range","value","onInput"])]),(0,l._)("div",g,[(0,l._)("input",{type:"number",value:e.value.value,onChange:t[1]||(t[1]=(...e)=>k.setValue&&k.setValue(...e))},null,40,f)])])):((0,l.wg)(),(0,l.iD)("div",m,[(0,l._)("div",w,[(0,l._)("input",{type:"number",value:e.value.value,onChange:t[2]||(t[2]=(...e)=>k.setValue&&k.setValue(...e))},null,40,y)])]))])]))])}var _=a(4358),k=a(847),C=a(4967),x={name:"Dimmer",components:{Slider:_.Z,EntityIcon:C["default"]},mixins:[k["default"]],computed:{parsedValue(){if(this.value?.is_write_only||null==this.value?.value)return null;let e=this.value.value;return this.value.unit&&(e=`${e} ${this.value.unit}`),e}},methods:{prevent(e){return e.stopPropagation(),!1},async setValue(e){if(e.target.value?.length){this.$emit("loading",!0);try{await this.request("entities.execute",{id:this.value.id,action:"set",value:+e.target.value})}finally{this.$emit("loading",!1)}}}}},$=a(3744);const D=(0,$.Z)(x,[["render",b],["__scopeId","data-v-3affff53"]]);var V=D}}]);
//# sourceMappingURL=7523.367c2045.js.map
//# sourceMappingURL=7523.5fed230e.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[7590],{7590:function(e,t,n){n.r(t),n.d(t,{default:function(){return b}});var l=n(6252),a=n(3577);const o={class:"entity battery-container"},s={class:"head"},r={class:"icon"},c={class:"label"},u=["textContent"],i={class:"value-container"},v=["textContent"];function d(e,t,n,d,f,p){const C=(0,l.up)("EntityIcon");return(0,l.wg)(),(0,l.iD)("div",o,[(0,l._)("div",s,[(0,l._)("div",r,[(0,l.Wm)(C,{entity:e.value,icon:p.icon,loading:e.loading,error:e.error},null,8,["entity","icon","loading","error"])]),(0,l._)("div",c,[(0,l._)("div",{class:"name",textContent:(0,a.zw)(e.value.name)},null,8,u)]),(0,l._)("div",i,[null!=p.valuePercent?((0,l.wg)(),(0,l.iD)("span",{key:0,class:"value",textContent:(0,a.zw)(p.valuePercent+"%")},null,8,v)):(0,l.kq)("",!0)])])])}var f=n(847),p=n(4967);const C=[{iconClass:"full",color:"#157145",value:.9},{iconClass:"three-quarters",color:"#94C595",value:.825},{iconClass:"half",color:"#F0B67F",value:.625},{iconClass:"quarter",color:"#FE5F55",value:.375},{iconClass:"low",color:"#CC444B",value:.15},{iconClass:"empty",color:"#EC0B43",value:.05}];var h={name:"Battery",components:{EntityIcon:p["default"]},mixins:[f["default"]],computed:{valuePercent(){if(null==this.value?.value)return null;const e=this.value.min||0,t=this.value.max||100;return(100*this.value.value/(t-e)).toFixed(0)},icon(){const e={...this.value.meta?.icon||{}};let t=this.valuePercent,n=C[0];if(null!=t){t=parseFloat(t)/100;for(const e of C){if(t>e.value)break;n=e}}return e["class"]=`fas fa-battery-${n.iconClass}`,e["color"]=n.color,e}},methods:{prevent(e){return e.stopPropagation(),!1}}},m=n(3744);const y=(0,m.Z)(h,[["render",d],["__scopeId","data-v-4b2ced66"]]);var b=y}}]);
//# sourceMappingURL=7590.6cda174b.js.map
//# sourceMappingURL=7590.ebe62444.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/7590.6cda174b.js","mappings":"gMACOA,MAAM,4B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAcM,MAdNC,EAcM,EAbJC,EAAAA,EAAAA,GAYM,MAZNC,EAYM,EAXJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAA6EC,EAAA,CAAhEC,OAAQC,EAAAC,MAAQC,KAAMC,EAAAD,KAAOE,QAASJ,EAAAI,QAAUC,MAAOL,EAAAK,O,+CAGtEX,EAAAA,EAAAA,GAEM,MAFNY,EAEM,EADJZ,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOgB,EAAAA,EAAAA,IAAQP,EAAWC,MAALO,O,aAGlCd,EAAAA,EAAAA,GAEM,MAFNe,EAEM,CADkE,MAAhBN,EAAAO,eAAY,WAAlElB,EAAAA,EAAAA,IAA8E,Q,MAAxED,MAAM,Q,aAAQgB,EAAAA,EAAAA,IAAQJ,EAAmBO,aAAJ,M,wDAUnD,MAAMC,EAAa,CACjB,CACEC,UAAW,OACXC,MAAO,UACPZ,MAAO,IAET,CACEW,UAAW,iBACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,OACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,UACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,MACXC,MAAO,UACPZ,MAAO,KAET,CACEW,UAAW,QACXC,MAAO,UACPZ,MAAO,MAIX,OACEO,KAAM,UACNM,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRR,YAAAA,GACE,GAAyB,MAArBS,KAAKlB,OAAOA,MACd,OAAO,KAET,MAAMmB,EAAMD,KAAKlB,MAAMmB,KAAO,EACxBC,EAAMF,KAAKlB,MAAMoB,KAAO,IAC9B,OAAS,IAAMF,KAAKlB,MAAMA,OAAUoB,EAAMD,IAAME,QAAQ,EAC1D,EAEApB,IAAAA,GACE,MAAMA,EAAO,IAAKiB,KAAKlB,MAAMsB,MAAMrB,MAAQ,CAAC,GAC5C,IAAID,EAAQkB,KAAKT,aACbc,EAAYb,EAAW,GAE3B,GAAa,MAATV,EAAe,CACjBA,EAAQwB,WAAWxB,GAAS,IAC5B,IAAK,MAAMyB,KAAKf,EAAY,CAC1B,GAAIV,EAAQyB,EAAEzB,MACZ,MACFuB,EAAYE,CACd,CACF,CAIA,OAFAxB,EAAK,SAAY,kBAAiBsB,EAAUZ,YAC5CV,EAAK,SAAWsB,EAAUX,MACnBX,CACT,GAGFyB,QAAS,CACPC,OAAAA,CAAQC,GAEN,OADAA,EAAMC,mBACC,CACT,I,UCvFJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Battery.vue","webpack://platypush/./src/components/panels/Entities/Battery.vue?1b53"],"sourcesContent":["<template>\n <div class=\"entity battery-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :icon=\"icon\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <span class=\"value\" v-text=\"valuePercent + '%'\" v-if=\"valuePercent != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nconst thresholds = [\n {\n iconClass: 'full',\n color: '#157145',\n value: 0.9,\n },\n {\n iconClass: 'three-quarters',\n color: '#94C595',\n value: 0.825,\n },\n {\n iconClass: 'half',\n color: '#F0B67F',\n value: 0.625,\n },\n {\n iconClass: 'quarter',\n color: '#FE5F55',\n value: 0.375,\n },\n {\n iconClass: 'low',\n color: '#CC444B',\n value: 0.15,\n },\n {\n iconClass: 'empty',\n color: '#EC0B43',\n value: 0.05,\n },\n]\n\nexport default {\n name: 'Battery',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n valuePercent() {\n if (this.value?.value == null)\n return null\n\n const min = this.value.min || 0\n const max = this.value.max || 100\n return ((100 * this.value.value) / (max - min)).toFixed(0)\n },\n\n icon() {\n const icon = {...(this.value.meta?.icon || {})}\n let value = this.valuePercent\n let threshold = thresholds[0]\n\n if (value != null) {\n value = parseFloat(value) / 100\n for (const t of thresholds) {\n if (value > t.value)\n break\n threshold = t\n }\n }\n\n icon['class'] = `fas fa-battery-${threshold.iconClass}`\n icon['color'] = threshold.color\n return icon\n },\n },\n\n methods: {\n prevent(event) {\n event.stopPropagation()\n return false\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Battery.vue?vue&type=template&id=4b2ced66&scoped=true\"\nimport script from \"./Battery.vue?vue&type=script&lang=js\"\nexport * from \"./Battery.vue?vue&type=script&lang=js\"\n\nimport \"./Battery.vue?vue&type=style&index=0&id=4b2ced66&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-4b2ced66\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","icon","$options","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","valuePercent","thresholds","iconClass","color","components","EntityIcon","mixins","EntityMixin","computed","this","min","max","toFixed","meta","threshold","parseFloat","t","methods","prevent","event","stopPropagation","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/7590.ebe62444.js","mappings":"gMACOA,MAAM,4B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAINA,MAAM,S,qBAINA,MAAM,mB,2FAVfC,EAAAA,EAAAA,IAcM,MAdNC,EAcM,EAbJC,EAAAA,EAAAA,GAYM,MAZNC,EAYM,EAXJD,EAAAA,EAAAA,GAEM,MAFNE,EAEM,EADJC,EAAAA,EAAAA,IAA6EC,EAAA,CAAhEC,OAAQC,EAAAC,MAAQC,KAAMC,EAAAD,KAAOE,QAASJ,EAAAI,QAAUC,MAAOL,EAAAK,O,+CAGtEX,EAAAA,EAAAA,GAEM,MAFNY,EAEM,EADJZ,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOgB,EAAAA,EAAAA,IAAQP,EAAWC,MAALO,O,aAGlCd,EAAAA,EAAAA,GAEM,MAFNe,EAEM,CADkE,MAAhBN,EAAAO,eAAY,WAAlElB,EAAAA,EAAAA,IAA8E,Q,MAAxED,MAAM,Q,aAAQgB,EAAAA,EAAAA,IAAQJ,EAAmBO,aAAJ,M,wDAUnD,MAAMC,EAAa,CACjB,CACEC,UAAW,OACXC,MAAO,UACPZ,MAAO,IAET,CACEW,UAAW,iBACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,OACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,UACXC,MAAO,UACPZ,MAAO,MAET,CACEW,UAAW,MACXC,MAAO,UACPZ,MAAO,KAET,CACEW,UAAW,QACXC,MAAO,UACPZ,MAAO,MAIX,OACEO,KAAM,UACNM,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,YAETC,SAAU,CACRR,YAAAA,GACE,GAAyB,MAArBS,KAAKlB,OAAOA,MACd,OAAO,KAET,MAAMmB,EAAMD,KAAKlB,MAAMmB,KAAO,EACxBC,EAAMF,KAAKlB,MAAMoB,KAAO,IAC9B,OAAS,IAAMF,KAAKlB,MAAMA,OAAUoB,EAAMD,IAAME,QAAQ,EAC1D,EAEApB,IAAAA,GACE,MAAMA,EAAO,IAAKiB,KAAKlB,MAAMsB,MAAMrB,MAAQ,CAAC,GAC5C,IAAID,EAAQkB,KAAKT,aACbc,EAAYb,EAAW,GAE3B,GAAa,MAATV,EAAe,CACjBA,EAAQwB,WAAWxB,GAAS,IAC5B,IAAK,MAAMyB,KAAKf,EAAY,CAC1B,GAAIV,EAAQyB,EAAEzB,MACZ,MACFuB,EAAYE,CACd,CACF,CAIA,OAFAxB,EAAK,SAAY,kBAAiBsB,EAAUZ,YAC5CV,EAAK,SAAWsB,EAAUX,MACnBX,CACT,GAGFyB,QAAS,CACPC,OAAAA,CAAQC,GAEN,OADAA,EAAMC,mBACC,CACT,I,UCvFJ,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Battery.vue","webpack://platypush/./src/components/panels/Entities/Battery.vue?1b53"],"sourcesContent":["<template>\n <div class=\"entity battery-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon :entity=\"value\" :icon=\"icon\" :loading=\"loading\" :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n\n <div class=\"value-container\">\n <span class=\"value\" v-text=\"valuePercent + '%'\" v-if=\"valuePercent != null\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nconst thresholds = [\n {\n iconClass: 'full',\n color: '#157145',\n value: 0.9,\n },\n {\n iconClass: 'three-quarters',\n color: '#94C595',\n value: 0.825,\n },\n {\n iconClass: 'half',\n color: '#F0B67F',\n value: 0.625,\n },\n {\n iconClass: 'quarter',\n color: '#FE5F55',\n value: 0.375,\n },\n {\n iconClass: 'low',\n color: '#CC444B',\n value: 0.15,\n },\n {\n iconClass: 'empty',\n color: '#EC0B43',\n value: 0.05,\n },\n]\n\nexport default {\n name: 'Battery',\n components: {EntityIcon},\n mixins: [EntityMixin],\n\n computed: {\n valuePercent() {\n if (this.value?.value == null)\n return null\n\n const min = this.value.min || 0\n const max = this.value.max || 100\n return ((100 * this.value.value) / (max - min)).toFixed(0)\n },\n\n icon() {\n const icon = {...(this.value.meta?.icon || {})}\n let value = this.valuePercent\n let threshold = thresholds[0]\n\n if (value != null) {\n value = parseFloat(value) / 100\n for (const t of thresholds) {\n if (value > t.value)\n break\n threshold = t\n }\n }\n\n icon['class'] = `fas fa-battery-${threshold.iconClass}`\n icon['color'] = threshold.color\n return icon\n },\n },\n\n methods: {\n prevent(event) {\n event.stopPropagation()\n return false\n },\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Battery.vue?vue&type=template&id=4b2ced66&scoped=true\"\nimport script from \"./Battery.vue?vue&type=script&lang=js\"\nexport * from \"./Battery.vue?vue&type=script&lang=js\"\n\nimport \"./Battery.vue?vue&type=style&index=0&id=4b2ced66&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-4b2ced66\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","icon","$options","loading","error","_hoisted_4","_toDisplayString","name","_hoisted_6","valuePercent","thresholds","iconClass","color","components","EntityIcon","mixins","EntityMixin","computed","this","min","max","toFixed","meta","threshold","parseFloat","t","methods","prevent","event","stopPropagation","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[8391],{3405:function(t,e,a){a.d(e,{Z:function(){return h}});var n=a(6252),i=a(3577),l=a(9963);const s=t=>((0,n.dD)("data-v-a6396ae8"),t=t(),(0,n.Cn)(),t),o=["checked"],u=s((()=>(0,n._)("div",{class:"switch"},[(0,n._)("div",{class:"dot"})],-1))),c={class:"label"};function d(t,e,a,s,d,r){return(0,n.wg)(),(0,n.iD)("div",{class:(0,i.C_)(["power-switch",{disabled:a.disabled}]),onClick:e[0]||(e[0]=(0,l.iM)(((...t)=>r.onInput&&r.onInput(...t)),["stop"]))},[(0,n._)("input",{type:"checkbox",checked:a.value},null,8,o),(0,n._)("label",null,[u,(0,n._)("span",c,[(0,n.WI)(t.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(t){if(this.disabled)return!1;this.$emit("input",t)}}},p=a(3744);const v=(0,p.Z)(r,[["render",d],["__scopeId","data-v-a6396ae8"]]);var h=v},8391:function(t,e,a){a.r(e),a.d(e,{default:function(){return y}});var n=a(6252),i=a(3577),l=a(9963);const s={class:"entity switch-container"},o={class:"head"},u={class:"col-1 icon"},c={class:"col-9 label"},d=["textContent"],r={class:"col-2 switch pull-right"};function p(t,e,a,p,v,h){const g=(0,n.up)("EntityIcon"),_=(0,n.up)("ToggleSwitch");return(0,n.wg)(),(0,n.iD)("div",s,[(0,n._)("div",o,[(0,n._)("div",u,[(0,n.Wm)(g,{entity:t.value,loading:t.loading,error:t.error},null,8,["entity","loading","error"])]),(0,n._)("div",c,[(0,n._)("div",{class:"name",textContent:(0,i.zw)(t.value.name)},null,8,d)]),(0,n._)("div",r,[(0,n.Wm)(_,{value:!t.value.is_write_only&&t.value.state,disabled:t.loading||t.value.is_read_only,onInput:h.toggle,onClick:e[0]||(e[0]=(0,l.iM)((()=>{}),["stop"]))},null,8,["value","disabled","onInput"])])])])}var v=a(3405),h=a(4967),g=a(847),_={name:"Switch",components:{ToggleSwitch:v.Z,EntityIcon:h["default"]},mixins:[g["default"]],methods:{async toggle(t){t.stopPropagation(),this.$emit("loading",!0);try{if(await this.request("entities.execute",{id:this.value.id,action:"toggle"}),this.value.is_write_only){const t=this;t.value.state=!0,setTimeout((()=>t.value.state=!1),250)}}finally{this.$emit("loading",!1)}}}},f=a(3744);const w=(0,f.Z)(_,[["render",p],["__scopeId","data-v-2aaabd26"]]);var y=w}}]);
//# sourceMappingURL=8391.119357c7.js.map
//# sourceMappingURL=8391.16e30eb1.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[8621],{3405:function(e,n,t){t.d(n,{Z:function(){return h}});var a=t(6252),l=t(3577),s=t(9963);const i=e=>((0,a.dD)("data-v-a6396ae8"),e=e(),(0,a.Cn)(),e),o=["checked"],u=i((()=>(0,a._)("div",{class:"switch"},[(0,a._)("div",{class:"dot"})],-1))),c={class:"label"};function d(e,n,t,i,d,r){return(0,a.wg)(),(0,a.iD)("div",{class:(0,l.C_)(["power-switch",{disabled:t.disabled}]),onClick:n[0]||(n[0]=(0,s.iM)(((...e)=>r.onInput&&r.onInput(...e)),["stop"]))},[(0,a._)("input",{type:"checkbox",checked:t.value},null,8,o),(0,a._)("label",null,[u,(0,a._)("span",c,[(0,a.WI)(e.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(e){if(this.disabled)return!1;this.$emit("input",e)}}},v=t(3744);const p=(0,v.Z)(r,[["render",d],["__scopeId","data-v-a6396ae8"]]);var h=p},8621:function(e,n,t){t.r(n),t.d(n,{default:function(){return g}});var a=t(6252),l=t(3577);const s={class:"entity sensor-container"},i={class:"head"},o={class:"icon"},u={class:"label"},c=["textContent"],d={key:0,class:"value-container"};function r(e,n,t,r,v,p){const h=(0,a.up)("EntityIcon"),f=(0,a.up)("ToggleSwitch");return(0,a.wg)(),(0,a.iD)("div",s,[(0,a._)("div",i,[(0,a._)("div",o,[(0,a.Wm)(h,{entity:e.value,loading:e.loading,error:e.error},null,8,["entity","loading","error"])]),(0,a._)("div",u,[(0,a._)("div",{class:"name",textContent:(0,l.zw)(e.value.name)},null,8,c)]),null!=e.value.value?((0,a.wg)(),(0,a.iD)("div",d,[(0,a.Wm)(f,{value:e.value.value,disabled:!0},null,8,["value"])])):(0,a.kq)("",!0)])])}var v=t(847),p=t(4967),h=t(3405),f={name:"BinarySensor",components:{EntityIcon:p["default"],ToggleSwitch:h.Z},mixins:[v["default"]]},b=t(3744);const _=(0,b.Z)(f,[["render",r],["__scopeId","data-v-8baaebb4"]]);var g=_}}]);
//# sourceMappingURL=8621.0aa03df1.js.map
//# sourceMappingURL=8621.33df9b41.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[8769],{8769:function(n,e,t){t.r(e),t.d(e,{default:function(){return h}});var a=t(6252),i=t(3577);const c={class:"entity device-container"},l={class:"head"},s={class:"icon"},o={class:"label"},r=["textContent"];function u(n,e,t,u,d,v){const p=(0,a.up)("EntityIcon");return(0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",l,[(0,a._)("div",s,[(0,a.Wm)(p,{entity:n.value,loading:n.loading,error:n.error},null,8,["entity","loading","error"])]),(0,a._)("div",o,[(0,a._)("div",{class:"name",textContent:(0,i.zw)(n.value.name)},null,8,r)])])])}var d=t(847),v=t(4967),p={name:"Device",components:{EntityIcon:v["default"]},mixins:[d["default"]]},f=t(3744);const y=(0,f.Z)(p,[["render",u],["__scopeId","data-v-07323f6c"]]);var h=y}}]);
//# sourceMappingURL=8769.5ea5c0cb.js.map
//# sourceMappingURL=8769.02eed3a9.js.map

View file

@ -1 +1 @@
{"version":3,"file":"static/js/8769.5ea5c0cb.js","mappings":"gMACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAONA,MAAM,S,2FATfC,EAAAA,EAAAA,IAaM,MAbNC,EAaM,EAZJC,EAAAA,EAAAA,GAWM,MAXNC,EAWM,EAVJD,EAAAA,EAAAA,GAKM,MALNE,EAKM,EAJJC,EAAAA,EAAAA,IAGmBC,EAAA,CAFhBC,OAAQC,EAAAC,MACRC,QAASF,EAAAE,QACTC,MAAOH,EAAAG,O,wCAGZT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,uCAUxC,GACEA,KAAM,SACNC,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UCjBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Device.vue","webpack://platypush/./src/components/panels/Entities/Device.vue?1785"],"sourcesContent":["<template>\n <div class=\"entity device-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon\n :entity=\"value\"\n :loading=\"loading\"\n :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Device',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Device.vue?vue&type=template&id=07323f6c&scoped=true\"\nimport script from \"./Device.vue?vue&type=script&lang=js\"\nexport * from \"./Device.vue?vue&type=script&lang=js\"\n\nimport \"./Device.vue?vue&type=style&index=0&id=07323f6c&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-07323f6c\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}
{"version":3,"file":"static/js/8769.02eed3a9.js","mappings":"gMACOA,MAAM,2B,GACJA,MAAM,Q,GACJA,MAAM,Q,GAONA,MAAM,S,2FATfC,EAAAA,EAAAA,IAaM,MAbNC,EAaM,EAZJC,EAAAA,EAAAA,GAWM,MAXNC,EAWM,EAVJD,EAAAA,EAAAA,GAKM,MALNE,EAKM,EAJJC,EAAAA,EAAAA,IAGmBC,EAAA,CAFhBC,OAAQC,EAAAC,MACRC,QAASF,EAAAE,QACTC,MAAOH,EAAAG,O,wCAGZT,EAAAA,EAAAA,GAEM,MAFNU,EAEM,EADJV,EAAAA,EAAAA,GAAwC,OAAnCH,MAAM,O,aAAOc,EAAAA,EAAAA,IAAQL,EAAWC,MAALK,O,uCAUxC,GACEA,KAAM,SACNC,WAAY,CAACC,WAAUA,EAAAA,YACvBC,OAAQ,CAACC,EAAAA,a,UCjBX,MAAMC,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/panels/Entities/Device.vue","webpack://platypush/./src/components/panels/Entities/Device.vue?1785"],"sourcesContent":["<template>\n <div class=\"entity device-container\">\n <div class=\"head\">\n <div class=\"icon\">\n <EntityIcon\n :entity=\"value\"\n :loading=\"loading\"\n :error=\"error\" />\n </div>\n\n <div class=\"label\">\n <div class=\"name\" v-text=\"value.name\" />\n </div>\n </div>\n </div>\n</template>\n\n<script>\nimport EntityMixin from \"./EntityMixin\"\nimport EntityIcon from \"./EntityIcon\"\n\nexport default {\n name: 'Device',\n components: {EntityIcon},\n mixins: [EntityMixin],\n}\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"common\";\n</style>\n","import { render } from \"./Device.vue?vue&type=template&id=07323f6c&scoped=true\"\nimport script from \"./Device.vue?vue&type=script&lang=js\"\nexport * from \"./Device.vue?vue&type=script&lang=js\"\n\nimport \"./Device.vue?vue&type=style&index=0&id=07323f6c&lang=scss&scoped=true\"\n\nimport exportComponent from \"../../../../node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-07323f6c\"]])\n\nexport default __exports__"],"names":["class","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_component_EntityIcon","entity","_ctx","value","loading","error","_hoisted_4","_toDisplayString","name","components","EntityIcon","mixins","EntityMixin","__exports__","render"],"sourceRoot":""}

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[9624],{9624:function(l,e,a){a.r(e),a.d(e,{default:function(){return D}});var t=a(6252),s=a(3577),i=a(9963);const n=l=>((0,t.dD)("data-v-c4b6a144"),l=l(),(0,t.Cn)(),l),o={key:0,class:"entity variable-container"},u={class:"icon"},d={class:"label"},c=["textContent"],r=["textContent"],v={class:"row"},p={class:"row"},h={class:"col-9"},_=["disabled"],b={class:"col-3 pull-right"},f=["disabled"],m=n((()=>(0,t._)("i",{class:"fas fa-trash"},null,-1))),y=[m],g=["disabled"],k=n((()=>(0,t._)("i",{class:"fas fa-check"},null,-1))),w=[k];function C(l,e,a,n,m,k){const C=(0,t.up)("EntityIcon");return null!=l.value.value?((0,t.wg)(),(0,t.iD)("div",o,[(0,t._)("div",{class:(0,s.C_)(["head",{collapsed:l.collapsed}])},[(0,t._)("div",u,[(0,t.Wm)(C,{entity:l.value,loading:l.loading,error:l.error},null,8,["entity","loading","error"])]),(0,t._)("div",d,[(0,t._)("div",{class:"name",textContent:(0,s.zw)(l.value.name)},null,8,c)]),(0,t._)("div",{class:"value-and-toggler",onClick:e[1]||(e[1]=(0,i.iM)((e=>l.collapsed=!l.collapsed),["stop"]))},[null!=l.value?.value?((0,t.wg)(),(0,t.iD)("div",{key:0,class:"value",textContent:(0,s.zw)(l.value.value)},null,8,r)):(0,t.kq)("",!0),(0,t._)("div",{class:"collapse-toggler",onClick:e[0]||(e[0]=(0,i.iM)((e=>l.collapsed=!l.collapsed),["stop"]))},[(0,t._)("i",{class:(0,s.C_)(["fas",{"fa-chevron-down":l.collapsed,"fa-chevron-up":!l.collapsed}])},null,2)])])],2),l.collapsed?(0,t.kq)("",!0):((0,t.wg)(),(0,t.iD)("div",{key:0,class:"body",onClick:e[5]||(e[5]=(0,i.iM)(((...e)=>l.prevent&&l.prevent(...e)),["stop"]))},[(0,t._)("div",v,[(0,t._)("form",{onSubmit:e[4]||(e[4]=(0,i.iM)(((...l)=>k.setValue&&k.setValue(...l)),["prevent"]))},[(0,t._)("div",p,[(0,t._)("div",h,[(0,t.wy)((0,t._)("input",{type:"text","onUpdate:modelValue":e[2]||(e[2]=e=>l.value_=e),placeholder:"Variable value",disabled:l.loading,ref:"text"},null,8,_),[[i.nr,l.value_]])]),(0,t._)("div",b,[(0,t._)("button",{type:"button",title:"Clear",onClick:e[3]||(e[3]=(0,i.iM)(((...l)=>k.clearValue&&k.clearValue(...l)),["stop"])),disabled:l.loading},y,8,f),(0,t._)("button",{type:"submit",title:"Edit",disabled:l.loading},w,8,g)])])],32)])]))])):(0,t.kq)("",!0)}var V=a(847),x=a(4967),q={name:"Variable",components:{EntityIcon:x["default"]},mixins:[V["default"]],emits:["loading"],data:function(){return{collapsed:!0,value_:null}},computed:{isCollapsed(){return this.collapsed}},methods:{async clearValue(){this.$emit("loading",!0);try{await this.request("variable.unset",{name:this.value.name})}finally{this.$emit("loading",!1)}},async setValue(){const l=this.value_;if(!l?.length)return await this.clearValue();this.$emit("loading",!0);try{const e={};e[this.value.name]=l,await this.request("variable.set",e)}finally{this.$emit("loading",!1)}}},mounted(){this.value_=this.value.value,this.$watch((()=>this.value.value),(l=>{this.value_=l}))}},M=a(3744);const $=(0,M.Z)(q,[["render",C],["__scopeId","data-v-c4b6a144"]]);var D=$}}]);
//# sourceMappingURL=9624.5124c411.js.map
//# sourceMappingURL=9624.e590eb03.js.map

View file

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[984],{3405:function(t,e,n){n.d(e,{Z:function(){return v}});var i=n(6252),a=n(3577),o=n(9963);const s=t=>((0,i.dD)("data-v-a6396ae8"),t=t(),(0,i.Cn)(),t),l=["checked"],c=s((()=>(0,i._)("div",{class:"switch"},[(0,i._)("div",{class:"dot"})],-1))),d={class:"label"};function u(t,e,n,s,u,r){return(0,i.wg)(),(0,i.iD)("div",{class:(0,a.C_)(["power-switch",{disabled:n.disabled}]),onClick:e[0]||(e[0]=(0,o.iM)(((...t)=>r.onInput&&r.onInput(...t)),["stop"]))},[(0,i._)("input",{type:"checkbox",checked:n.value},null,8,l),(0,i._)("label",null,[c,(0,i._)("span",d,[(0,i.WI)(t.$slots,"default",{},void 0,!0)])])],2)}var r={name:"ToggleSwitch",emits:["input"],props:{value:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1}},methods:{onInput(t){if(this.disabled)return!1;this.$emit("input",t)}}},p=n(3744);const h=(0,p.Z)(r,[["render",u],["__scopeId","data-v-a6396ae8"]]);var v=h},984:function(t,e,n){n.r(e),n.d(e,{default:function(){return _}});var i=n(6252),a=n(3577),o=n(9963);const s={class:"entity bluetooth-service-container"},l={class:"head"},c={class:"col-1 icon"},d={class:"col-9 label"},u=["textContent"],r={class:"col-2 connector pull-right"};function p(t,e,n,p,h,v){const g=(0,i.up)("EntityIcon"),f=(0,i.up)("ToggleSwitch");return(0,i.wg)(),(0,i.iD)("div",s,[(0,i._)("div",l,[(0,i._)("div",c,[(0,i.Wm)(g,{entity:t.value,loading:t.loading,error:t.error},null,8,["entity","loading","error"])]),(0,i._)("div",d,[(0,i._)("div",{class:"name",textContent:(0,a.zw)(t.value.name)},null,8,u)]),(0,i._)("div",r,[(0,i.Wm)(f,{value:t.value.connected,disabled:t.loading,onInput:v.connect,onClick:e[0]||(e[0]=(0,o.iM)((()=>{}),["stop"]))},null,8,["value","disabled","onInput"])])])])}var h=n(3405),v=n(4967),g=n(847),f={name:"BluetoothService",components:{ToggleSwitch:h.Z,EntityIcon:v["default"]},mixins:[g["default"]],methods:{async connect(t){t.stopPropagation(),this.$emit("loading",!0);const e="bluetooth."+(this.value.connected?"disconnect":"connect");try{await this.request(e,{device:this.parent.address,service_uuid:this.uuid})}finally{this.$emit("loading",!1)}},async disconnect(t){t.stopPropagation(),this.$emit("loading",!0);try{await this.request("bluetooth.disconnect",{device:this.parent.address})}finally{this.$emit("loading",!1)}}}},m=n(3744);const y=(0,m.Z)(f,[["render",p],["__scopeId","data-v-5c801a06"]]);var _=y}}]);
//# sourceMappingURL=984.ae424e7e.js.map
//# sourceMappingURL=984.b15beee9.js.map

View file

@ -4,7 +4,7 @@ import Utils from "@/Utils"
export default {
name: "EntityMixin",
mixins: [Utils],
emits: ['input'],
emits: ['input', 'loading'],
props: {
loading: {
type: Boolean,

View file

@ -168,10 +168,11 @@ export default {
methods: {
addEntity(entity) {
this.entities[entity.id] = entity
if (entity.parent_id != null)
return // Only group entities that have no parent
this.entities[entity.id] = entity;
['id', 'type', 'category', 'plugin'].forEach((attr) => {
if (entity[attr] == null)
return

View file

@ -96,7 +96,7 @@ $collapse-toggler-width: 2em;
.value {
font-size: 1.1em;
font-weight: bold;
word-break: break-all;
word-break: break-word;
opacity: 0.8;
}

View file

@ -6,8 +6,12 @@ from websocket import WebSocketApp
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.trello import MoveCardEvent, NewCardEvent, ArchivedCardEvent, \
UnarchivedCardEvent
from platypush.message.event.trello import (
MoveCardEvent,
NewCardEvent,
ArchivedCardEvent,
UnarchivedCardEvent,
)
from platypush.plugins.trello import TrelloPlugin
@ -33,9 +37,9 @@ class TrelloBackend(Backend):
Triggers:
* :class:`platypush.message.event.trello.NewCardEvent` when a card is created.
* :class:`platypush.message.event.MoveCardEvent` when a card is moved.
* :class:`platypush.message.event.ArchivedCardEvent` when a card is archived/closed.
* :class:`platypush.message.event.UnarchivedCardEvent` when a card is un-archived/opened.
* :class:`platypush.message.event.trello.MoveCardEvent` when a card is moved.
* :class:`platypush.message.event.trello.ArchivedCardEvent` when a card is archived/closed.
* :class:`platypush.message.event.trello.UnarchivedCardEvent` when a card is un-archived/opened.
"""
@ -69,21 +73,26 @@ class TrelloBackend(Backend):
def _initialize_connection(self, ws: WebSocketApp):
for board_id in self._boards_by_id.keys():
self._send(ws, {
'type': 'subscribe',
'modelType': 'Board',
'idModel': board_id,
'tags': ['clientActions', 'updates'],
'invitationTokens': [],
})
self._send(
ws,
{
'type': 'subscribe',
'modelType': 'Board',
'idModel': board_id,
'tags': ['clientActions', 'updates'],
'invitationTokens': [],
},
)
self.logger.info('Trello boards subscribed')
def _on_msg(self):
def hndl(*args):
if len(args) < 2:
self.logger.warning('Missing websocket argument - make sure that you are using '
'a version of websocket-client < 0.53.0 or >= 0.58.0')
self.logger.warning(
'Missing websocket argument - make sure that you are using '
'a version of websocket-client < 0.53.0 or >= 0.58.0'
)
return
ws, msg = args[:2]
@ -96,7 +105,9 @@ class TrelloBackend(Backend):
try:
msg = json.loads(msg)
except Exception as e:
self.logger.warning('Received invalid JSON message from Trello: {}: {}'.format(msg, e))
self.logger.warning(
'Received invalid JSON message from Trello: {}: {}'.format(msg, e)
)
return
if 'error' in msg:
@ -119,8 +130,12 @@ class TrelloBackend(Backend):
args = {
'card_id': delta['data']['card']['id'],
'card_name': delta['data']['card']['name'],
'list_id': (delta['data'].get('list') or delta['data'].get('listAfter', {})).get('id'),
'list_name': (delta['data'].get('list') or delta['data'].get('listAfter', {})).get('name'),
'list_id': (
delta['data'].get('list') or delta['data'].get('listAfter', {})
).get('id'),
'list_name': (
delta['data'].get('list') or delta['data'].get('listAfter', {})
).get('name'),
'board_id': delta['data']['board']['id'],
'board_name': delta['data']['board']['name'],
'closed': delta.get('closed'),
@ -134,14 +149,20 @@ class TrelloBackend(Backend):
self.bus.post(NewCardEvent(**args))
elif delta.get('type') == 'updateCard':
if 'listBefore' in delta['data']:
args.update({
'old_list_id': delta['data']['listBefore']['id'],
'old_list_name': delta['data']['listBefore']['name'],
})
args.update(
{
'old_list_id': delta['data']['listBefore']['id'],
'old_list_name': delta['data']['listBefore']['name'],
}
)
self.bus.post(MoveCardEvent(**args))
elif 'closed' in delta['data'].get('old', {}):
cls = UnarchivedCardEvent if delta['data']['old']['closed'] else ArchivedCardEvent
cls = (
UnarchivedCardEvent
if delta['data']['old']['closed']
else ArchivedCardEvent
)
self.bus.post(cls(**args))
return hndl
@ -185,11 +206,13 @@ class TrelloBackend(Backend):
self._req_id += 1
def _connect(self) -> WebSocketApp:
return WebSocketApp(self.url,
on_open=self._on_open(),
on_message=self._on_msg(),
on_error=self._on_error(),
on_close=self._on_close())
return WebSocketApp(
self.url,
on_open=self._on_open(),
on_message=self._on_msg(),
on_error=self._on_error(),
on_close=self._on_close(),
)
def run(self):
super().run()

View file

@ -1,8 +1,8 @@
manifest:
events:
platypush.message.event.ArchivedCardEvent: when a card is archived/closed.
platypush.message.event.MoveCardEvent: when a card is moved.
platypush.message.event.UnarchivedCardEvent: when a card is un-archived/opened.
platypush.message.event.trello.ArchivedCardEvent: when a card is archived/closed.
platypush.message.event.trello.MoveCardEvent: when a card is moved.
platypush.message.event.trello.UnarchivedCardEvent: when a card is un-archived/opened.
platypush.message.event.trello.NewCardEvent: when a card is created.
install:
pip: []

View file

@ -15,7 +15,7 @@ class WiimoteBackend(Backend):
Triggers:
* :class:`platypush.message.event.Wiimote.WiimoteEvent` \
* :class:`platypush.message.event.wiimote.WiimoteEvent` \
when the state of the Wiimote (battery, buttons, acceleration etc.) changes
Requires:

View file

@ -1,6 +1,6 @@
manifest:
events:
platypush.message.event.Wiimote.WiimoteEvent: when the state of the Wiimote (battery,
platypush.message.event.wiimote.WiimoteEvent: when the state of the Wiimote (battery,
buttons, acceleration etc.) changes
install:
pip: []

View file

@ -1,35 +0,0 @@
import warnings
from platypush.backend import Backend
class ZigbeeMqttBackend(Backend):
"""
Listen for events on a zigbee2mqtt service.
**WARNING**: This backend is **DEPRECATED** and it will be removed in a
future version.
It has been merged with
:class:`platypush.plugins.zigbee.mqtt.ZigbeeMqttPlugin`.
Now you can simply configure the `zigbee.mqtt` plugin in order to enable
the Zigbee integration - no need to enable both the plugin and the backend.
"""
def run(self):
super().run()
warnings.warn(
'''
The zigbee.mqtt backend has been merged into the zigbee.mqtt
plugin. It is now deprecated and it will be removed in a future
version.
Please remove any references to it from your configuration.
''',
DeprecationWarning,
)
self.wait_stop()
# vim:sw=4:ts=4:et:

View file

@ -1,350 +0,0 @@
import inspect
import logging
import queue
import os
import threading
from typing import Optional
from platypush.backend import Backend
from platypush.config import Config
from platypush.message.event.zwave import (
ZwaveNetworkReadyEvent,
ZwaveNetworkStoppedEvent,
ZwaveEvent,
ZwaveNodeAddedEvent,
ZwaveValueAddedEvent,
ZwaveNodeQueryCompletedEvent,
ZwaveValueChangedEvent,
ZwaveValueRefreshedEvent,
ZwaveValueRemovedEvent,
ZwaveNetworkResetEvent,
ZwaveCommandEvent,
ZwaveCommandWaitingEvent,
ZwaveNodeRemovedEvent,
ZwaveNodeRenamedEvent,
ZwaveNodeReadyEvent,
ZwaveButtonRemovedEvent,
ZwaveButtonCreatedEvent,
ZwaveButtonOnEvent,
ZwaveButtonOffEvent,
ZwaveNetworkErrorEvent,
ZwaveNodeGroupEvent,
ZwaveNodePollingEnabledEvent,
ZwaveNodePollingDisabledEvent,
ZwaveNodeSceneEvent,
ZwaveNodeEvent,
)
event_queue = queue.Queue()
network_ready = threading.Event()
class _ZWEvent:
def __init__(self, signal: str, sender: str, network=None, **kwargs):
self.signal = signal
self.sender = sender
self.network = network
self.args = kwargs
def _zwcallback(signal, sender, network, **kwargs):
if signal == network.SIGNAL_NETWORK_AWAKED:
network_ready.set()
event_queue.put(_ZWEvent(signal=signal, sender=sender, network=network, **kwargs))
class ZwaveBackend(Backend):
"""
Start and manage a Z-Wave network.
If you are using a USB adapter and want a consistent naming for the device paths, you can use udev.
.. code-block:: shell
# Get the vendorID and productID of your device through lsusb.
# Then add a udev rule for it to link it e.g. to /dev/zwave.
cat <<EOF > /etc/udev/rules.d/92-zwave.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0658", ATTRS{idProduct}=="0200", SYMLINK+="zwave"
EOF
# Restart the udev service
systemctl restart systemd-udevd.service
.. note::
This backend is deprecated, since the underlying ``python-openzwave`` is
quite buggy and largely unmaintained.
Use the `zwave.mqtt` backend instead
(:class:`platypush.backend.zwave.mqtt.ZwaveMqttBackend`).
Triggers:
* :class:`platypush.message.event.zwave.ZwaveNetworkReadyEvent` when the network is up and running.
* :class:`platypush.message.event.zwave.ZwaveNetworkStoppedEvent` when the network goes down.
* :class:`platypush.message.event.zwave.ZwaveNetworkResetEvent` when the network is reset.
* :class:`platypush.message.event.zwave.ZwaveNetworkErrorEvent` when an error occurs on the network.
* :class:`platypush.message.event.zwave.ZwaveNodeQueryCompletedEvent` when all the nodes on the network
have been queried.
* :class:`platypush.message.event.zwave.ZwaveNodeEvent` when a node attribute changes.
* :class:`platypush.message.event.zwave.ZwaveNodeAddedEvent` when a node is added to the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRemovedEvent` when a node is removed from the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRenamedEvent` when a node is renamed.
* :class:`platypush.message.event.zwave.ZwaveNodeReadyEvent` when a node is ready.
* :class:`platypush.message.event.zwave.ZwaveNodeGroupEvent` when a node is associated/de-associated to a
group.
* :class:`platypush.message.event.zwave.ZwaveNodeSceneEvent` when a scene is set on a node.
* :class:`platypush.message.event.zwave.ZwaveNodePollingEnabledEvent` when the polling is successfully turned
on a node.
* :class:`platypush.message.event.zwave.ZwaveNodePollingDisabledEvent` when the polling is successfully turned
off a node.
* :class:`platypush.message.event.zwave.ZwaveButtonCreatedEvent` when a button is added to the network.
* :class:`platypush.message.event.zwave.ZwaveButtonRemovedEvent` when a button is removed from the network.
* :class:`platypush.message.event.zwave.ZwaveButtonOnEvent` when a button is pressed.
* :class:`platypush.message.event.zwave.ZwaveButtonOffEvent` when a button is released.
* :class:`platypush.message.event.zwave.ZwaveValueAddedEvent` when a value is added to a node on the network.
* :class:`platypush.message.event.zwave.ZwaveValueChangedEvent` when the value of a node on the network
changes.
* :class:`platypush.message.event.zwave.ZwaveValueRefreshedEvent` when the value of a node on the network
is refreshed.
* :class:`platypush.message.event.zwave.ZwaveValueRemovedEvent` when the value of a node on the network
is removed.
* :class:`platypush.message.event.zwave.ZwaveCommandEvent` when a command is received on the network.
* :class:`platypush.message.event.zwave.ZwaveCommandWaitingEvent` when a command is waiting for a message
to complete.
Requires:
* **python-openzwave** (``pip install python-openzwave``)
"""
def __init__(
self,
device: str,
config_path: Optional[str] = None,
user_path: Optional[str] = None,
ready_timeout: float = 10.0,
*args,
**kwargs
):
"""
:param device: Path to the Z-Wave adapter (e.g. /dev/ttyUSB0 or /dev/ttyACM0).
:param config_path: Z-Wave configuration path (default: ``<OPENZWAVE_PATH>/ozw_config``).
:param user_path: Z-Wave user path where runtime and configuration files will be stored
(default: ``<PLATYPUSH_WORKDIR>/zwave``).
:param ready_timeout: Network ready timeout in seconds (default: 60).
"""
import python_openzwave
from openzwave.network import ZWaveNetwork
super().__init__(*args, **kwargs)
self.device = device
if not config_path:
# noinspection PyTypeChecker
config_path = os.path.join(
os.path.dirname(inspect.getfile(python_openzwave)), 'ozw_config'
)
if not user_path:
user_path = os.path.join(Config.get('workdir'), 'zwave')
os.makedirs(user_path, mode=0o770, exist_ok=True)
self.config_path = config_path
self.user_path = user_path
self.ready_timeout = ready_timeout
self.network: Optional[ZWaveNetwork] = None
def start_network(self):
if self.network and self.network.state >= self.network.STATE_AWAKED:
self.logger.info('Z-Wave network already started')
return
from openzwave.network import ZWaveNetwork, dispatcher
from openzwave.option import ZWaveOption
network_ready.clear()
logging.getLogger('openzwave').addHandler(self.logger)
opts = ZWaveOption(
self.device, config_path=self.config_path, user_path=self.user_path
)
opts.set_console_output(False)
opts.lock()
self.network = ZWaveNetwork(opts, log=None)
dispatcher.connect(_zwcallback)
ready = network_ready.wait(self.ready_timeout)
if not ready:
self.logger.warning(
'Driver not ready after {} seconds: continuing anyway'.format(
self.ready_timeout
)
)
def stop_network(self):
if self.network:
self.network.stop()
network_ready.clear()
self.network = None
def _process_event(self, event: _ZWEvent):
from platypush.plugins.zwave import ZwavePlugin
network = (
event.network
if hasattr(event, 'network') and event.network
else self.network
)
if (
event.signal == network.SIGNAL_NETWORK_STOPPED
or event.signal == network.SIGNAL_DRIVER_REMOVED
):
event = ZwaveNetworkStoppedEvent(device=self.device)
elif (
event.signal == network.SIGNAL_ALL_NODES_QUERIED
or event.signal == network.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD
):
event = ZwaveNodeQueryCompletedEvent(device=self.device)
elif event.signal == network.SIGNAL_NETWORK_FAILED:
event = ZwaveNetworkErrorEvent(device=self.device)
self.logger.warning('Z-Wave network error')
elif (
event.signal == network.SIGNAL_NETWORK_RESETTED
or event.signal == network.SIGNAL_DRIVER_RESET
):
event = ZwaveNetworkResetEvent(device=self.device)
elif event.signal == network.SIGNAL_BUTTON_ON:
event = ZwaveButtonOnEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_BUTTON_OFF:
event = ZwaveButtonOffEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_CONTROLLER_COMMAND:
event = ZwaveCommandEvent(
device=self.device,
state=event.args['state'],
state_description=event.args['state_full'],
error=event.args['error'] if event.args['error_int'] else None,
error_description=event.args['error_full']
if event.args['error_int']
else None,
node=ZwavePlugin.node_to_dict(event.args['node'])
if event.args['node']
else None,
)
elif event.signal == network.SIGNAL_CONTROLLER_WAITING:
event = ZwaveCommandWaitingEvent(
device=self.device,
state=event.args['state'],
state_description=event.args['state_full'],
)
elif event.signal == network.SIGNAL_CREATE_BUTTON:
event = ZwaveButtonCreatedEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_DELETE_BUTTON:
event = ZwaveButtonRemovedEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_GROUP:
event = ZwaveNodeGroupEvent(
device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
group_index=event.args['groupidx'],
)
elif event.signal == network.SIGNAL_NETWORK_AWAKED:
event = ZwaveNetworkReadyEvent(
device=self.device,
ozw_library_version=self.network.controller.ozw_library_version,
python_library_version=self.network.controller.python_library_version,
zwave_library=self.network.controller.library_description,
home_id=self.network.controller.home_id,
node_id=self.network.controller.node_id,
node_version=self.network.controller.node.version,
nodes_count=self.network.nodes_count,
)
elif event.signal == network.SIGNAL_NODE_EVENT:
event = ZwaveNodeEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_NODE_ADDED:
event = ZwaveNodeAddedEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_NODE_NAMING:
event = ZwaveNodeRenamedEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_NODE_READY:
event = ZwaveNodeReadyEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_NODE_REMOVED:
event = ZwaveNodeRemovedEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_POLLING_DISABLED:
event = ZwaveNodePollingEnabledEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_POLLING_ENABLED:
event = ZwaveNodePollingDisabledEvent(
device=self.device, node=ZwavePlugin.node_to_dict(event.args['node'])
)
elif event.signal == network.SIGNAL_SCENE_EVENT:
event = ZwaveNodeSceneEvent(
device=self.device,
scene_id=event.args['scene_id'],
node=ZwavePlugin.node_to_dict(event.args['node']),
)
elif event.signal == network.SIGNAL_VALUE_ADDED:
event = ZwaveValueAddedEvent(
device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']),
)
elif event.signal == network.SIGNAL_VALUE_CHANGED:
event = ZwaveValueChangedEvent(
device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']),
)
elif event.signal == network.SIGNAL_VALUE_REFRESHED:
event = ZwaveValueRefreshedEvent(
device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']),
)
elif event.signal == network.SIGNAL_VALUE_REMOVED:
event = ZwaveValueRemovedEvent(
device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']),
)
else:
self.logger.info('Received unhandled ZWave event: {}'.format(event))
if isinstance(event, ZwaveEvent):
self.bus.post(event)
def __enter__(self):
self.start_network()
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop_network()
def loop(self):
try:
event = event_queue.get(block=True, timeout=1.0)
self._process_event(event)
except queue.Empty:
pass
# vim:sw=4:ts=4:et:

View file

@ -1,48 +0,0 @@
manifest:
events:
platypush.message.event.zwave.ZwaveButtonCreatedEvent: when a button is added
to the network.
platypush.message.event.zwave.ZwaveButtonOffEvent: when a button is released.
platypush.message.event.zwave.ZwaveButtonOnEvent: when a button is pressed.
platypush.message.event.zwave.ZwaveButtonRemovedEvent: when a button is removed
from the network.
platypush.message.event.zwave.ZwaveCommandEvent: when a command is received on
the network.
platypush.message.event.zwave.ZwaveCommandWaitingEvent: when a command is waiting
for a messageto complete.
platypush.message.event.zwave.ZwaveNetworkErrorEvent: when an error occurs on
the network.
platypush.message.event.zwave.ZwaveNetworkReadyEvent: when the network is up and
running.
platypush.message.event.zwave.ZwaveNetworkResetEvent: when the network is reset.
platypush.message.event.zwave.ZwaveNetworkStoppedEvent: when the network goes
down.
platypush.message.event.zwave.ZwaveNodeAddedEvent: when a node is added to the
network.
platypush.message.event.zwave.ZwaveNodeEvent: when a node attribute changes.
platypush.message.event.zwave.ZwaveNodeGroupEvent: when a node is associated/de-associated
to agroup.
platypush.message.event.zwave.ZwaveNodePollingDisabledEvent: when the polling
is successfully turnedoff a node.
platypush.message.event.zwave.ZwaveNodePollingEnabledEvent: when the polling is
successfully turnedon a node.
platypush.message.event.zwave.ZwaveNodeQueryCompletedEvent: when all the nodes
on the networkhave been queried.
platypush.message.event.zwave.ZwaveNodeReadyEvent: when a node is ready.
platypush.message.event.zwave.ZwaveNodeRemovedEvent: when a node is removed from
the network.
platypush.message.event.zwave.ZwaveNodeRenamedEvent: when a node is renamed.
platypush.message.event.zwave.ZwaveNodeSceneEvent: when a scene is set on a node.
platypush.message.event.zwave.ZwaveValueAddedEvent: when a value is added to a
node on the network.
platypush.message.event.zwave.ZwaveValueChangedEvent: when the value of a node
on the networkchanges.
platypush.message.event.zwave.ZwaveValueRefreshedEvent: when the value of a node
on the networkis refreshed.
platypush.message.event.zwave.ZwaveValueRemovedEvent: when the value of a node
on the networkis removed.
install:
pip:
- python-openzwave
package: platypush.backend.zwave
type: backend

View file

@ -1,15 +1,34 @@
from collections import defaultdict
from dataclasses import dataclass, field
import logging
import threading
import time
from queue import Queue, Empty
from typing import Callable, Type
from typing import Callable, Dict, Iterable, Type
from platypush.message import Message
from platypush.message.event import Event
logger = logging.getLogger('platypush:bus')
@dataclass
class MessageHandler:
"""
Wrapper for a message callback handler.
"""
msg_type: Type[Message]
callback: Callable[[Message], None]
kwargs: dict = field(default_factory=dict)
def match(self, msg: Message) -> bool:
return isinstance(msg, self.msg_type) and all(
getattr(msg, k, None) == v for k, v in self.kwargs.items()
)
class Bus:
"""
Main local bus where the daemon will listen for new messages.
@ -21,7 +40,10 @@ class Bus:
self.bus = Queue()
self.on_message = on_message
self.thread_id = threading.get_ident()
self.event_handlers = {}
self.handlers: Dict[
Type[Message], Dict[Callable[[Message], None], MessageHandler]
] = defaultdict(dict)
self._should_stop = threading.Event()
def post(self, msg):
@ -38,26 +60,24 @@ class Bus:
def stop(self):
self._should_stop.set()
def _get_matching_handlers(
self, msg: Message
) -> Iterable[Callable[[Message], None]]:
return [
hndl.callback
for cls in type(msg).__mro__
for hndl in self.handlers.get(cls, [])
if hndl.match(msg)
]
def _msg_executor(self, msg):
def event_handler(event: Event, handler: Callable[[Event], None]):
logger.info('Triggering event handler %s', handler.__name__)
handler(event)
def executor():
if isinstance(msg, Event):
handlers = self.event_handlers.get(
type(msg),
{
*[
hndl
for event_type, hndl in self.event_handlers.items()
if isinstance(msg, event_type)
]
},
)
for hndl in handlers:
threading.Thread(target=event_handler, args=(msg, hndl))
for hndl in self._get_matching_handlers(msg):
threading.Thread(target=event_handler, args=(msg, hndl)).start()
try:
if self.on_message:
@ -100,27 +120,25 @@ class Bus:
logger.info('Bus service stopped')
def register_handler(
self, event_type: Type[Event], handler: Callable[[Event], None]
self, type: Type[Message], handler: Callable[[Message], None], **kwargs
) -> Callable[[], None]:
"""
Register an event handler to the bus.
Register a generic handler to the bus.
:param event_type: Event type to subscribe (event inheritance also works).
:param handler: Event handler - a function that takes an Event object as parameter.
:param type: Type of the message to subscribe to (event inheritance also works).
:param handler: Event handler - a function that takes a Message object as parameter.
:param kwargs: Extra filter on the message values.
:return: A function that can be called to remove the handler (no parameters required).
"""
if event_type not in self.event_handlers:
self.event_handlers[event_type] = set()
self.event_handlers[event_type].add(handler)
self.handlers[type][handler] = MessageHandler(type, handler, kwargs)
def unregister():
self.unregister_handler(event_type, handler)
self.unregister_handler(type, handler)
return unregister
def unregister_handler(
self, event_type: Type[Event], handler: Callable[[Event], None]
self, type: Type[Message], handler: Callable[[Message], None]
) -> None:
"""
Remove an event handler.
@ -128,14 +146,12 @@ class Bus:
:param event_type: Event type.
:param handler: Existing event handler.
"""
if event_type not in self.event_handlers:
if type not in self.handlers:
return
if handler in self.event_handlers[event_type]:
self.event_handlers[event_type].remove(handler)
if len(self.event_handlers[event_type]) == 0:
del self.event_handlers[event_type]
self.handlers[type].pop(handler, None)
if len(self.handlers[type]) == 0:
del self.handlers[type]
# vim:sw=4:ts=4:et:

View file

@ -106,10 +106,11 @@ class Config:
if cfgfile is None:
cfgfile = self._get_default_cfgfile()
cfgfile = os.path.abspath(os.path.expanduser(cfgfile))
if cfgfile is None or not os.path.exists(cfgfile):
cfgfile = self._create_default_config()
cfgfile = self._create_default_config(cfgfile)
self.config_file = os.path.abspath(os.path.expanduser(cfgfile))
self.config_file = cfgfile
def _init_logging(self):
logging_config = {
@ -211,21 +212,24 @@ class Config:
'variable': {},
}
def _create_default_config(self):
@staticmethod
def _create_default_config(cfgfile: Optional[str] = None):
cfg_mod_dir = os.path.dirname(os.path.abspath(__file__))
# Use /etc/platypush/config.yaml if the user is running as root,
# otherwise ~/.config/platypush/config.yaml
cfgfile = (
(
os.path.join(os.environ['XDG_CONFIG_HOME'], 'config.yaml')
if os.environ.get('XDG_CONFIG_HOME')
else os.path.join(
os.path.expanduser('~'), '.config', 'platypush', 'config.yaml'
if not cfgfile:
# Use /etc/platypush/config.yaml if the user is running as root,
# otherwise ~/.config/platypush/config.yaml
cfgfile = (
(
os.path.join(os.environ['XDG_CONFIG_HOME'], 'config.yaml')
if os.environ.get('XDG_CONFIG_HOME')
else os.path.join(
os.path.expanduser('~'), '.config', 'platypush', 'config.yaml'
)
)
if not is_root()
else os.path.join(os.sep, 'etc', 'platypush', 'config.yaml')
)
if not is_root()
else os.path.join(os.sep, 'etc', 'platypush', 'config.yaml')
)
cfgdir = pathlib.Path(cfgfile).parent
cfgdir.mkdir(parents=True, exist_ok=True)
@ -526,6 +530,7 @@ class Config:
Get a config value or the whole configuration object.
:param key: Configuration entry to get (default: all entries).
:param default: Default value to return if the key is missing.
"""
# pylint: disable=protected-access
config = cls._get_instance()._config.copy()

View file

@ -26,6 +26,7 @@
### Include directives
### ------------------
###
# # You can split your configuration over multiple files and use the include
# # directive to import other files into your configuration.
#
@ -37,11 +38,13 @@
# - logging.yaml
# - media.yaml
# - sensors.yaml
###
### -----------------
### Working directory
### -----------------
###
# # Working directory of the application. This is where the main database will be
# # stored by default (if the default SQLite configuration is used), and it's
# # where the integrations will store their state.
@ -55,11 +58,13 @@
# # - $HOME/.local/share/platypush otherwise.
#
# workdir: ~/.local/share/platypush
###
### ----------------------
### Database configuration
### ----------------------
###
# # By default Platypush will use a SQLite database named `main.db` under the
# # `workdir`. You can specify any other engine string here - the application has
# # been tested against SQLite, Postgres and MariaDB/MySQL >= 8.
@ -73,11 +78,13 @@
# engine: sqlite:///home/user/.local/share/platypush/main.db
# # OR, if you want to use e.g. Postgres with the pg8000 driver:
# engine: postgresql+pg8000://dbuser:dbpass@dbhost/dbname
###
### ---------------------
### Logging configuration
### ---------------------
###
# # Platypush logs on stdout by default. You can use the logging section to
# # specify an alternative file or change the logging level.
#
@ -87,11 +94,13 @@
# logging:
# filename: ~/.local/log/platypush/platypush.log
# level: INFO
###
### -----------------------
### device_id configuration
### -----------------------
###
# # The device_id is used by many components of Platypush and it should uniquely
# # identify a device in your network. If nothing is specified then the hostname
# # will be used.
@ -100,11 +109,13 @@
# # -d/--device-id option.
#
# device_id: my_device
###
### -------------------
### Redis configuration
### -------------------
###
# # Platypush needs a Redis instance for inter-process communication.
# #
# # By default, the application will try and connect to a Redis server listening
@ -123,11 +134,13 @@
# port: 6379
# username: user
# password: secret
###
### ------------------------
### Web server configuration
### ------------------------
###
# Platypush comes with a versatile Web server that is used to:
#
# - Serve the main UI and the UIs for the plugins that provide one.
@ -225,6 +238,30 @@ backend.http:
# poll_interval: 20
###
###
# # Example configuration of the MQTT plugin.
# # This plugin allows you to subscribe to MQTT topics and trigger `platypush.message.event.mqtt.MQTTMessageEvent`
# # events that you can hook on when new messages are received.
# # You can also publish messages to MQTT topics through the `mqtt.publish` action.
#
# mqtt:
# # Host and port of the MQTT broker
# host: my-mqtt-broker
# port: 1883
# # Topic to subscribe to. Messages received on these topics will trigger `MQTTMessageEvent` events.
# topics:
# - platypush/sensors
#
# # Extra listeners. You can use them to subscribe to multiple brokers at the same time.
# listeners:
# - host: another-mqtt-broker
# port: 1883
# username: user
# password: secret
# topics:
# - platypush/tests
###
###
# # Example configuration of music.mpd plugin, a plugin to interact with MPD and
# # Mopidy music server instances. See
@ -244,17 +281,6 @@ backend.http:
# clipboard:
###
###
# # Example configuration of the MQTT plugin. This specifies a server that the
# # application will use by default (if not specified on the request body).
#
# mqtt:
# host: 192.168.1.2
# port: 1883
# username: user
# password: secret
###
###
# # Enable the system plugin if you want your device to periodically report
# # system statistics (CPU load, disk usage, memory usage etc.)
@ -262,7 +288,7 @@ backend.http:
# # When new data is gathered, an `EntityUpdateEvent` with `plugin='system'` will
# # be triggered with the new data, and you can subscribe a hook to these events
# # to run your custom logic.
#
#
# system:
# # How often we should poll for new data
# poll_interval: 60
@ -311,7 +337,7 @@ backend.http:
###
# # Example configuration of a weather plugin
#
#
# weather.openweathermap:
# token: secret
# lat: lat
@ -328,7 +354,7 @@ backend.http:
# # using Web hooks (i.e. event hooks that subscribe to
# # `platypush.message.event.http.hook.WebhookEvent` events), provided that the
# # Web server is listening on a publicly accessible address.
#
#
# ifttt:
# ifttt_key: SECRET
###
@ -348,14 +374,14 @@ backend.http:
# # build automation routines on. You can also use Platypush to control your
# # Zigbee devices, either through the Web interface or programmatically through
# # the available plugin actions.
#
#
# zigbee.mqtt:
# # Host of the MQTT broker
# host: riemann
# host: my-mqtt-broker
# # Listen port of the MQTT broker
# port: 1883
# # Base topic, as specified in `<zigbee2mqtt_dir>/data/configuration.yaml`
# base_topic: zigbee2mqtt
# topic_prefix: zigbee2mqtt
###
###
@ -366,10 +392,10 @@ backend.http:
# # automation routines on.
# # You can also use Platypush to control your Z-Wave devices, either through the
# # Web interface or programmatically through the available plugin actions.
#
#
# zwave.mqtt:
# # Host of the MQTT broker
# host: riemann
# host: my-mqtt-broker
# # Listen port of the MQTT broker
# port: 1883
# # Gateway name, usually configured in the ZWaveJS-UI through `Settings ->
@ -402,7 +428,7 @@ backend.http:
#
# # You can also capture images by connecting to the
# # `/camera/<plugin>/photo[.extension]`, for example `/camera/ffmpeg/photo.jpg`.
#
#
# camera.ffmpeg:
# # Default video device to use
# device: /dev/video0
@ -543,7 +569,7 @@ backend.http:
# # `platypush.message.event.sensor.SensorDataChangeEvent` events will be
# # triggered when the data changes - you can subscribe to them in your custom
# # hooks.
#
#
# serial:
# # The path to the USB interface with e.g. an Arduino or ESP microcontroller
# # connected.
@ -579,7 +605,7 @@ backend.http:
# temperature: 0.5
# humidity: 0.75
# luminosity: 5
#
#
# # If a threshold is defined for a sensor, and the value of that sensor goes
# # below/above that temperature between two reads, then a
# # `SensorDataBelowThresholdEvent` or a `SensorDataAboveThresholdEvent` will
@ -599,7 +625,7 @@ backend.http:
# # Note that the interface of this plugin is basically the same as the serial
# # plugin, and any other plugin that extends `SensorPlugin` in general.
# # Therefore, poll_interval, tolerance and thresholds are supported here too.
#
#
# arduino:
# board: /dev/ttyUSB0
# # name -> PIN number mapping (similar for digital_pins).
@ -607,10 +633,10 @@ backend.http:
# # the forwarded events.
# analog_pins:
# temperature: 7
#
#
# tolerance:
# temperature: 0.5
#
#
# thresholds:
# temperature: 25.0
###
@ -619,13 +645,13 @@ backend.http:
# # Another example: the LTR559 is a common sensor for proximity and luminosity
# # that can be wired to a Raspberry Pi or similar devices over SPI or I2C
# # interface. It exposes the same base interface as all other sensor plugins.
#
#
# sensor.ltr559:
# poll_interval: 1.0
# tolerance:
# light: 7.0
# proximity: 5.0
#
#
# thresholds:
# proximity: 10.0
###
@ -637,7 +663,7 @@ backend.http:
###
# # `tts` is the simplest TTS integration. It leverages the Google Translate open
# # "say" endpoint to render text as audio speech.
#
#
# tts:
# # The media plugin that should be used to play the audio response
# media_plugin: media.vlc
@ -655,7 +681,7 @@ backend.http:
# # Google developers console, create an API key, and follow the instruction
# # logged on the next restart to give your app the required permissions to your
# # account.
#
#
# tts.google:
# # The media plugin that should be used to play the audio response
# media_plugin: media.vlc
@ -674,7 +700,7 @@ backend.http:
# # Follow the instructions at
# # https://docs.platypush.tech/platypush/plugins/tts.mimic3.html to quickly
# # bootstrap a mimic3 server.
#
#
# tts.mimic3:
# # The base URL of the mimic3 server
# server_url: http://riemann:59125
@ -731,7 +757,7 @@ backend.http:
# # A use-case can be the one where you have a Tasker automation running on your
# # Android device that detects when your phone enters or exits a certain area,
# # and sends the appropriate request to your Platypush server.
#
#
# procedure.at_home:
# # Set the db variable AT_HOME to 1.
# # Variables are flexible entities with a name and a value that will be
@ -741,11 +767,11 @@ backend.http:
# - action: variable.set
# args:
# AT_HOME: 1
#
#
# # Check the luminosity level from e.g. a connected LTR559 sensor.
# # It could also be a Bluetooth, Zigbee, Z-Wave, serial etc. sensor.
# - action: sensor.ltr559.get_measurement
#
#
# # If it's below a certain threshold, turn on the lights.
# # In this case, `light` is a parameter returned by the previous response,
# # so we can directly access it here through the `${}` context operator.
@ -753,12 +779,12 @@ backend.http:
# # ${output["light"]}.
# - if ${int(light or 0) < 110}:
# - action: light.hue.on
#
#
# # Say a welcome home message
# - action: tts.mimic3.say
# args:
# text: Welcome home
#
#
# # Start the music
# - action: music.mpd.play
###
@ -771,10 +797,10 @@ backend.http:
# - action: variable.unset
# args:
# name: AT_HOME
#
#
# # Stop the music
# - action: music.mpd.stop
#
#
# # Turn off the lights
# - action: light.hue.off
###
@ -789,12 +815,12 @@ backend.http:
# #
# # See the event hook section below for a sample hook that listens for messages
# # sent by other clients using this procedure.
#
#
# procedure.send_sensor_data(name, value):
# - action: mqtt.send_message
# args:
# topic: platypush/sensors
# host: mqtt-server
# host: my-mqtt-broker
# port: 1883
# msg:
# name: ${name}
@ -807,7 +833,7 @@ backend.http:
## -------------------
# Event hooks are procedures that are run when a certain condition is met.
#
#
# Check the documentation of your configured backends and plugins to see which
# events they can trigger, and check https://docs.platypush.tech/events.html
# for the full list of available events with their schemas.
@ -830,7 +856,7 @@ backend.http:
# # Note that, for this event to be triggered, the application must first
# # subscribe to the `platypush/sensor` topic - e.g. by adding `platypush/sensor`
# # to the active subscriptions in the `mqtt` configurations.
#
#
# event.hook.OnSensorDataReceived:
# if:
# type: platypush.message.event.mqtt.MQTTMessageEvent
@ -849,7 +875,7 @@ backend.http:
###
# # The example below plays the music on mpd/mopidy when your voice assistant
# # triggers a speech recognized event with "play the music" content.
#
#
# event.hook.PlayMusicAssistantCommand:
# if:
# type: platypush.message.event.assistant.SpeechRecognizedEvent
@ -863,7 +889,7 @@ backend.http:
###
# # This will turn on the lights when you say "turn on the lights"
#
#
# event.hook.TurnOnLightsCommand:
# if:
# type: platypush.message.event.assistant.SpeechRecognizedEvent
@ -887,7 +913,7 @@ backend.http:
# # By default they don't have an authentication layer at all. You are however
# # advised to create your custom passphrase and checks the request's headers or
# # query string for it - preferably one passphrase per endpoint.
#
#
# event.hook.WebhookExample:
# if:
# type: platypush.message.event.http.hook.WebhookEvent
@ -910,7 +936,7 @@ backend.http:
# # Standard UNIX cron syntax is supported, plus an optional 6th indicator
# # at the end of the expression to run jobs with second granularity.
# # The example below executes a script at intervals of 1 minute.
#
#
# cron.TestCron:
# cron_expression: '* * * * *'
# actions:

View file

@ -25,8 +25,8 @@ class EntitiesEngine(Thread):
together (preventing excessive writes and throttling events), and
prevents race conditions when SQLite is used.
2. Merge any existing entities with their newer representations.
3. Update the entities taxonomy.
4. Persist the new state to the entities database.
3. Update the entities' taxonomy.
4. Persist the new state to the entities' database.
5. Trigger events for the updated entities.
"""

View file

@ -32,7 +32,10 @@ def action(f: Callable[..., Any]) -> Callable[..., Response]:
response = Response()
try:
result = f(*args, **kwargs)
except TypeError as e:
except Exception as e:
if isinstance(e, KeyboardInterrupt):
return response
_logger.exception(e)
result = Response(errors=[str(e)])

View file

@ -12,6 +12,7 @@ from typing import Callable, Dict, Generator, Optional, Type, Union
from platypush.backend import Backend
from platypush.config import Config
from platypush.plugins import Plugin, action
from platypush.message import Message
from platypush.message.event import Event
from platypush.message.response import Response
from platypush.utils import (
@ -314,7 +315,8 @@ class InspectPlugin(Plugin):
{
get_plugin_name_by_class(cls): dict(plugin)
for cls, plugin in self._components_cache.get(Plugin, {}).items()
}
},
cls=Message.Encoder,
)
@action

View file

@ -1,16 +1,25 @@
from collections import defaultdict
import hashlib
import io
import json
import os
import threading
from typing import Any, Dict, Iterable, Optional, IO
from typing_extensions import override
from typing import Any, Optional, IO
import paho.mqtt.client as mqtt
from platypush.config import Config
from platypush.context import get_bus
from platypush.message import Message
from platypush.plugins import Plugin, action
from platypush.message.event.mqtt import MQTTMessageEvent
from platypush.message.request import Request
from platypush.plugins import RunnablePlugin, action
from platypush.utils import get_message_response
from ._client import DEFAULT_TIMEOUT, MqttCallback, MqttClient
class MqttPlugin(Plugin):
class MqttPlugin(RunnablePlugin):
"""
This plugin allows you to send custom message to a message queue compatible
with the MQTT protocol, see https://mqtt.org/
@ -19,253 +28,131 @@ class MqttPlugin(Plugin):
* **paho-mqtt** (``pip install paho-mqtt``)
Triggers:
* :class:`platypush.message.event.mqtt.MQTTMessageEvent` when a new
message is received on a subscribed topic.
"""
def __init__(
self,
host=None,
port=1883,
tls_cafile=None,
tls_certfile=None,
tls_keyfile=None,
tls_version=None,
tls_ciphers=None,
tls_insecure=False,
username=None,
password=None,
client_id=None,
timeout=None,
host: Optional[str] = None,
port: int = 1883,
topics: Optional[Iterable[str]] = None,
tls_cafile: Optional[str] = None,
tls_certfile: Optional[str] = None,
tls_keyfile: Optional[str] = None,
tls_version: Optional[str] = None,
tls_ciphers: Optional[str] = None,
tls_insecure: bool = False,
username: Optional[str] = None,
password: Optional[str] = None,
client_id: Optional[str] = None,
timeout: Optional[int] = DEFAULT_TIMEOUT,
run_topic_prefix: Optional[str] = None,
listeners: Optional[Iterable[dict]] = None,
**kwargs,
):
"""
:param host: If set, MQTT messages will by default routed to this host
unless overridden in `send_message` (default: None)
:type host: str
:param port: If a default host is set, specify the listen port
(default: 1883)
:type port: int
:param topics: If a default ``host`` is specified, then this list will
include a default list of topics that should be subscribed on that
broker at startup.
:param tls_cafile: If a default host is set and requires TLS/SSL,
specify the certificate authority file (default: None)
:type tls_cafile: str
:param tls_certfile: If a default host is set and requires TLS/SSL,
specify the certificate file (default: None)
:type tls_certfile: str
:param tls_keyfile: If a default host is set and requires TLS/SSL,
specify the key file (default: None)
:type tls_keyfile: str
:param tls_version: If TLS/SSL is enabled on the MQTT server and it
requires a certain TLS version, specify it here (default: None).
Supported versions: ``tls`` (automatic), ``tlsv1``, ``tlsv1.1``,
``tlsv1.2``.
:type tls_version: str
:param tls_ciphers: If a default host is set and requires TLS/SSL,
specify the supported ciphers (default: None)
:type tls_ciphers: str
:param tls_insecure: Set to True to ignore TLS insecure warnings
(default: False).
:type tls_insecure: bool
:param username: If a default host is set and requires user
authentication, specify the username ciphers (default: None)
:type username: str
:param password: If a default host is set and requires user
authentication, specify the password ciphers (default: None)
:type password: str
:param client_id: ID used to identify the client on the MQTT server
(default: None). If None is specified then
``Config.get('device_id')`` will be used.
:type client_id: str
:param timeout: Client timeout in seconds (default: 30 seconds).
:param run_topic_prefix: If specified, the MQTT plugin will listen for
messages on a topic in the format `{run_topic_prefix}/{device_id}.
When a message is received, it will interpret it as a JSON request
to execute, in the format
``{"type": "request", "action": "plugin.action", "args": {...}}``.
.. warning:: This parameter is mostly kept for backwards
compatibility, but you should avoid it - unless the MQTT broker
is on a personal safe network that you own, or it requires
user authentication and it uses SSL. The reason is that the
messages received on this topic won't be subject to token
verification, allowing unauthenticated arbitrary command
execution on the target host. If you still want the ability of
running commands remotely over an MQTT broker, then you may
consider creating a dedicated topic listener with an attached
event hook on
:class:`platypush.message.event.mqtt.MQTTMessageEvent`. The
hook can implement whichever authentication logic you like.
:param listeners: If specified, the MQTT plugin will listen for
messages on these topics. Use this parameter if you also want to
listen on other MQTT brokers other than the primary one. This
parameter supports a list of maps, where each item supports the
same arguments passed to the main configuration (host, port, topic,
password etc.). If host/port are omitted, then the host/port value
from the plugin configuration will be used. If any of the other
fields are omitted, then their default value will be used (usually
null). Example:
.. code-block:: yaml
listeners:
# This listener use the default configured host/port
- topics:
- topic1
- topic2
- topic3
# This will use a custom MQTT broker host
- host: sensors
port: 11883
username: myuser
password: secret
topics:
- topic4
- topic5
:param timeout: Client timeout in seconds (default: None).
:type timeout: int
"""
super().__init__(**kwargs)
self.host = host
self.port = port
self.username = username
self.password = password
self.client_id = client_id or Config.get('device_id')
self.tls_cafile = self._expandpath(tls_cafile) if tls_cafile else None
self.tls_certfile = self._expandpath(tls_certfile) if tls_certfile else None
self.tls_keyfile = self._expandpath(tls_keyfile) if tls_keyfile else None
self.tls_version = self.get_tls_version(tls_version)
self.tls_insecure = tls_insecure
self.tls_ciphers = tls_ciphers
self.client_id = client_id or str(Config.get('device_id'))
self.run_topic = (
f'{run_topic_prefix}/{Config.get("device_id")}'
if type(self) == MqttPlugin and run_topic_prefix
else None
)
self._listeners_lock = defaultdict(threading.RLock)
self.listeners: Dict[str, MqttClient] = {} # client_id -> MqttClient map
self.timeout = timeout
@staticmethod
def get_tls_version(version: Optional[str] = None):
import ssl
if not version:
return None
if isinstance(version, type(ssl.PROTOCOL_TLS)):
return version
if isinstance(version, str):
version = version.lower()
if version == 'tls':
return ssl.PROTOCOL_TLS
if version == 'tlsv1':
return ssl.PROTOCOL_TLSv1
if version == 'tlsv1.1':
return ssl.PROTOCOL_TLSv1_1
if version == 'tlsv1.2':
return ssl.PROTOCOL_TLSv1_2
assert f'Unrecognized TLS version: {version}'
def _mqtt_args(self, **kwargs):
return {
'host': kwargs.get('host', self.host),
'port': kwargs.get('port', self.port),
'timeout': kwargs.get('timeout', self.timeout),
'tls_certfile': kwargs.get('tls_certfile', self.tls_certfile),
'tls_keyfile': kwargs.get('tls_keyfile', self.tls_keyfile),
'tls_version': kwargs.get('tls_version', self.tls_version),
'tls_ciphers': kwargs.get('tls_ciphers', self.tls_ciphers),
'username': kwargs.get('username', self.username),
'password': kwargs.get('password', self.password),
}
@staticmethod
def _expandpath(path: Optional[str] = None) -> Optional[str]:
return os.path.abspath(os.path.expanduser(path)) if path else None
def _get_client(
self,
tls_cafile: Optional[str] = None,
tls_certfile: Optional[str] = None,
tls_keyfile: Optional[str] = None,
tls_version: Optional[str] = None,
tls_ciphers: Optional[str] = None,
tls_insecure: Optional[bool] = None,
username: Optional[str] = None,
password: Optional[str] = None,
):
from paho.mqtt.client import Client
tls_cafile = self._expandpath(tls_cafile or self.tls_cafile)
tls_certfile = self._expandpath(tls_certfile or self.tls_certfile)
tls_keyfile = self._expandpath(tls_keyfile or self.tls_keyfile)
tls_ciphers = tls_ciphers or self.tls_ciphers
username = username or self.username
password = password or self.password
tls_version = tls_version or self.tls_version # type: ignore[reportGeneralTypeIssues]
if tls_version:
tls_version = self.get_tls_version(tls_version) # type: ignore[reportGeneralTypeIssues]
if tls_insecure is None:
tls_insecure = self.tls_insecure
client = Client()
if username and password:
client.username_pw_set(username, password)
if tls_cafile:
client.tls_set(
ca_certs=tls_cafile,
certfile=tls_certfile,
keyfile=tls_keyfile,
tls_version=tls_version, # type: ignore[reportGeneralTypeIssues]
ciphers=tls_ciphers,
)
client.tls_insecure_set(tls_insecure)
return client
@action
def publish(
self,
topic: str,
msg: Any,
host: Optional[str] = None,
port: Optional[int] = None,
reply_topic: Optional[str] = None,
timeout: int = 60,
tls_cafile: Optional[str] = None,
tls_certfile: Optional[str] = None,
tls_keyfile: Optional[str] = None,
tls_version: Optional[str] = None,
tls_ciphers: Optional[str] = None,
tls_insecure: Optional[bool] = None,
username: Optional[str] = None,
password: Optional[str] = None,
qos: int = 0,
):
"""
Sends a message to a topic.
:param topic: Topic/channel where the message will be delivered
:param msg: Message to be sent. It can be a list, a dict, or a Message
object.
:param host: MQTT broker hostname/IP (default: default host configured
on the plugin).
:param port: MQTT broker port (default: default port configured on the
plugin).
:param reply_topic: If a ``reply_topic`` is specified, then the action
will wait for a response on this topic.
:param timeout: If ``reply_topic`` is set, use this parameter to
specify the maximum amount of time to wait for a response (default:
60 seconds).
:param tls_cafile: If TLS/SSL is enabled on the MQTT server and the
certificate requires a certificate authority to authenticate it,
`ssl_cafile` will point to the provided ca.crt file (default:
None).
:param tls_certfile: If TLS/SSL is enabled on the MQTT server and a
client certificate it required, specify it here (default: None).
:param tls_keyfile: If TLS/SSL is enabled on the MQTT server and a
client certificate key it required, specify it here (default:
None).
:param tls_version: If TLS/SSL is enabled on the MQTT server and it
requires a certain TLS version, specify it here (default: None).
Supported versions: ``tls`` (automatic), ``tlsv1``, ``tlsv1.1``,
``tlsv1.2``.
:param tls_insecure: Set to True to ignore TLS insecure warnings
(default: False).
:param tls_ciphers: If TLS/SSL is enabled on the MQTT server and an
explicit list of supported ciphers is required, specify it here
(default: None).
:param username: Specify it if the MQTT server requires authentication
(default: None).
:param password: Specify it if the MQTT server requires authentication
(default: None).
:param qos: Quality of Service (_QoS_) for the message - see `MQTT QoS
<https://assetwolf.com/learn/mqtt-qos-understanding-quality-of-service>`_
(default: 0).
"""
response_buffer = io.BytesIO()
client = None
try:
# Try to parse it as a platypush message or dump it to JSON from a dict/list
if isinstance(msg, (dict, list)):
msg = json.dumps(msg)
try:
msg = Message.build(json.loads(msg))
except Exception as e:
self.logger.debug('Not a valid JSON: %s', e)
host = host or self.host
port = port or self.port or 1883
assert host, 'No host specified'
client = self._get_client(
self.default_listener = (
self._get_client(
host=host,
port=port,
topics=(
(tuple(topics) if topics else ())
+ ((self.run_topic,) if self.run_topic else ())
),
on_message=self.on_mqtt_message(),
tls_cafile=tls_cafile,
tls_certfile=tls_certfile,
tls_keyfile=tls_keyfile,
@ -274,11 +161,258 @@ class MqttPlugin(Plugin):
tls_insecure=tls_insecure,
username=username,
password=password,
client_id=client_id,
timeout=timeout,
)
if host
else None
)
for listener in listeners or []:
self._get_client(
**self._mqtt_args(on_message=self.on_mqtt_message(), **listener)
)
client.connect(host, port, keepalive=timeout)
def _get_client_id(
self,
host: str,
port: int,
client_id: Optional[str] = None,
on_message: Optional[MqttCallback] = None,
topics: Iterable[str] = (),
**_,
) -> str:
"""
Calculates a unique client ID given an MQTT configuration.
"""
client_id = client_id or self.client_id
client_hash = hashlib.sha1(
'|'.join(
[
self.__class__.__name__,
host,
str(port),
json.dumps(sorted(topics)),
str(id(on_message)),
]
).encode()
).hexdigest()
return f'{client_id}-{client_hash}'
def _mqtt_args(
self,
host: Optional[str] = None,
port: int = 1883,
timeout: Optional[int] = DEFAULT_TIMEOUT,
topics: Iterable[str] = (),
**kwargs,
):
"""
:return: An MQTT configuration mapping that uses either the specified
arguments (if host is specified), or falls back to the default
configurated arguments.
"""
default_conf = (
self.default_listener.configuration if self.default_listener else {}
)
if not host:
assert (
self.default_listener
), 'No host specified and no configured default host'
return {
**default_conf,
'topics': (*self.default_listener.topics, *topics),
}
return {
'host': host,
'port': port,
'timeout': timeout or default_conf.get('timeout'),
'topics': topics,
**kwargs,
}
def on_mqtt_message(self) -> MqttCallback:
"""
Default MQTT message handler. It forwards a
:class:`platypush.message.event.mqtt.MQTTMessageEvent` event to the
bus.
"""
def handler(client: MqttClient, _, msg: mqtt.MQTTMessage):
data = msg.payload
try:
data = data.decode('utf-8')
data = json.loads(data)
except (TypeError, AttributeError, ValueError):
# Not a serialized JSON
pass
if self.default_listener and msg.topic == self.run_topic:
try:
app_msg = Message.build(data)
self.on_exec_message(client, app_msg)
except Exception as e:
self.logger.warning(
'Message execution error: %s: %s', type(e).__name__, str(e)
)
else:
get_bus().post(
MQTTMessageEvent(
host=client.host, port=client.port, topic=msg.topic, msg=data
)
)
return handler
def on_exec_message(self, client: MqttClient, msg):
"""
Message handler for (legacy) application requests over MQTT.
"""
def response_thread(req: Request):
"""
A separate thread to handle the response to a request.
"""
if not self.run_topic:
return
response = get_message_response(req)
if not response:
return
response_topic = f'{self.run_topic}/responses/{req.id}'
self.logger.info(
'Processing response on the MQTT topic %s: %s',
response_topic,
response,
)
client.publish(payload=str(response), topic=response_topic)
self.logger.info('Received message on the MQTT backend: %s', msg)
try:
get_bus().post(msg)
except Exception as e:
self.logger.exception(e)
return
if isinstance(msg, Request):
threading.Thread(
target=response_thread,
name='MQTTProcessorResponseThread',
args=(msg,),
).start()
def _get_client(
self,
host: Optional[str] = None,
port: int = 1883,
topics: Iterable[str] = (),
client_id: Optional[str] = None,
on_message: Optional[MqttCallback] = None,
**kwargs,
) -> MqttClient:
"""
:return: A :class:`platypush.message.event.mqtt.MqttClient` instance.
It will return the existing client with the given inferred ID if it
already exists, or it will register a new one.
"""
if host:
kwargs['host'] = host
kwargs['port'] = port
else:
assert (
self.default_listener
), 'No host specified and no configured default host'
kwargs = self.default_listener.configuration
kwargs.update(
{
'topics': topics,
'on_message': on_message,
'client_id': client_id,
}
)
on_message = on_message or self.on_mqtt_message()
client_id = self._get_client_id(
host=kwargs['host'],
port=kwargs['port'],
client_id=client_id,
on_message=on_message,
topics=topics,
)
kwargs['client_id'] = client_id
with self._listeners_lock[client_id]:
client = self.listeners.get(client_id)
if not (client and client.is_alive()):
client = self.listeners[
client_id
] = MqttClient( # pylint: disable=E1125
**kwargs
)
if topics:
client.subscribe(*topics)
return client
@action
def publish(
self,
topic: str,
msg: Any,
qos: int = 0,
reply_topic: Optional[str] = None,
**mqtt_kwargs,
):
"""
Sends a message to a topic.
:param topic: Topic/channel where the message will be delivered
:param msg: Message to be sent. It can be a list, a dict, or a Message
object.
:param qos: Quality of Service (_QoS_) for the message - see `MQTT QoS
<https://assetwolf.com/learn/mqtt-qos-understanding-quality-of-service>`_
(default: 0).
:param reply_topic: If a ``reply_topic`` is specified, then the action
will wait for a response on this topic.
:param mqtt_kwargs: MQTT broker configuration (host, port, username,
password etc.). See :meth:`.__init__` parameters.
"""
response_buffer = io.BytesIO()
client = None
try:
# Try to parse it as a Platypush message or dump it to JSON from a dict/list
if isinstance(msg, (dict, list)):
msg = json.dumps(msg)
try:
msg = Message.build(json.loads(msg))
except (KeyError, TypeError, ValueError):
pass
client = self._get_client(**mqtt_kwargs)
client.connect()
response_received = threading.Event()
# If it's a request, then wait for the response
if (
isinstance(msg, Request)
and self.default_listener
and client.host == self.default_listener.host
and self.run_topic
and topic == self.run_topic
):
reply_topic = f'{self.run_topic}/responses/{msg.id}'
if reply_topic:
client.on_message = self._response_callback(
reply_topic=reply_topic,
@ -289,12 +423,13 @@ class MqttPlugin(Plugin):
client.publish(topic, str(msg), qos=qos)
if not reply_topic:
return
return None
client.loop_start()
ok = response_received.wait(timeout=timeout)
ok = response_received.wait(timeout=client.timeout)
if not ok:
raise TimeoutError('Response timed out')
return response_buffer.getvalue()
finally:
response_buffer.close()
@ -303,12 +438,50 @@ class MqttPlugin(Plugin):
try:
client.loop_stop()
except Exception as e:
self.logger.warning('Could not stop client loop: %s', e)
self.logger.warning(
'Could not stop client loop: %s: %s', type(e).__name__, e
)
client.disconnect()
@action
def subscribe(self, topic: str, **mqtt_kwargs):
"""
Programmatically subscribe to a topic on an MQTT broker.
Messages received on this topic will trigger a
:class:`platypush.message.event.mqtt.MQTTMessageEvent` event that you
can subscribe to.
:param topic: Topic to subscribe to.
:param mqtt_kwargs: MQTT broker configuration (host, port, username,
password etc.). See :meth:`.__init__` parameters.
"""
client = self._get_client(
topics=(topic,), on_message=self.on_mqtt_message(), **mqtt_kwargs
)
if not client.is_alive():
client.start()
@action
def unsubscribe(self, topic: str, **mqtt_kwargs):
"""
Programmatically unsubscribe from a topic on an MQTT broker.
:param topic: Topic to unsubscribe from.
:param mqtt_kwargs: MQTT broker configuration (host, port, username,
password etc.). See :meth:`.__init__` parameters.
"""
self._get_client(**mqtt_kwargs).unsubscribe(topic)
@staticmethod
def _response_callback(reply_topic: str, event: threading.Event, buffer: IO[bytes]):
"""
A response callback that writes the response to an IOBuffer and stops
the client loop.
"""
def on_message(client, _, msg):
if msg.topic != reply_topic:
return
@ -322,9 +495,40 @@ class MqttPlugin(Plugin):
@action
def send_message(self, *args, **kwargs):
"""
Alias for :meth:`platypush.plugins.mqtt.MqttPlugin.publish`.
Legacy alias for :meth:`platypush.plugins.mqtt.MqttPlugin.publish`.
"""
return self.publish(*args, **kwargs)
@override
def main(self):
if self.run_topic:
self.logger.warning(
'The MQTT integration is listening for commands on the topic %s.\n'
'This approach is unsafe, as it allows any client to run unauthenticated requests.\n'
'Please only enable it in test/trusted environments.',
self.run_topic,
)
for listener in self.listeners.values():
listener.start()
self.wait_stop()
@override
def stop(self):
"""
Disconnect all the clients upon plugin stop.
"""
for listener in self.listeners.values():
listener.stop()
super().stop()
for listener in self.listeners.values():
try:
listener.join(timeout=1)
except Exception:
pass
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1,244 @@
from enum import IntEnum
import logging
import os
import threading
from typing import Any, Callable, Dict, Final, Iterable, Optional, Union
import paho.mqtt.client as mqtt
from platypush.config import Config
MqttCallback = Callable[["MqttClient", Any, mqtt.MQTTMessage], Any]
DEFAULT_TIMEOUT: Final[int] = 30
class MqttClient(mqtt.Client, threading.Thread):
"""
Wrapper class for an MQTT client executed in a separate thread.
"""
def __init__(
self,
*args,
host: str,
port: int,
client_id: str,
topics: Iterable[str] = (),
on_message: Optional[MqttCallback] = None,
username: Optional[str] = None,
password: Optional[str] = None,
tls_cafile: Optional[str] = None,
tls_certfile: Optional[str] = None,
tls_keyfile: Optional[str] = None,
tls_version: Optional[Union[str, IntEnum]] = None,
tls_ciphers: Optional[str] = None,
tls_insecure: bool = False,
timeout: int = DEFAULT_TIMEOUT,
**kwargs,
):
self.client_id = client_id or str(Config.get('device_id'))
mqtt.Client.__init__(self, *args, client_id=self.client_id, **kwargs)
threading.Thread.__init__(self, name=f'MQTTClient:{self.client_id}')
self.logger = logging.getLogger(self.__class__.__name__)
self.host = host
self.port = port
self.tls_cafile = self._expandpath(tls_cafile)
self.tls_certfile = self._expandpath(tls_certfile)
self.tls_keyfile = self._expandpath(tls_keyfile)
self.tls_version = self._get_tls_version(tls_version)
self.tls_ciphers = self._expandpath(tls_ciphers)
self.tls_insecure = tls_insecure
self.username = username
self.password = password
self.topics = set(topics or [])
self.timeout = timeout
self.on_connect = self.connect_hndl()
self.on_disconnect = self.disconnect_hndl()
if on_message:
self.on_message = on_message # type: ignore
if username and password:
self.username_pw_set(username, password)
if tls_cafile:
self.tls_set(
ca_certs=self.tls_cafile,
certfile=self.tls_certfile,
keyfile=self.tls_keyfile,
tls_version=self.tls_version,
ciphers=self.tls_ciphers,
)
self.tls_insecure_set(self.tls_insecure)
self._running = False
self._stop_scheduled = False
@staticmethod
def _expandpath(path: Optional[str] = None) -> Optional[str]:
"""
Utility method to expand a path string.
"""
return os.path.abspath(os.path.expanduser(path)) if path else None
@staticmethod
def _get_tls_version(version: Optional[Union[str, IntEnum]] = None):
"""
A utility method that normalizes an SSL version string or enum to a
standard ``_SSLMethod`` enum.
"""
import ssl
if not version:
return None
if isinstance(version, type(ssl.PROTOCOL_TLS)):
return version
if isinstance(version, str):
version = version.lower()
if version == 'tls':
return ssl.PROTOCOL_TLS
if version == 'tlsv1':
return ssl.PROTOCOL_TLSv1
if version == 'tlsv1.1':
return ssl.PROTOCOL_TLSv1_1
if version == 'tlsv1.2':
return ssl.PROTOCOL_TLSv1_2
raise AssertionError(f'Unrecognized TLS version: {version}')
def connect(
self,
*args,
host: Optional[str] = None,
port: Optional[int] = None,
keepalive: Optional[int] = None,
**kwargs,
):
"""
Overrides the default connect method.
"""
if not self.is_connected():
self.logger.debug(
'Connecting to MQTT broker %s:%d, client_id=%s...',
self.host,
self.port,
self.client_id,
)
return super().connect(
host=host or self.host,
port=port or self.port,
keepalive=keepalive or self.timeout,
*args,
**kwargs,
)
return None
@property
def configuration(self) -> Dict[str, Any]:
"""
:return: The configuration of the client.
"""
return {
'host': self.host,
'port': self.port,
'topics': self.topics,
'on_message': self.on_message,
'username': self.username,
'password': self.password,
'client_id': self.client_id,
'tls_cafile': self.tls_cafile,
'tls_certfile': self.tls_certfile,
'tls_keyfile': self.tls_keyfile,
'tls_version': self.tls_version,
'tls_ciphers': self.tls_ciphers,
'tls_insecure': self.tls_insecure,
'timeout': self.timeout,
}
def subscribe(self, *topics, **kwargs):
"""
Client subscription handler.
"""
if not topics:
topics = self.topics
self.topics.update(topics)
for topic in topics:
super().subscribe(topic, **kwargs)
def unsubscribe(self, *topics, **kwargs):
"""
Client unsubscribe handler.
"""
if not topics:
topics = self.topics
for topic in topics:
if topic not in self.topics:
self.logger.info('The topic %s is not subscribed', topic)
continue
super().unsubscribe(topic, **kwargs)
self.topics.remove(topic)
def connect_hndl(self):
"""
When the client connects, subscribe to all the registered topics.
"""
def handler(*_, **__):
self.logger.debug(
'Connected to MQTT broker %s:%d, client_id=%s',
self.host,
self.port,
self.client_id,
)
self.subscribe()
return handler
def disconnect_hndl(self):
"""
Notifies the client disconnection.
"""
def handler(*_, **__):
self.logger.debug(
'Disconnected from MQTT broker %s:%d, client_id=%s',
self.host,
self.port,
self.client_id,
)
return handler
def run(self):
"""
Connects to the MQTT server, subscribes to all the registered topics
and listens for messages.
"""
super().run()
self.connect()
self._running = True
self.loop_forever()
def stop(self):
"""
The stop method schedules the stop and disconnects the client.
"""
if not self.is_alive():
return
self._stop_scheduled = True
self.disconnect()
self._running = False
# vim:sw=4:ts=4:et:

View file

@ -1,5 +1,6 @@
manifest:
events: {}
events:
- platypush.message.event.mqtt.MQTTMessageEvent
install:
apk:
- py3-paho-mqtt

File diff suppressed because it is too large Load diff

View file

@ -1,269 +0,0 @@
import contextlib
import json
from typing import Mapping
from platypush.backend.mqtt import MqttBackend
from platypush.bus import Bus
from platypush.context import get_bus, get_plugin
from platypush.message.event.zigbee.mqtt import (
ZigbeeMqttOnlineEvent,
ZigbeeMqttOfflineEvent,
ZigbeeMqttDevicePropertySetEvent,
ZigbeeMqttDevicePairingEvent,
ZigbeeMqttDeviceConnectedEvent,
ZigbeeMqttDeviceBannedEvent,
ZigbeeMqttDeviceRemovedEvent,
ZigbeeMqttDeviceRemovedFailedEvent,
ZigbeeMqttDeviceWhitelistedEvent,
ZigbeeMqttDeviceRenamedEvent,
ZigbeeMqttDeviceBindEvent,
ZigbeeMqttDeviceUnbindEvent,
ZigbeeMqttGroupAddedEvent,
ZigbeeMqttGroupAddedFailedEvent,
ZigbeeMqttGroupRemovedEvent,
ZigbeeMqttGroupRemovedFailedEvent,
ZigbeeMqttGroupRemoveAllEvent,
ZigbeeMqttGroupRemoveAllFailedEvent,
ZigbeeMqttErrorEvent,
)
from platypush.plugins.zigbee.mqtt import ZigbeeMqttPlugin
class ZigbeeMqttListener(MqttBackend):
"""
Listener for zigbee2mqtt events.
"""
def __init__(self):
plugin = self._plugin
self.base_topic = plugin.base_topic # type: ignore
self._devices = {}
self._devices_info = {}
self._groups = {}
self._last_state = None
self.server_info = {
'host': plugin.host, # type: ignore
'port': plugin.port or self._default_mqtt_port, # type: ignore
'tls_cafile': plugin.tls_cafile, # type: ignore
'tls_certfile': plugin.tls_certfile, # type: ignore
'tls_ciphers': plugin.tls_ciphers, # type: ignore
'tls_keyfile': plugin.tls_keyfile, # type: ignore
'tls_version': plugin.tls_version, # type: ignore
'username': plugin.username, # type: ignore
'password': plugin.password, # type: ignore
}
listeners = [
{
**self.server_info,
'topics': [
self.base_topic + '/' + topic
for topic in [
'bridge/state',
'bridge/log',
'bridge/logging',
'bridge/devices',
'bridge/groups',
]
],
}
]
super().__init__(
subscribe_default_topic=False, listeners=listeners, **self.server_info
)
assert self.client_id
self.client_id += '-zigbee-mqtt'
def _process_state_message(self, client, msg):
if msg == self._last_state:
return
if msg == 'online':
evt = ZigbeeMqttOnlineEvent
elif msg == 'offline':
evt = ZigbeeMqttOfflineEvent
self.logger.warning('zigbee2mqtt service is offline')
else:
return
self._bus.post(evt(host=client._host, port=client._port))
self._last_state = msg
def _process_log_message(self, client, msg):
msg_type = msg.get('type')
text = msg.get('message')
args = {'host': client._host, 'port': client._port}
if msg_type == 'devices':
devices = {}
for dev in text or []:
devices[dev['friendly_name']] = dev
client.subscribe(self.base_topic + '/' + dev['friendly_name'])
elif msg_type == 'pairing':
self._bus.post(ZigbeeMqttDevicePairingEvent(device=text, **args))
elif msg_type in ['device_ban', 'device_banned']:
self._bus.post(ZigbeeMqttDeviceBannedEvent(device=text, **args))
elif msg_type in ['device_removed_failed', 'device_force_removed_failed']:
force = msg_type == 'device_force_removed_failed'
self._bus.post(
ZigbeeMqttDeviceRemovedFailedEvent(device=text, force=force, **args)
)
elif msg_type == 'device_whitelisted':
self._bus.post(ZigbeeMqttDeviceWhitelistedEvent(device=text, **args))
elif msg_type == 'device_renamed':
self._bus.post(ZigbeeMqttDeviceRenamedEvent(device=text, **args))
elif msg_type == 'device_bind':
self._bus.post(ZigbeeMqttDeviceBindEvent(device=text, **args))
elif msg_type == 'device_unbind':
self._bus.post(ZigbeeMqttDeviceUnbindEvent(device=text, **args))
elif msg_type == 'device_group_add':
self._bus.post(ZigbeeMqttGroupAddedEvent(group=text, **args))
elif msg_type == 'device_group_add_failed':
self._bus.post(ZigbeeMqttGroupAddedFailedEvent(group=text, **args))
elif msg_type == 'device_group_remove':
self._bus.post(ZigbeeMqttGroupRemovedEvent(group=text, **args))
elif msg_type == 'device_group_remove_failed':
self._bus.post(ZigbeeMqttGroupRemovedFailedEvent(group=text, **args))
elif msg_type == 'device_group_remove_all':
self._bus.post(ZigbeeMqttGroupRemoveAllEvent(group=text, **args))
elif msg_type == 'device_group_remove_all_failed':
self._bus.post(ZigbeeMqttGroupRemoveAllFailedEvent(group=text, **args))
elif msg_type == 'zigbee_publish_error':
self.logger.error('zigbee2mqtt error: {}'.format(text))
self._bus.post(ZigbeeMqttErrorEvent(error=text, **args))
elif msg.get('level') in ['warning', 'error']:
log = getattr(self.logger, msg['level'])
log(
'zigbee2mqtt {}: {}'.format(
msg['level'], text or msg.get('error', msg.get('warning'))
)
)
def _process_devices(self, client, msg):
devices_info = {
device.get('friendly_name', device.get('ieee_address')): device
for device in msg
}
# noinspection PyProtectedMember
event_args = {'host': client._host, 'port': client._port}
client.subscribe(
*[self.base_topic + '/' + device for device in devices_info.keys()]
)
for name, device in devices_info.items():
if name not in self._devices:
self._bus.post(
ZigbeeMqttDeviceConnectedEvent(device=name, **event_args)
)
exposes = (device.get('definition', {}) or {}).get('exposes', [])
payload = self._plugin._build_device_get_request(exposes) # type: ignore
if payload:
client.publish(
self.base_topic + '/' + name + '/get',
json.dumps(payload),
)
devices_copy = [*self._devices.keys()]
for name in devices_copy:
if name not in devices_info:
self._bus.post(ZigbeeMqttDeviceRemovedEvent(device=name, **event_args))
del self._devices[name]
self._devices = {device: {} for device in devices_info.keys()}
self._devices_info = devices_info
def _process_groups(self, client, msg):
# noinspection PyProtectedMember
event_args = {'host': client._host, 'port': client._port}
groups_info = {
group.get('friendly_name', group.get('id')): group for group in msg
}
for name in groups_info.keys():
if name not in self._groups:
self._bus.post(ZigbeeMqttGroupAddedEvent(group=name, **event_args))
groups_copy = [*self._groups.keys()]
for name in groups_copy:
if name not in groups_info:
self._bus.post(ZigbeeMqttGroupRemovedEvent(group=name, **event_args))
del self._groups[name]
self._groups = {group: {} for group in groups_info.keys()}
def on_mqtt_message(self):
def handler(client, _, msg):
topic = msg.topic[len(self.base_topic) + 1 :]
data = msg.payload.decode()
if not data:
return
with contextlib.suppress(ValueError, TypeError):
data = json.loads(data)
if topic == 'bridge/state':
self._process_state_message(client, data)
elif topic in ['bridge/log', 'bridge/logging']:
self._process_log_message(client, data)
elif topic == 'bridge/devices':
self._process_devices(client, data)
elif topic == 'bridge/groups':
self._process_groups(client, data)
else:
suffix = topic.split('/')[-1]
if suffix not in self._devices:
return
name = suffix
changed_props = {
k: v for k, v in data.items() if v != self._devices[name].get(k)
}
if changed_props:
self._process_property_update(name, data)
self._bus.post(
ZigbeeMqttDevicePropertySetEvent(
host=client._host,
port=client._port,
device=name,
properties=changed_props,
)
)
self._devices[name].update(data)
return handler
@property
def _plugin(self) -> ZigbeeMqttPlugin:
plugin = get_plugin('zigbee.mqtt')
assert plugin, 'The zigbee.mqtt plugin is not configured'
return plugin
@property
def _bus(self) -> Bus:
return get_bus()
def _process_property_update(self, device_name: str, properties: Mapping):
device_info = self._devices_info.get(device_name)
if not (device_info and properties):
return
self._plugin.publish_entities( # type: ignore
[
{
**device_info,
'state': properties,
}
]
)
def run(self):
super().run()
# vim:sw=4:ts=4:et:

Some files were not shown because too many files have changed in this diff Show more