diff --git a/__init__.py b/__init__.py index 044fe8250..09102aba6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ def plugin_release(): - return '1.7' + return '1.7.1' def plugin_branch(): diff --git a/alexa4p3/Create IAM-role and Lambda.pdf b/alexa4p3/Create IAM-role and Lambda.pdf deleted file mode 100644 index c3a48fe34..000000000 Binary files a/alexa4p3/Create IAM-role and Lambda.pdf and /dev/null differ diff --git a/alexa4p3/Hoerprobe.mp3 b/alexa4p3/Hoerprobe.mp3 deleted file mode 100644 index 22f98f325..000000000 Binary files a/alexa4p3/Hoerprobe.mp3 and /dev/null differ diff --git a/alexa4p3/How to create a new Smarthome Skill for SmartHomeNG 2019-01.pdf b/alexa4p3/How to create a new Smarthome Skill for SmartHomeNG 2019-01.pdf deleted file mode 100644 index 602c768f6..000000000 Binary files a/alexa4p3/How to create a new Smarthome Skill for SmartHomeNG 2019-01.pdf and /dev/null differ diff --git a/alexa4p3/README.md b/alexa4p3/README.md old mode 100644 new mode 100755 index 734a4a26f..fa8b4849c --- a/alexa4p3/README.md +++ b/alexa4p3/README.md @@ -4,23 +4,33 @@ 1. [Generell](#generell) 2. [Change Log](#changelog) -3. [Icon / Display Categories](#Icons) -4. [Entwicklung / Einbau von neuen Skills](#Entwicklung) -5. [Alexa-ThermostatController](#ThermostatController) + [Thermosensor](#Thermostatsensor) -6. [Alexa-PowerController](#PowerController) -7. [Alexa-BrightnessController](#BrightnessController) -8. [Alexa-PowerLevelController](#PowerLevelController) -9. [Alexa-PercentageController](#PercentageController) -10. [Alexa-LockController](#LockController) **Update** -11. [Alexa-CameraStreamController](#CameraStreamController) **Update** -12. [Alexa-SceneController](#SceneController) -13. [Alexa-ContactSensor](#ContactSensor) **Neu** -14. [Alexa-ColorController](#ColorController) **Neu** - - +3. [Requrirements](#requirements) +4. [Icon / Display Categories](#Icons) **Update** +5. [Entwicklung / Einbau von neuen Skills](#Entwicklung) +6. [Alexa-ThermostatController](#ThermostatController) + [Thermosensor](#Thermostatsensor) +7. [Alexa-PowerController](#PowerController) +8. [Alexa-BrightnessController](#BrightnessController) +9. [Alexa-PowerLevelController](#PowerLevelController) +10. [Alexa-PercentageController](#PercentageController) +11. [Alexa-LockController](#LockController) +12. [Alexa-CameraStreamController](#CameraStreamController) **Update** +13. [Alexa-SceneController](#SceneController) +14. [Alexa-ContactSensor](#ContactSensor) +15. [Alexa-ColorController](#ColorController) +16. [Alexa-RangeController](#RangeController) **Neu** +17. [Alexa-ColorTemperaturController](#ColorTemperaturController) **Neu** +18. [Alexa-PlaybackController](#PlaybackController) **Neu** +19. [Web-Interface](#webinterface) **Neu** + + +## [Beispiel-Konfigurationen](#Beispiel) **Neu** + + +- [der fast perfekte Rolladen](#perfect_blind) **Neu** +- [Items in Abhängikeit des letzten benutzten Echos-Devices schalten](#get_last_alexa) **Neu** # -------------------------------------- -##Generell +## Generell Die Daten des Plugin müssen in den Ordner /usr/local/smarthome/plugins/alexa4p3/ (wie gewohnt) Die Rechte entsprechend setzen. @@ -34,8 +44,7 @@ Das Plugin muss in der plugin.yaml eingefügt werden :

 Alexa4P3:
-    class_name: Alexa4P3
-    class_path: plugins.alexa4p3
+    plugin_name: Alexa4P3
     service_port: 9000
 
@@ -57,20 +66,48 @@ In den Items sind die "neuen" V3 Actions zu definieren : Zum Beispiel : PayloadV2 : turnon + PayloadV3 : TurnOn Die Actions unterscheiden sich zwischen Payload V2 und V3 oft nur durch Gross/Klein-Schreibung -##Change Log -###17.02.2019 + +## Change Log + +### 11.04.2020 +- Version auf 1.0.2 für shNG Release 1.7 erhöht + +### 12.03.2020 +- Ergänzung bei Wertänderung durch das Plugin wid der "Plugin Identifier" "alexa4p3" an die Change Item-Methode übegeben (PR #332) + +### 07.12.2019 +- Web-Interface um Protokoll-Log ergänzt +- PlaybackController realisiert +- bux-fix for alias-Devices, es wurden nicht alle Eigenschaften an das Alias-Device übergeben. Voice-Steuerung funktionierte, Darstellung in der App war nicht korrekt. + +### 06.12.2019 - zum Nikolaus :-) +- RangeController mit global "utterances" für Rolladen realisiert - endlich "Alexa, mach den Rolladen zu/auf - hoch/runter" + +### 01.12.2019 +- Web-Interface ergänzt +- Prüfung auf Verwendung von gemischtem Payload V2/V3 im Web-Interface +- Bug-Fix bei falsch definierten Devices (alexa_name fehlt) - Issue #300 - diese werden entfernt und ein Log-Eintrag erfolgt +- Bug-Fix alexa-description (PR #292) - die Beschreibung in der App lautet nun "device.name" + "by smarthomeNG" +- alexa_description beim Geräte Discovery ergänzt + +### 20.04.2019 +- Authentifizierungsdaten (Credentials) für AlexaCamProxy eingebaut +- Umbennung des Plugin-Pfades auf "alexa4p3" !! Hier die Einträge in der plugin.yaml anpassen. + +### 17.02.2019 - Version erhöht aktuell 1.0.1 - CameraStreamController Integration für Beta-Tests fertiggestellt -###26.01.2019 +### 26.01.2019 - ColorController eingebaut - Doku für ColorController erstellt - Neues Attribut für CameraStreamController (**alexa_csc_proxy_uri**) zum streamen von Kameras in lokalen Netzwerken in Verbindung mit CamProxy4AlexaP3 -###19.01.2019 +### 19.01.2019 - Version auf 1.0.0.2 erhöht - ContactSensor Interface eingebaut - Doku für ContactSensor Interface ergänzt @@ -79,7 +116,7 @@ Die Actions unterscheiden sich zwischen Payload V2 und V3 oft nur durch Gross/Kl - ReportLockState eingebaut - Doku für die Erstellung des Alexa-Skill´s auf Amazon als PDF erstellt -###31.12.2018 +### 31.12.2018 - Version auf 1.0.0.1 erhöht - CameraStreamController eingebaut - Dokumentation für CameraStreamController ergänzt @@ -87,116 +124,177 @@ Die Actions unterscheiden sich zwischen Payload V2 und V3 oft nur durch Gross/Kl - Dokumentation für PowerLevelController ergänzt - Debugs und Testfunktionen kontrolliert und für Upload entfernt -###24.12.2018 +### 24.12.2018 - Doku für PercentageController erstellt - Bug Fix für fehlerhafte Testfunktionen aus der Lambda -###12.12.2018 +### 12.12.2018 - Scene Controller eingebaut - Doku für Scene Controller erstellt - PercentageController eingebaut -##Icons / Catagories + +## Requrirements + +Das Plugin benötigt Modul Python-Requests. Dies sollte mit dem Core immer auf dem aktuellen Stand mitkommen. + +Ansonsten keine Requirements. + +## Icons / Catagories Optional kann im Item angegeben werden welches Icon in der Alexa-App verwendet werden soll :

 alexa_icon = "LIGHT"
 
 
-  
++    +    +
+
+    
+        
+        
+    
+
+
+    
+        
+        
+    
+    
+        
+        
+    
-      
-      
-      
+        
+        
-  
-  
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-      
-      
-      
+        
+        
-  
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+    
+        
+        
+    
+
ValueDescription
ACTIVITY_TRIGGERA combination of devices set to a specific state. Use activity triggers for scenes when the state changes must occur in a specific order. For example, for a scene named "watch Netflix" you might power on the TV first, and then set the input to HDMI1.
CAMERAA media device with video or photo functionality.
ValueDescriptionNotesCOMPUTERA non-mobile computer, such as a desktop computer.
ACTIVITY_TRIGGERDescribes a combination of devices set to a specific state, when the state change must occur in a specific order. For example, a "watch Netflix" scene might require the: 1. TV to be powered on & 2. Input set to HDMI1.Applies to ScenesCONTACT_SENSORAn endpoint that detects and reports changes in contact between two surfaces.
CAMERAIndicates media devices with video or photo capabilities. DOORA door.
CONTACT_SENSORIndicates an endpoint that detects and reports changes in contact between two surfaces. DOORBELLA doorbell.
DOORIndicates a door. EXTERIOR_BLINDA window covering on the outside of a structure.
DOORBELLIndicates a doorbell. FANA fan.
LIGHTIndicates light sources or fixtures. GAME_CONSOLEA game console, such as Microsoft Xbox or Nintendo Switch
MICROWAVEIndicates a microwave oven endpoint. GARAGE_DOORA garage door. Garage doors must implement the ModeController interface to open and close the door.
MOTION_SENSORIndicates an endpoint that detects and reports movement in an area. INTERIOR_BLINDA window covering on the inside of a structure.
OTHERAn endpoint that cannot be described in one of the other categories. LAPTOPA laptop or other mobile computer.
SCENE_TRIGGERDescribes a combination of devices set to a specific state, when the order of the state change is not important. For example a bedtime scene might include turning off lights and lowering the thermostat, but the order is unimportant.Applies to ScenesLIGHTA light source or fixture.
SMARTLOCKIndicates an endpoint that locks. MICROWAVEA microwave oven.
SMARTPLUGIndicates modules that are plugged into an existing electrical outlet.Can control a variety of devices.MOBILE_PHONEA mobile phone.
SPEAKERIndicates the endpoint is a speaker or speaker system. MOTION_SENSORAn endpoint that detects and reports movement in an area.
SWITCHIndicates in-wall switches wired to the electrical system.Can control a variety of devices.MUSIC_SYSTEMA network-connected music system.
TEMPERATURE_SENSORIndicates endpoints that report the temperature only.The endpoint's temperature data is not shown in the Alexa app.NETWORK_HARDWAREA network router.
THERMOSTATIndicates endpoints that control temperature, stand-alone air conditioners, or heaters with direct temperature control. OTHERAn endpoint that doesn't belong to one of the other categories.
TVIndicates the endpoint is a television. OVENAn oven cooking appliance.
PHONEA non-mobile phone, such as landline or an IP phone.
SCENE_TRIGGERA combination of devices set to a specific state. Use scene triggers for scenes when the order of the state change is not important. For example, for a scene named "bedtime" you might turn off the lights and lower the thermostat, in any order.
SCREENA projector screen.
SECURITY_PANELA security panel.
SMARTLOCKAn endpoint that locks.
SMARTPLUGA module that is plugged into an existing electrical outlet, and then has a device plugged into it. For example, a user can plug a smart plug into an outlet, and then plug a lamp into the smart plug. A smart plug can control a variety of devices.
SPEAKERA speaker or speaker system.
STREAMING_DEVICEA streaming device such as Apple TV, Chromecast, or Roku.
SWITCHA switch wired directly to the electrical system. A switch can control a variety of devices.
TABLETA tablet computer.
TEMPERATURE_SENSORAn endpoint that reports temperature, but does not control it. The temperature data of the endpoint is not shown in the Alexa app.
THERMOSTATAn endpoint that controls temperature, stand-alone air conditioners, or heaters with direct temperature control.
TVA television.
WEARABLEA network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear.
default = "Switch" (vergleiche : https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories ) @@ -294,7 +392,7 @@ OG: enforce_updates: 'true' -##Entwicklung / Einbau von neuen Fähigkeiten +## Entwicklung / Einbau von neuen Fähigkeiten Um weitere Actions hinzuzufügen muss die Datei p3_actions.py mit den entsprechenden Actions ergänzt werden. (wie ursprünglich als selbstregistrierende Funktion) @@ -350,7 +448,7 @@ alexa_icon = "THERMOSTAT" = Thermostatcontroller alexa_icon = "TEMPERATURE_SENSOR" = Temperatursensor -###Thermostatsensor +### Thermostatsensor Der Temperartursensor wird beim Item der Ist-Temperatur hinterlegt. Der Thermostatconroller wird beim Thermostat-Item hinterlegt. An Amazon werden die Icons als Array übertragen. @@ -362,7 +460,7 @@ alexa_actions : "ReportTemperature" Alexa wie ist die Temperatur in der Küche ? -###Verändern der Temperatur (SetTargetTemperature AdjustTargetTemperature) +### Verändern der Temperatur (SetTargetTemperature AdjustTargetTemperature)

 alexa_actions = "SetTargetTemperature AdjustTargetTemperature"
@@ -377,7 +475,7 @@ Alexa stelle die Temperatur in der Küche auf zweiundzwanzig Grad
 
 Alexa wie ist die Temperatur in der Küche eingestellt ?
 
-###Thermostatmode
+### Thermostatmode
 
 alexa_actions = "SetThermostatMode"
 
@@ -571,7 +669,7 @@ Beispiel :
 
## Alexa-PowerLevelController -## !!!! erst ab Plugin-Version 1.0.0.0.1 oder höher !!!! +## !!!! erst ab Plugin-Version 1.0.1 oder höher !!!! Alexa stelle Energie Licht Küche auf achtzig Alexa erhöhe Energie Licht Küche um zehn @@ -647,7 +745,7 @@ Beispiel Konfiguration im yaml-Format: ## Alexa-LockController -## !!!! erst ab Plugin-Version 1.0.0.0.2 oder höher !!!! +## !!!! erst ab Plugin-Version 1.0.1 oder höher !!!! Die Probleme in der Amazon-Cloud mit dem LockController sind behoben. Die Funktion ist im Moment so realisiert, das bei "Unlock" ein "ON" (=1) auf @@ -753,7 +851,7 @@ Beispiel mit einem Aktor-Kanal für öffnen, ein Aktor-Kanal für schliessen mit ## Alexa-CameraStreamContoller -## !!!! erst ab Plugin-Version 1.0.0.0.1 oder höher !!!! +## !!!! erst ab Plugin-Version 1.0.1 oder höher !!!! Alexa zeige die Haustür Kamera. @@ -763,7 +861,7 @@ d.h. : - Kamera auf Port 443 erreichbar ##!! für Kameras im lokalen Netzwerk wird gerade noch ein Camera Proxy entwickelt - dieser gibt dann die Möglichkeit auch private Kameras einzubinden !! -#Look out for : CamProxy4AlexpaP3 +#Look out for : AlexaCamProxy4P3 Aus den bereitgestellten Streams wird @@ -771,12 +869,16 @@ immer der mit der höchsten Auflösung an Alexa übermittelt. Folgende Parameter sind anzugeben : -#####alexa_csc_proxy_uri **Update**: URL über DynDNS vergeben um die Kamera mittels CamProxy4AlexaP3 zu streamen -#####alexa_camera_imageUri: die URL des Vorschau-Pictures der Kamera +##### alexa_csc_proxy_uri **Update**: URL über DynDNS vergeben um die Kamera mittels CamProxy4AlexaP3 zu streamen + +##### alexa_proxy_credentials **Update**: Zugangsdaten für den AlexaCamProxy falls dieser mit Authentication "Basic" oder "Digest" parametriert wird. Angabe in der Form "USER":"PWD" -#####alexa_stream_1: Definition für den ersten Stream der Kamara, es werden bis zu 3 Streams unterstützt. Hier müssen die Details zum Stream definiert werden (protocol = rtsp, resolutions = Array mit der Auflösung, authorizationTypes = Autorisierung, videoCodecs = Array der VideoCodes, autoCodecs = Array der Audiocodes) -alexa_csc_uri: Auflistung der Stream-URL´s für Stream1: / Stream2: / Stream3 +##### alexa_camera_imageUri: die URL des Vorschau-Pictures der Kamera + +##### alexa_stream_1: Definition für den ersten Stream der Kamara, es werden bis zu 3 Streams unterstützt. Hier müssen die Details zum Stream definiert werden (protocol = rtsp, resolutions = Array mit der Auflösung, authorizationTypes = Autorisierung, videoCodecs = Array der VideoCodes, autoCodecs = Array der Audiocodes) + +##### alexa_csc_uri: Auflistung der Stream-URL´s für Stream1: / Stream2: / Stream3 siehe Tabelle unten für mögliche Werte (Beispiel im YAML-Format): @@ -809,6 +911,7 @@ siehe Tabelle unten für mögliche Werte alexa_stream_3: '{....... }' alexa_csc_proxy_uri: alexatestcam.ddns.de:443 + alexa_proxy_credentials: user:pwd Als Action ist fix "alexa_actions: InitializeCameraStreams" anzugeben. @@ -867,7 +970,7 @@ scene: alexa_retrievable : false -##ContactSensor Interface +## ContactSensor Interface Alexa ist das Küchenfenster geschlossen ? Alexa ist das Küchenfenster geöffnet ? @@ -892,7 +995,7 @@ fensterkontakt: -##ColorController +## ColorController Alexa, setze Licht Speicher auf rot @@ -943,3 +1046,217 @@ Speicher: - G_WERT = list[1] - B_WERT = list[2] + + +## RangeController + + +Folgende Paramter sind anzugeben : + +

+alexa_actions: SetRangeValue AdjustRangeValue 
+alexa_range_delta: 20
+alexa_item_range: 0-255
+
+ +ergänzt um das entsprechende Categorie-Icon + +

+alexa_icon: EXTERIOR_BLIND
+
+ +oder + +

+alexa_icon: INTERIOR_BLIND
+
+ +Der RangeController kann mit dem Percentage-Controller kombiniert werden + +

+alexa_actions: SetRangeValue AdjustRangeValue SetPercentage 
+alexa_range_delta: 20
+alexa_item_range: 0-255
+
+ + +## ColorTemperaturController + +Es müssen die Parameter für den einstellbaren Weiss-Bereich unter "alexa_item_range" in Kelvin von/bis angegeben werden. +Da die Geräte der verschiedenen Hersteller unterschiedliche Weißbereiche abdecken ist wird dieser Wert benötigt. +Falls ein Weißwert angefordert wird den das jeweilige Gerät nicht darstellen kann wird auf den Minimum bzw. den Maximumwert gestellt. + +Als Alexa-Actions müssen SetColorTemperature/IncreaseColorTemperature/DecreaseColorTemperature angegeben werden. +Als Rückgabewert wird das entsprechende Item vom plugin auf den Wert von 0 (warmweiss) bis 255 (kaltweiss) gesetzt. + +Hinweis : Alexa unterstützt 1.000 Kelvin - 10.000 Kelvin + +

+alexa_item_range: 3000-6500
+alexa_actions: SetColorTemperature IncreaseColorTemperature DecreaseColorTemperature
+
+ +## PlaybackController + +Eingebaut um fahrende Rolladen zu stoppen. + +#### Alexa, stoppe den Rolladen Büro + +Das funktioniert nur, wenn beim Rolladen/Jalousie kein TurnOn/TurnOff definiert sind. Die Rolladen müssen mittels "AdjustPercentage" und "SetPercentage" angesteuert werden. Dann kann mit dem "Stop" Befehl der Rolladen angehalten werden. + +Die Action lautet "Stop". Es wird an dieser Stelle der Alexa.PlaybackController zweck entfremded. Dieser Controller hat eine "Stop" Funktion implementiert welche hier genutzt wird. +Beim ausführen des Befehls wird eine "1" an das Item übergeben. Das Item muss der Stopbefehl für den Rolladen sein. enforce_update muss auf True stehen. + +Alle Actions senden jeweils ein "True" bzw. "EIN" bzw. "1" + +implementierte Funktionen: + +alexa_actions: Stop / Play / Pause / FastForward / Next / Previous / Rewind / StartOver + + +# Web-Interface + +Das Plugin bietet ein Web-Interface an. + +Auf der ersten Seite werden alle Alexa-Geräte, die definierten Actions sowie die jeweiligen Aliase angezeigt. Actions in Payload-Version 3 werden grün angezeigt. Actions in Payload-Version 2 werden in rot angezeigt. +Eine Zusammenfassung wird oben rechts dargestellt. Durch anklicken eine Zeile kann ein Alexa-Geräte für die Testfunktionen auf Seite 3 des Web-Interfaces auswewählt werden +![webif_Seite1](./assets/Alexa4P3_Seite1.jpg) + +Auf der Zweiten Seite wird ein Kommunikationsprotokoll zur Alexa-Cloud angezeigt. +![webif_Seite2](./assets/Alexa4P3_Seite2.jpg) + +Auf Seite drei können "Directiven" ähnlich wie in der Lambda-Test-Funktion der Amazon-Cloud ausgeführt werden. Der jeweilige Endpunkt ist auf Seite 1 duch anklicken zu wählen. Die Kommunikation wird auf Seite 2 protokolliert. +So könnne einzelne Geräte und "Actions" getestet werden. + +![webif_Seite3](./assets/Alexa4P3_Seite3.jpg) + +Auf Seite 4 kann interaktiv ein YAML-Eintrag für einen Alexa-Kamera erzeugt werden. Der fertige YAML-Eintrag wird unten erzeugt und kann via Cut & Paste in die Item-Definition von shNG übernommen werden. + +![webif_Seite4](./assets/Alexa4P3_Seite4.jpg) + + +# Beispiele + +## Der fast perfekte Rolladen + +Mit diesen Einstellungen kann ein Rolladen wie folgt gesteuert werden : + +Alexa, + +mache den Rolladen hoch + +mache den Rolladen runter + +öffne den Rolladen im Büro + +mache den Rolladen im Büro auf + +schliesse den Rolladen im Büro + +mache den Rolladen im Büro zu + +fahre Rolladen Büro auf siebzig Prozent + +stoppe Rolladen Büro + + + +Es wird zum einen der RangeController mit erweiterten Ausdrücken verwendet zum anderen wird +der PlaybackController zweckentfremdet für das Stop-Signal verwendet. + +### !! Wichtig !! + + +Die erweiterten Ausdrücke (öffnen/schliessen - hoch/runter) werden durch das Plugin automatisch +beim RangeController eingebunden wenn als Alexa-Icon "EXTERIOR_BLIND" oder "INTERIOR_BLIND" parametriert werden. + +Beim Stop des Rolladen-Items muss "alexa_actions: Stop" angegeben werden +Um das Item automatisch zurückzusetzen empfiehlt sich der autotimer-Eintrag. + + +Bei der Positionierung des Rolladen muss "alexa_range_delta: xx" angegeben werden. +"xx" ist hier der Wert der beim Kommando hoch/runter gesendet wird. +Bei xx=20 und "Rolladen runter" würde der Rolladen 20 Prozent nach unten fahren. +Bei xx=20 und "Rolladen hoch" würde der Rolladen 20 Prozent nach oben fahren. +Wenn der Rolladen bei "hoch/runter" komplett fahren soll kann hier auch 100 angegeben werden. + +Für die Positionierung ist "alexa_item_range: 0-255" anzugeben. + +

+        Rolladen:
+            alexa_name: Rollladen Büro
+            alexa_device: rolladen_buero
+            alexa_description: Rollladen Büro
+            alexa_icon: EXTERIOR_BLIND
+            alexa_proactivelyReported: 'False'
+            alexa_retrievable: 'True'
+
+            move:
+                type: num
+                visu_acl: rw
+                knx_dpt: 1
+                knx_send: 3/2/23
+                enforce_updates: 'true'
+
+            stop:
+                type: num
+                visu_acl: rw
+                enforce_updates: 'true'
+                knx_dpt: 1
+                knx_send: 3/1/23
+                alexa_device: rolladen_buero
+                alexa_actions: Stop 
+                alexa_retrievable: 'False'
+                alexa_proactivelyReported: 'False'
+                autotimer: 1 = 0
+
+            pos:
+                type: num
+                visu_acl: rw
+                knx_dpt: 5
+                knx_listen: 3/3/23
+                knx_send: 3/4/23
+                knx_init: 3/3/23
+                enforce_updates: 'true'
+                alexa_actions: SetRangeValue AdjustRangeValue 
+                alexa_retrievable: 'True'
+                alexa_range_delta: 20
+                alexa_item_range: 0-255
+
+ + +## Items in Abhängikeit des letzten benutzten Echos-Devices schalten + +Wenn das AlexaRc4shNG-Plugin aktiviert ist kann über eine Logik das letzte Echo-Gerät welches einen Sprachbefehl bekommen hat ermittelt werden und abhängig davon können Items geschalten werden. So kann z.b. eine raumabhängige Steuerung für das Licht und Rolladen erstellt werden. + +Es wird ein Item für Licht pauschal erstellt : +``` + Licht_pauschal: + alexa_name: Licht + alexa_device: Licht_pauschal + alexa_description: Licht Pauschal + alexa_icon: OTHER + alexa_actions: TurnOn TurnOff + alexa_proactivelyReported: 'False' + type: num + visu_acl: rw + enforce_updates: 'true' +``` + +eine entsprechende Logik welche durch das item "Licht_pauschal" getriggert wird schaltet dann die entsprechenden Items. +``` +#!/usr/bin/env python3 +#last_alexa.py + +myAlexa = sh.alexarc4shng.get_last_alexa() +if myAlexa != None: + triggeredItem=trigger['source'] + triggerValue = trigger['value'] + if triggeredItem == "test.testzimmer.Licht_pauschal": + if myAlexa == "ShowKueche": + sh.EG.Kueche.Spots_Sued(triggerValue) + if myAlexa == "Wohnzimmer": + sh.OG.Wohnzimmer.Spots_Nord(triggerValue) + sh.OG.Wohnzimmer.Spots_Sued(triggerValue) + +``` \ No newline at end of file diff --git a/alexa4p3/__init__.py b/alexa4p3/__init__.py index 961d3fe04..fff2955bf 100644 --- a/alexa4p3/__init__.py +++ b/alexa4p3/__init__.py @@ -26,10 +26,12 @@ -from lib.model.smartplugin import SmartPlugin +from lib.model.smartplugin import * +from lib.item import Items +from lib.shtime import Shtime import logging import json - +import datetime from .device import AlexaDevices, AlexaDevice from .action import AlexaActions @@ -42,28 +44,69 @@ # Tools for Payload V3 from . import p3_action +import requests + +class protocoll(object): + + log = [] + + def __init__(self): + pass + + def addEntry(self,type, _text ): + myLog = self.log + if (myLog == None): + return + try: + if len (myLog) >= 500: + myLog = myLog[0:499] + except: + return + now = str(datetime.datetime.now())[0:22] + myLog.insert(0,str(now)[0:19]+' Type: ' + str(type) + ' - '+str(_text)) + self.log = myLog class Alexa4P3(SmartPlugin): - PLUGIN_VERSION = "1.0.1" + PLUGIN_VERSION = "1.0.2" ALLOW_MULTIINSTANCE = False - def __init__(self, sh, service_host='0.0.0.0', service_port=9000, service_https_certfile=None, service_https_keyfile=None): - self.logger = logging.getLogger(__name__) - self.sh = sh + def __init__(self, sh, *args, **kwargs): + from bin.smarthome import VERSION + if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5': + self.logger = logging.getLogger(__name__) + + self.sh = self.get_sh() + self.service_port = self.get_parameter_value('service_port') + self.service_https_certfile = None + self.service_https_keyfile = None + self.service_host='0.0.0.0' + self.devices = AlexaDevices() self.actions = AlexaActions(self.sh, self.logger, self.devices) - self.service = AlexaService(self.logger, self.PLUGIN_VERSION, self.devices, self.actions, - service_host, int(service_port), service_https_certfile, service_https_keyfile) + + self._proto = protocoll() + + self.service = AlexaService(self._proto,self.logger, self.PLUGIN_VERSION, self.devices, self.actions, + self.service_host, int(self.service_port), self.service_https_certfile, self.service_https_keyfile) + self.action_count_v2 = 0 + self.action_count_v3 = 0 + + + self.init_webinterface() def run(self): self.validate_devices() self.create_alias_devices() - self.service.start() self.alive = True + #myProto = getattr(self,'_proto.addEntry') + #self.service._proto = myProto + self.service.start() + + def stop(self): self.service.stop() @@ -86,7 +129,12 @@ def parse_item(self, item): if action_name and self.actions.by_name(action_name) is None: self.logger.error("Alexa: invalid alexa action '{}' specified in item {}, ignoring item".format(action_name, item.id())) return None - + actAction = self.actions.by_name(action_name) + if actAction.payload_version == "3": + self.action_count_v3 += 1 + else: + self.action_count_v2 += 1 + # friendly name name = None name_is_explicit = None @@ -161,6 +209,18 @@ def parse_item(self, item): alexa_color_value_type = item.conf['alexa_color_value_type'] device.alexa_color_value_type = alexa_color_value_type self.logger.debug("Alexa4P3: {}-ColorValueType = {}".format(item.id(), device.alexa_color_value_type)) + + # special Alexa_Instance for RangeController + if 'alexa_range_delta' in item.conf: + alexa_range_delta = item.conf['alexa_range_delta'] + device.alexa_range_delta = alexa_range_delta + self.logger.debug("Alexa4P3: {}-Alexa_Range_Delta = {}".format(item.id(), device.alexa_range_delta)) + + # special Alexa_Instance for ColorTemperaturController + if 'alexa_color_temp_delta' in item.conf: + alexa_color_temp_delta = item.conf['alexa_color_temp_delta'] + item.alexa_color_temp_delta = alexa_color_temp_delta + self.logger.debug("Alexa4P3: {}-Alexa_ColorTemp_Delta = {}".format(item.id(), item.alexa_color_temp_delta)) #=============================================== @@ -189,6 +249,11 @@ def parse_item(self, item): except Exception as e: self.logger.debug("Alexa4P3: {}-wrong Stream Settings = {}".format(item.id(), camera_uri)) i +=1 + + if 'alexa_proxy_credentials' in item.conf: + alexa_proxy_credentials = item.conf['alexa_proxy_credentials'] + device.alexa_proxy_credentials = alexa_proxy_credentials + self.logger.debug("Alexa4P3: {}-Proxy-Credentials = {}".format(item.id(), device.alexa_proxy_credentials)) if 'alexa_csc_uri' in item.conf: camera_uri = item.conf['alexa_csc_uri'] @@ -205,6 +270,12 @@ def parse_item(self, item): alexa_camera_imageUri = item.conf['alexa_camera_imageUri'] device.camera_imageUri = alexa_camera_imageUri self.logger.debug("Alexa4P3: {}-Camera-Image-Uri = {}".format(item.id(), device.camera_imageUri)) + + if 'alexa_cam_modifiers' in item.conf: + alexa_cam_modifiers = item.conf['alexa_cam_modifiers'] + device.camera_imageUri = alexa_cam_modifiers + self.logger.debug("Alexa4P3: {}-Camera-Stream-Modifiers = {}".format(item.id(), device.alexa_cam_modifiers)) + # ---- Ende CamerStreamController @@ -237,6 +308,7 @@ def parse_item(self, item): for action_name in action_names: device.register(action_name, item) self.logger.info("Alexa: {} supports {} as device {}".format(item.id(), action_names, device_id, device.supported_actions())) + self._proto.addEntry('INFO ', "Alexa: {} supports {} as device {}".format(item.id(), action_names, device_id, device.supported_actions())) return None @@ -245,15 +317,382 @@ def _update_values(self): def validate_devices(self): for device in self.devices.all(): - self.logger.debug("Alexa: validating device {}".format(device.id)) - if not device.validate(self.logger): - raise ValueError("Alexa: invalid device {}".format(device.id)) + self.logger.debug("validating device {}".format(device.id)) + self._proto.addEntry('INFO ', "validating device {}".format(device.id)) + if not device.validate(self.logger, self._proto): + self.devices.delete(device.id) + self.logger.warning("invalid device {} - removed Device from Plugin".format(device.id)) + self._proto.addEntry('WARNING', "invalid device {} - removed Device from Plugin".format(device.id)) def create_alias_devices(self): for device in self.devices.all(): alias_devices = device.create_alias_devices() for alias_device in alias_devices: self.logger.info("Alexa: device {} aliased '{}' via {}".format(device.id, alias_device.name, alias_device.id)) + self._proto.addEntry('INFO ', "Alexa: device {} aliased '{}' via {}".format(device.id, alias_device.name, alias_device.id)) self.devices.put( alias_device ) self.logger.info("Alexa: providing {} devices".format( len(self.devices.all()) )) + + + ################################################ + # Function for Web-Interface to Test directives + ################################################ + def load_command_let(self,cmdName,path=None): + myDescription = '' + myUrl = '' + myJson = '' + retJson = {} + + if path==None: + path=self.sh.get_basedir()+"/plugins/alexa4p3/directives/" + + try: + file=open(path+cmdName+'.cmd','r') + for line in file: + line=line.replace("\r\n","") + line=line.replace("\n","") + myFields=line.split("|") + if (myFields[0]=="apiurl"): + myUrl=myFields[1] + pass + if (myFields[0]=="description"): + myDescription=myFields[1] + pass + if (myFields[0]=="json"): + myJson=myFields[1] + retJson=json.loads(myJson) + pass + file.close() + except: + self.logger.error("Error while loading Directive : {}".format(cmdName)) + return myDescription,myUrl,retJson + + + + def load_cmd_list(self): + retValue=[] + + files = os.listdir(self.sh.get_basedir()+'/plugins/alexa4p3/directives/') + for line in files: + try: + line=line.split(".") + if line[1] == "cmd": + newCmd = {'Name':line[0]} + retValue.append(newCmd) + except: + pass + + return json.dumps(retValue) + + def check_json(self,payload): + try: + myDump = json.loads(payload) + return 'Json OK' + except Exception as err: + return 'Json - Not OK - '+ err.args[0] + + def delete_cmd_let(self,name): + result = "" + try: + os.remove(self.sh.get_basedir()+"/plugins/alexa4p3/directives/"+name+'.cmd') + result = "Status:OK\n" + result += "value1:File deleted\n" + except Exception as err: + result = "Status:failure\n" + result += "value1:Error - "+err.args[1]+"\n" + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + def test_cmd_let(self,selectedDevice,txtValue,txtDescription,txt_payload,txtApiUrl): + result = "" + + JsonResult = self.check_json(txt_payload) + if (JsonResult != 'Json OK'): + result = "Status:failure\n" + result += "value1:"+JsonResult+"\n" + else: + try: + myDummy=txt_payload.replace('',selectedDevice) + myDummy=myDummy.replace('""',str(txtValue)) + myDummy=myDummy.replace('',selectedDevice) + myDummy = json.loads(myDummy) + myResponse = requests.post("http://127.0.0.1:"+str(self.service_port), data = json.dumps(myDummy)) + result = "Status:OK\n" + result += "value1: HTTP "+str(myResponse.status_code)+"\n" + #result += "payload: HTTP "+str(myResponse.status_code)+"\n" + except Exception as err: + result = "Status:failure\n" + result += "value1:"+err.args[0]+"\n" + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + def load_cmd_2_webIf(self,txtCmdName): + try: + myDescription,myUrl,myDict = self.load_command_let(txtCmdName,None) + result = "Status|OK\n" + result += "Description|"+myDescription+"\n" + result += "myUrl|"+myUrl+"\n" + result += "payload|"+str(myDict)+"\n" + except Exception as err: + result = "Status|failure\n" + result += "value1|"+err.args[0]+"\n" + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split("|") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + + def save_cmd_let(self,name,description,payload,ApiURL,path=None): + if path==None: + path=self.sh.get_basedir()+"/plugins/alexa4p3/directives/" + + result = "" + mydummy = ApiURL[0:1] + if (ApiURL[0:1] != "/"): + ApiURL = "/"+ApiURL + + JsonResult = self.check_json(payload) + if (JsonResult != 'Json OK'): + result = "Status:failure\n" + result += "value1:"+JsonResult+"\n" + + else: + try: + myDict = json.loads(payload) + myDump = json.dumps(myDict) + description=description.replace("\r"," ") + description=description.replace("\n"," ") + file=open(path+name+".cmd","w") + file.write("apiurl|"+ApiURL+"\r\n") + file.write("description|"+description+"\r\n") + file.write("json|"+myDump+"\r\n") + file.close + + result = "Status:OK\n" + result += "value1:"+JsonResult + "\n" + result += "value2:Saved Directive\n" + except Exception as err: + print (err) + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + + + def init_webinterface(self): + """" + Initialize the web interface for this plugin + + This method is only needed if the plugin is implementing a web interface + """ + try: + self.mod_http = Modules.get_instance().get_module( + 'http') # try/except to handle running in a core version that does not support modules + except: + self.mod_http = None + if self.mod_http == None: + self.logger.error("Not initializing the web interface") + return False + + import sys + if not "SmartPluginWebIf" in list(sys.modules['lib.model.smartplugin'].__dict__): + self.logger.warning("Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface") + return False + + # set application configuration for cherrypy + webif_dir = self.path_join(self.get_plugin_dir(), 'webif') + config = { + '/': { + 'tools.staticdir.root': webif_dir, + }, + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static' + } + } + + # Register the web interface as a cherrypy app + self.mod_http.register_webif(WebInterface(webif_dir, self), + self.get_shortname(), + config, + self.get_classname(), self.get_instance_name(), + description='') + + return True + + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = logging.getLogger(__name__) + self.webif_dir = webif_dir + self.plugin = plugin + self.tplenv = self.init_template_environment() + self.items = Items.get_instance() + + + def get_alexa_items(self): + item_count = 0 + alexa_items = [] + for device in self.plugin.devices.all(): + newEntry = {} + supported_actions = device.supported_actions() + newEntry['AlexaName'] = device.name + newEntry['Alias_for'] = device.alias_for + newEntry['Actions'] = "" + for myAction in supported_actions: + #================== + actAction = self.plugin.actions.by_name(myAction) + if actAction.payload_version == "3": + newEntry['Actions'] += ''+myAction +''+ ' / ' + else: + newEntry['Actions'] += ''+myAction +''+ ' / ' + #================== + + newEntry['Actions']=newEntry['Actions'][:-2] + newEntry['Items'] = "" + for action_items in device.action_items: + if not str(device.action_items[action_items][0]) in newEntry['Items']: + newEntry['Items'] +=str(device.action_items[action_items][0])+ ' / ' + newEntry['Items']=newEntry['Items'][:-2] + newEntry['DeviceID']=device.id + alexa_items.append(newEntry) + item_count += 1 + return item_count, alexa_items + + @cherrypy.expose + def get_proto_html(self, proto_Name= None): + if proto_Name == 'state_log_file': + return json.dumps(self.plugin._proto.log) + + @cherrypy.expose + def clear_proto_html(self, proto_Name= None): + self.plugin._proto.log = [] + return None + + @cherrypy.expose + def handle_buttons_html(self,txtValue=None, selectedDevice=None,txtButton=None,txt_payload=None,txtCmdName=None,txtApiUrl=None,txtDescription=None): + if txtButton=="BtnSave": + result = self.plugin.save_cmd_let(txtCmdName,txtDescription,txt_payload,txtApiUrl) + elif txtButton =="BtnCheck": + pass + elif txtButton =="BtnLoad": + result = self.plugin.load_cmd_2_webIf(txtCmdName) + pass + elif txtButton =="BtnTest": + result = self.plugin.test_cmd_let(selectedDevice,txtValue,txtDescription,txt_payload,txtApiUrl) + elif txtButton =="BtnDelete": + result = self.plugin.delete_cmd_let(txtCmdName) + else: + pass + + #return self.render_template("index.html",txtresult=result) + return result + + + @cherrypy.expose + def build_cmd_list_html(self,reload=None): + myCommands = self.plugin.load_cmd_list() + return myCommands + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + tmpl = self.tplenv.get_template('index.html') + + item_count,alexa_items = self.get_alexa_items() + yaml_result = '\r\n' + + try: + my_state_loglines = self.plugin._proto.log + state_log_file = '' + for line in my_state_loglines: + state_log_file += str(line)+'\n' + except: + state_log_file = 'No Data available right now\n' + + payload_action = "used Actions Payload V2 = " + str(self.plugin.action_count_v2) + " / " +"used Actions Payload V3 = " + str(self.plugin.action_count_v3)+'' + if (self.plugin.action_count_v2 != 0 and self.plugin.action_count_v3 != 0): + payload_action = '' + payload_action + '' + else: + payload_action = '' + payload_action + '' + + + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + items=sorted(alexa_items, key=lambda k: str.lower(k['AlexaName'])), + item_count=item_count, + yaml_result=yaml_result, + payload_action=payload_action, + state_log_lines=state_log_file + ) + + diff --git a/alexa4p3/_config.yml b/alexa4p3/_config.yml deleted file mode 100644 index c4192631f..000000000 --- a/alexa4p3/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-cayman \ No newline at end of file diff --git a/alexa4p3/action.py b/alexa4p3/action.py index d702e71fb..caa849ebb 100644 --- a/alexa4p3/action.py +++ b/alexa4p3/action.py @@ -12,13 +12,14 @@ action_func_registry = [] # action-func decorator -def alexa(action_name, directive_type, response_type, namespace, properties ): +def alexa(action_name, directive_type, response_type, namespace, properties,payload_version ): def store_metadata(func): func.alexa_action_name = action_name func.alexa_directive_type = directive_type func.alexa_response_type = response_type func.alexa_namespace = namespace func.alexa_properties = properties + func.alexa_payload_version = payload_version action_func_registry.append( func ) return func @@ -31,7 +32,7 @@ def __init__(self, sh, logger, devices): self.actions_by_directive = {} for func in action_func_registry: logger.debug("Alexa: initializing action {}".format(func.alexa_action_name)) - action = AlexaAction(sh, logger, devices, func, func.alexa_action_name, func.alexa_directive_type, func.alexa_response_type,func.alexa_namespace, func.alexa_properties) + action = AlexaAction(sh, logger, devices, func, func.alexa_action_name, func.alexa_directive_type, func.alexa_response_type,func.alexa_namespace, func.alexa_properties,func.alexa_payload_version) self.actions[action.name] = action self.actions_by_directive[action.directive_type] = action @@ -44,7 +45,7 @@ def for_directive(self, directive): class AlexaAction(object): - def __init__(self, sh, logger, devices, func, action_name, directive_type, response_type, namespace,properties): + def __init__(self, sh, logger, devices, func, action_name, directive_type, response_type, namespace,properties,payload_version): self.sh = sh self.logger = logger self.devices = devices @@ -56,6 +57,7 @@ def __init__(self, sh, logger, devices, func, action_name, directive_type, respo self.namespace = namespace self.response_Value = None self.properties = properties + self.payload_version = payload_version def __call__(self, payload): return self.func(self, payload) @@ -162,7 +164,6 @@ def p3_AddDependencies(self, orgDirective, dependency, myEndPointID): return orgDirective def p3_respond(self, Request): - myEndpoint = self.search(Request,'endpoint') myScope = self.search(Request,'scope') myEndPointID = self.search(Request,'endpointId') diff --git a/alexa4p3/actions_lock.py b/alexa4p3/actions_lock.py index 5716e30ab..fe0bcce8d 100644 --- a/alexa4p3/actions_lock.py +++ b/alexa4p3/actions_lock.py @@ -3,7 +3,7 @@ DEFAULT_RANGE = (True, False) -@alexa('getLockState', 'GetLockStateRequest', 'GetLockStateResponse','',[]) +@alexa('getLockState', 'GetLockStateRequest', 'GetLockStateResponse','',[],"2") def get_lock_state(self, payload): items = self.items( payload['appliance']['applianceId'] ) @@ -18,7 +18,7 @@ def get_lock_state(self, payload): 'applianceResponseTimestamp': date.today().isoformat() }) -@alexa('setLockState', 'SetLockStateRequest', 'SetLockStateConfirmation','',[]) +@alexa('setLockState', 'SetLockStateRequest', 'SetLockStateConfirmation','',[],"2") def set_lock_state(self, payload): items = self.items( payload['appliance']['applianceId'] ) requested_state = payload['lockState'] diff --git a/alexa4p3/actions_percentage.py b/alexa4p3/actions_percentage.py index fd51e5469..0d3eb7aaf 100644 --- a/alexa4p3/actions_percentage.py +++ b/alexa4p3/actions_percentage.py @@ -18,7 +18,7 @@ def clamp_percentage(percent, range): _min, _max = range return min(_max, max(_min, percent)) -@alexa('setPercentage', 'SetPercentageRequest', 'SetPercentageConfirmation','',[]) +@alexa('setPercentage', 'SetPercentageRequest', 'SetPercentageConfirmation','',[],"2") def set_percentage(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) @@ -32,7 +32,7 @@ def set_percentage(self, payload): return self.respond() -@alexa('incrementPercentage', 'IncrementPercentageRequest', 'IncrementPercentageConfirmation','',[]) +@alexa('incrementPercentage', 'IncrementPercentageRequest', 'IncrementPercentageConfirmation','',[],"2") def incr_percentage(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) @@ -49,7 +49,7 @@ def incr_percentage(self, payload): return self.respond() -@alexa('decrementPercentage', 'DecrementPercentageRequest', 'DecrementPercentageConfirmation','',[]) +@alexa('decrementPercentage', 'DecrementPercentageRequest', 'DecrementPercentageConfirmation','',[],"2") def decr_percentage(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) diff --git a/alexa4p3/actions_temperature.py b/alexa4p3/actions_temperature.py index 8bdad105f..210a26a5d 100644 --- a/alexa4p3/actions_temperature.py +++ b/alexa4p3/actions_temperature.py @@ -6,7 +6,7 @@ def clamp_temp(temp, range): _min, _max = range return min(_max, max(_min, temp)) -@alexa('setTargetTemperature', 'SetTargetTemperatureRequest', 'SetTargetTemperatureConfirmation','',[]) +@alexa('setTargetTemperature', 'SetTargetTemperatureRequest', 'SetTargetTemperatureConfirmation','',[],"2") def set_target_temp(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) @@ -39,7 +39,7 @@ def set_target_temp(self, payload): } }) -@alexa('incrementTargetTemperature', 'IncrementTargetTemperatureRequest', 'IncrementTargetTemperatureConfirmation','',[]) +@alexa('incrementTargetTemperature', 'IncrementTargetTemperatureRequest', 'IncrementTargetTemperatureConfirmation','',[],"2") def incr_target_temp(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) @@ -73,7 +73,7 @@ def incr_target_temp(self, payload): } }) -@alexa('decrementTargetTemperature', 'DecrementTargetTemperatureRequest', 'DecrementTargetTemperatureConfirmation','',[]) +@alexa('decrementTargetTemperature', 'DecrementTargetTemperatureRequest', 'DecrementTargetTemperatureConfirmation','',[],"2") def decr_target_temp(self, payload): device_id = payload['appliance']['applianceId'] items = self.items(device_id) diff --git a/alexa4p3/actions_turn.py b/alexa4p3/actions_turn.py index db3cfef5d..7fc8d2bab 100644 --- a/alexa4p3/actions_turn.py +++ b/alexa4p3/actions_turn.py @@ -2,7 +2,7 @@ DEFAULT_RANGE = (True, False) -@alexa('turnOn', 'TurnOnRequest', 'TurnOnConfirmation','',[]) +@alexa('turnOn', 'TurnOnRequest', 'TurnOnConfirmation','',[],"2") def turn_on(self, payload): items = self.items( payload['appliance']['applianceId'] ) @@ -14,7 +14,7 @@ def turn_on(self, payload): return self.respond() -@alexa('turnOff', 'TurnOffRequest', 'TurnOffConfirmation','',[]) +@alexa('turnOff', 'TurnOffRequest', 'TurnOffConfirmation','',[],"2") def turn_off(self, payload): items = self.items( payload['appliance']['applianceId'] ) diff --git a/alexa4p3/assets/Alexa4P3_Seite1.jpg b/alexa4p3/assets/Alexa4P3_Seite1.jpg new file mode 100755 index 000000000..aa6aea711 Binary files /dev/null and b/alexa4p3/assets/Alexa4P3_Seite1.jpg differ diff --git a/alexa4p3/assets/Alexa4P3_Seite2.jpg b/alexa4p3/assets/Alexa4P3_Seite2.jpg new file mode 100755 index 000000000..969572014 Binary files /dev/null and b/alexa4p3/assets/Alexa4P3_Seite2.jpg differ diff --git a/alexa4p3/assets/Alexa4P3_Seite3.jpg b/alexa4p3/assets/Alexa4P3_Seite3.jpg new file mode 100755 index 000000000..cedb57fcb Binary files /dev/null and b/alexa4p3/assets/Alexa4P3_Seite3.jpg differ diff --git a/alexa4p3/assets/Alexa4P3_Seite4.jpg b/alexa4p3/assets/Alexa4P3_Seite4.jpg new file mode 100755 index 000000000..394a1e945 Binary files /dev/null and b/alexa4p3/assets/Alexa4P3_Seite4.jpg differ diff --git a/alexa4p3/aws_lambda.js b/alexa4p3/aws_lambda.js old mode 100644 new mode 100755 diff --git a/alexa4p3/device.py b/alexa4p3/device.py index 149b4f3f5..c8816995e 100644 --- a/alexa4p3/device.py +++ b/alexa4p3/device.py @@ -13,6 +13,9 @@ def put(self, device): def all(self): return list( self.devices.values() ) + + def delete(self,id): + del self.devices[id] class AlexaDevice(object): def __init__(self, id): @@ -33,6 +36,9 @@ def __init__(self, id): self.alexa_auth_cred = '' self.alexa_color_value_type = '' self.proxied_Urls = {} + self.alexa_proxy_credentials='' + self.alias_for = '' + self.alexa_cam_modifiers = '' @classmethod def create_id_from_name(cls, name): @@ -78,39 +84,60 @@ def create_alias_devices(self): alias_device.description = self.description alias_device.action_items = self.action_items alias_device.types = self.types - + # P3-properties + alias_device.icon = self.icon + alias_device.thermo_config = self.thermo_config + alias_device.retrievable = self.retrievable + alias_device.proactivelyReported = self.proactivelyReported + alias_device.camera_setting = self.camera_setting + alias_device.camera_uri = self.camera_uri + alias_device.camera_imageUri = self.camera_imageUri + alias_device.alexa_auth_cred = self.alexa_auth_cred + alias_device.alexa_color_value_type = self.alexa_color_value_type + alias_device.proxied_Urls = self.proxied_Urls + alias_device.alexa_proxy_credentials = self.alexa_proxy_credentials + alias_device.alias_for = self.name alias_devices.append( alias_device ) return alias_devices - def validate(self, logger): + def validate(self, logger ,proto): if not self.id: msg = "Alexa-Device {}: empty identifier".format(self.id) - logger.error(msg) + logger.warning(msg) + proto.addEntry('WARNING',msg) return False elif len(self.id) > 128: msg = "Alexa-Device: {}: identifier '{}' too long >128".format(self.id, self.id) - logger.error(msg) + logger.warning(msg) + proto.addEntry('WARNING',msg) return False if not self.name: msg = "Alexa-Device {}: empty name".format(self.id) - logger.error(msg) + logger.warning(msg) + proto.addEntry('WARNING',msg) return False elif len(self.name) > 128: msg = "Alexa-Device: {}: name '{}' too long >128".format(self.id, self.name) - logger.error(msg) + logger.warning(msg) + proto.addEntry('WARNING',msg) return False if not self.description: - logger.warning("Alexa-Device {}: empty description, fallback to name '{}' - please set `alexa_description`".format(self.id, self.name)) + msg = "Alexa-Device {}: empty description, fallback to name '{}' - please set `alexa_description`".format(self.id, self.name) + logger.warning(msg) + proto.addEntry('WARNING',msg) self.description = self.name elif len(self.description) > 128: msg = "Alexa-Device {}: description '{}' too long >128".format(self.id, self.description) - logger.error(msg) + logger.warning(msg) + proto.addEntry('WARNING',msg) return False if not self.action_items: - logger.warning("Alexa-Device {}: no actions/items registered - please set `alexa_actions`".format(self.id)) + msg="Alexa-Device {}: no actions/items registered - please set `alexa_actions`".format(self.id) + logger.warning(msg) + proto.addEntry('WARNING',msg) return True diff --git a/alexa4p3/directives/Activate.cmd b/alexa4p3/directives/Activate.cmd new file mode 100644 index 000000000..82a5e3ded --- /dev/null +++ b/alexa4p3/directives/Activate.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Aktiviert eine Szene +json|{"directive": {"header": {"name": "Activate", "namespace": "Alexa.SceneController", "payloadVersion": "3", "messageId": "abc-123-def-456", "correlationToken": "dFMb0z+PgpgdDmluhJ1LddFvSqZ/jCc8ptlAKulUj90jSqg=="}, "endpoint": {"endpointId": "", "scope": {"token": "access-token-from-skill", "type": "BearerToken"}}, "payload": {}}} diff --git a/alexa4p3/directives/AdjustBrightness.cmd b/alexa4p3/directives/AdjustBrightness.cmd new file mode 100644 index 000000000..b78105b82 --- /dev/null +++ b/alexa4p3/directives/AdjustBrightness.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt den Dimmwert des Endpoint +json|{"directive": {"endpoint": {"scope": {"type": "BearerToken", "token": ""}, "cookie": {}, "endpointId": ""}, "header": {"payloadVersion": "3", "name": "AdjustBrightness", "messageId": "", "namespace": "Alexa.BrightnessController", "correlationToken": ""}, "payload": {"brightnessDelta": ""}}} diff --git a/alexa4p3/directives/AdjustPercentage.cmd b/alexa4p3/directives/AdjustPercentage.cmd new file mode 100644 index 000000000..c556f539c --- /dev/null +++ b/alexa4p3/directives/AdjustPercentage.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt Prozentwert relativ +json|{"directive": {"header": {"name": "AdjustPercentage", "namespace": "Alexa.PercentageController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {"percentageDelta": ""}}} diff --git a/alexa4p3/directives/AdjustPowerLevel.cmd b/alexa4p3/directives/AdjustPowerLevel.cmd new file mode 100644 index 000000000..792e021ce --- /dev/null +++ b/alexa4p3/directives/AdjustPowerLevel.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt Powerlevel relativ +json|{"directive": {"header": {"name": "AdjustPowerLevel", "namespace": "Alexa.PowerLevelController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {"powerLevelDelta": 12}}} diff --git a/alexa4p3/directives/AdjustRangeValue.cmd b/alexa4p3/directives/AdjustRangeValue.cmd new file mode 100644 index 000000000..7ce53623e --- /dev/null +++ b/alexa4p3/directives/AdjustRangeValue.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Ändert den Wert des Endpoint relativ +json|{"directive": {"endpoint": {"scope": {"type": "BearerToken", "token": ""}, "cookie": {}, "endpointId": ""}, "header": {"payloadVersion": "3", "name": "SetRangeValue", "namespace": "Alexa.RangeController", "instance": "", "messageId": "", "correlationToken": ""}, "payload": {"rangeValue": ""}}} diff --git a/alexa4p3/directives/DecreaseColorTemperature.cmd b/alexa4p3/directives/DecreaseColorTemperature.cmd new file mode 100644 index 000000000..f90ba0fa6 --- /dev/null +++ b/alexa4p3/directives/DecreaseColorTemperature.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Verringert die Lichttemperatur +json|{"directive": {"header": {"name": "DecreaseColorTemperature", "namespace": "Alexa.ColorTemperatureController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {}}} diff --git a/alexa4p3/directives/IncreaseColorTemperature.cmd b/alexa4p3/directives/IncreaseColorTemperature.cmd new file mode 100644 index 000000000..02fefb6fa --- /dev/null +++ b/alexa4p3/directives/IncreaseColorTemperature.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Erhöht die Lichttemperatur +json|{"directive": {"header": {"name": "IncreaseColorTemperature", "namespace": "Alexa.ColorTemperatureController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {}}} diff --git a/alexa4p3/directives/SetBrightness.cmd b/alexa4p3/directives/SetBrightness.cmd new file mode 100644 index 000000000..13fd30cea --- /dev/null +++ b/alexa4p3/directives/SetBrightness.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt den Dimmwert des Endpoint +json|{"directive": {"endpoint": {"scope": {"type": "BearerToken", "token": ""}, "cookie": {}, "endpointId": ""}, "header": {"payloadVersion": "3", "name": "SetBrightness", "messageId": "", "namespace": "Alexa.BrightnessController", "correlationToken": ""}, "payload": {"brightness": ""}}} diff --git a/alexa4p3/directives/SetColorTemperature.cmd b/alexa4p3/directives/SetColorTemperature.cmd new file mode 100644 index 000000000..aa3625617 --- /dev/null +++ b/alexa4p3/directives/SetColorTemperature.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt die Farbtemperatur +json|{"directive": {"header": {"name": "SetColorTemperature", "namespace": "Alexa.ColorTemperatureController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {"colorTemperatureInKelvin": ""}}} diff --git a/alexa4p3/directives/SetPercentage.cmd b/alexa4p3/directives/SetPercentage.cmd new file mode 100644 index 000000000..b812e2cce --- /dev/null +++ b/alexa4p3/directives/SetPercentage.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt Prozentwert absolut +json|{"directive": {"header": {"name": "SetPercentage", "namespace": "Alexa.PercentageController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {"percentage": ""}}} diff --git a/alexa4p3/directives/SetPowerLevel.cmd b/alexa4p3/directives/SetPowerLevel.cmd new file mode 100644 index 000000000..13e2f0bd4 --- /dev/null +++ b/alexa4p3/directives/SetPowerLevel.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt Powerlevel absolut +json|{"directive": {"header": {"name": "SetPowerLevel", "namespace": "Alexa.PowerLevelController", "payloadVersion": "3", "messageId": "", "correlationToken": ""}, "endpoint": {"endpointId": "", "scope": {"token": "", "type": "BearerToken"}, "cookie": {}}, "payload": {"powerLevel": ""}}} diff --git a/alexa4p3/directives/SetRangeValue.cmd b/alexa4p3/directives/SetRangeValue.cmd new file mode 100644 index 000000000..c5972df8f --- /dev/null +++ b/alexa4p3/directives/SetRangeValue.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Setzt den Wert des Endpoint absolut +json|{"directive": {"endpoint": {"scope": {"type": "BearerToken", "token": ""}, "cookie": {}, "endpointId": ""}, "header": {"payloadVersion": "3", "name": "SetRangeValue", "namespace": "Alexa.RangeController", "instance": "", "messageId": "", "correlationToken": ""}, "payload": {"rangeValue": ""}}} diff --git a/alexa4p3/directives/TurnOff.cmd b/alexa4p3/directives/TurnOff.cmd new file mode 100644 index 000000000..b942991d5 --- /dev/null +++ b/alexa4p3/directives/TurnOff.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Schaltet den jeweiligen Endpoint AUS +json|{"directive": {"header": {"payloadVersion": "3", "correlationToken": "", "namespace": "Alexa.PowerController", "name": "TurnOff", "messageId": ""}, "endpoint": {"endpointId": "", "cookie": {}, "scope": {"type": "BearerToken", "token": ""}}, "payload": {}}} diff --git a/alexa4p3/directives/TurnOn.cmd b/alexa4p3/directives/TurnOn.cmd new file mode 100644 index 000000000..7fbd0bfba --- /dev/null +++ b/alexa4p3/directives/TurnOn.cmd @@ -0,0 +1,3 @@ +apiurl|/localhost:9001 +description|Schaltet den jeweiligen Endpoint EIN +json|{"directive": {"header": {"payloadVersion": "3", "correlationToken": "", "namespace": "Alexa.PowerController", "name": "TurnOn", "messageId": ""}, "endpoint": {"endpointId": "", "cookie": {}, "scope": {"type": "BearerToken", "token": ""}}, "payload": {}}} diff --git a/alexa4p3/p3_action.py b/alexa4p3/p3_action.py index 7aa4db679..c2d266a38 100644 --- a/alexa4p3/p3_action.py +++ b/alexa4p3/p3_action.py @@ -36,43 +36,188 @@ def clamp_temp(temp, range): _min, _max = range return min(_max, max(_min, temp)) - +DEFAULT_RANGE_COLOR_TEMP = (1000,10000) + +def kelvin_2_percent(kelvin, device_range): + _minDevice, _maxDevice = device_range + range2Set = _maxDevice - _minDevice + percent_kelvin = 100.0/range2Set + kelvin2Set = kelvin-_minDevice + value_new = kelvin2Set*percent_kelvin + if value_new > 100.0: + value_new = 100.0 + if value_new < 0.0: + value_new = 0.0 + + return value_new + +def percent_2_kelvin(value, device_range): + _minDevice, _maxDevice = device_range + range2Set = _maxDevice - _minDevice + + kelvin2Add = (value/100.0*range2Set) + + value_new = round((kelvin2Add+_minDevice)/10)*10 + if value_new > _maxDevice: + value_new = _maxDevice + if value_new < _minDevice: + value_new = _minDevice + + + return value_new + #====================================================== # Start - A.Kohler # P3 - Directives #====================================================== + + + +## Alexa-ColorTemperatureController + +@alexa('SetColorTemperature', 'SetColorTemperature', 'colorTemperatureInKelvin','Alexa.ColorTemperatureController',[],"3") +def SetColorTemperature(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + new_kelvin = float( directive['payload']['colorTemperatureInKelvin'] ) + + for item in items: + percent_new = kelvin_2_percent(new_kelvin, item.alexa_range) + item_new = int(percent_new/100.0*255.0) + self.logger.info("Alexa P3: SetColorTemperature({}, {:.1f})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) # in 0-255 + self.response_Value = None + self.response_Value = int(new_kelvin) + + return self.p3_respond(directive) + + +@alexa('IncreaseColorTemperature', 'IncreaseColorTemperature', 'colorTemperatureInKelvin','Alexa.ColorTemperatureController',[],"3") +def IncreaseColorTemperature(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + + for item in items: + + item_now = item() + _min, _max = item.alexa_range + myPercentage = int(what_percentage(item_now, [0,255])) + new_kelvin = percent_2_kelvin(myPercentage,item.alexa_range) + new_kelvin = new_kelvin + int(item.alexa_color_temp_delta) + if (new_kelvin > _max): + new_kelvin = _max + percent_new = kelvin_2_percent(new_kelvin, item.alexa_range) + item_new = int(percent_new/100.0*255.0) + item( item_new, "alexa4p3" ) + + self.logger.info("Alexa P3: IncreaseColorTemperature({}, {:.1f})".format(item.id(), item_new)) + + self.response_Value = None + self.response_Value = int(new_kelvin) + + return self.p3_respond(directive) + + +@alexa('DecreaseColorTemperature', 'DecreaseColorTemperature', 'colorTemperatureInKelvin','Alexa.ColorTemperatureController',[],"3") +def DecreaseColorTemperature(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + + for item in items: + + item_now = item() + _min, _max = item.alexa_range + myPercentage = int(what_percentage(item_now, [0,255])) + new_kelvin = percent_2_kelvin(myPercentage,item.alexa_range) + new_kelvin = new_kelvin - int(item.alexa_color_temp_delta) + if (new_kelvin < _min): + new_kelvin = _min + percent_new = kelvin_2_percent(new_kelvin, item.alexa_range) + item_new = int(percent_new/100.0*255.0) + item( item_new, "alexa4p3" ) + + self.logger.info("Alexa P3: DecreaseColorTemperature({}, {:.1f})".format(item.id(), item_new)) + + self.response_Value = None + self.response_Value = int(new_kelvin) + + return self.p3_respond(directive) + +# Alexa-Range-Controller + +@alexa('AdjustRangeValue', 'AdjustRangeValue', 'rangeValue','Alexa.RangeController',[],"3") +def AdjustRangeValue(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + + percentage_delta = float( directive['payload']['rangeValueDelta'] ) + + for item in items: + item_range = self.item_range(item, DEFAULT_RANGE) + item_now = item() + percentage_now = what_percentage(item_now, item_range) + percentage_new = clamp_percentage(percentage_now + percentage_delta, item_range) + item_new = calc_percentage(percentage_new, item_range) + self.logger.info("Alexa P3: AdjustRangeValue({}, {:.1f})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = int(percentage_new) + + return self.p3_respond(directive) + +@alexa('SetRangeValue', 'SetRangeValue', 'rangeValue','Alexa.RangeController',[],"3") +def SetRangeValue(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + new_percentage = float( directive['payload']['rangeValue'] ) + newValue = str(new_percentage) + for item in items: + oldValue=str(item()) + item_range = self.item_range(item, DEFAULT_RANGE) + item_new = calc_percentage(new_percentage, item_range) + self.sh.Alexa4P3._proto.addEntry('INFO ','Changed item :{} from {} to {}'.format(item.property.name,oldValue,newValue)) + self.logger.info("Alexa P3: SetRangeValue({}, {:.1f})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = int(new_percentage) + + return self.p3_respond(directive) + + + + # Alexa ThermostatController -@alexa('SetThermostatMode', 'SetThermostatMode', 'thermostatMode','Alexa.ThermostatController',['SetTargetTemperature']) +@alexa('SetThermostatMode', 'SetThermostatMode', 'thermostatMode','Alexa.ThermostatController',['SetTargetTemperature'],"3") def SetThermostatMode(self, directive): # define Mode-Lists device_id = directive['endpoint']['endpointId'] items = self.items(device_id) - + AlexaItem = self.devices.get(device_id) myModes = AlexaItem.thermo_config myValueList = self.GenerateThermoList(myModes,1) myModeList = self.GenerateThermoList(myModes,2) - + # End of Modes-List - - - new_Mode = directive['payload']['thermostatMode']['value'] - - - + + + new_Mode = directive['payload']['thermostatMode']['value'] + + + item_new = myModeList[new_Mode] for item in items: self.logger.info("Alexa: SetThermostatMode({}, {})".format(item.id(), item_new)) item( item_new, "alexa4p3" ) - + #new_temp = items[0]() if items else 0 - + self.response_Value = new_Mode myValue = self.p3_respond(directive) return myValue -@alexa('AdjustTargetTemperature', 'AdjustTargetTemperature', 'targetSetpoint','Alexa.ThermostatController',['SetThermostatMode']) +@alexa('AdjustTargetTemperature', 'AdjustTargetTemperature', 'targetSetpoint','Alexa.ThermostatController',['SetThermostatMode'],"3") def AdjustTargetTemperature(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -87,7 +232,7 @@ def AdjustTargetTemperature(self, directive): item( item_new, "alexa4p3" ) new_temp = items[0]() if items else 0 - + self.response_Value = None self.response_Value = { "value": new_temp, @@ -95,8 +240,8 @@ def AdjustTargetTemperature(self, directive): } myValue = self.p3_respond(directive) return myValue - -@alexa('SetTargetTemperature', 'SetTargetTemperature', 'targetSetpoint','Alexa.ThermostatController',['SetThermostatMode']) + +@alexa('SetTargetTemperature', 'SetTargetTemperature', 'targetSetpoint','Alexa.ThermostatController',['SetThermostatMode'],"3") def SetTargetTemperature(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -111,7 +256,7 @@ def SetTargetTemperature(self, directive): item( item_new, "alexa4p3" ) new_temp = items[0]() if items else 0 - + self.response_Value = None self.response_Value = { "value": new_temp, @@ -119,12 +264,12 @@ def SetTargetTemperature(self, directive): } myValue = self.p3_respond(directive) return myValue - + # Alexa PowerController -@alexa('TurnOn', 'TurnOn', 'powerState','Alexa.PowerController',[]) +@alexa('TurnOn', 'TurnOn', 'powerState','Alexa.PowerController',[],"3") def TurnOn(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -134,11 +279,12 @@ def TurnOn(self, directive): self.logger.info("Alexa: turnOn({}, {})".format(item.id(), on)) if on != None: item( on, "alexa4p3" ) - self.response_Value = 'ON' + self.response_Value = 'ON' + self.sh.Alexa4P3._proto.addEntry('INFO ', 'Changed item :{} to {}'.format(item.property.name,self.response_Value)) myValue = self.p3_respond(directive) return myValue -@alexa('TurnOff', 'TurnOff', 'powerState','Alexa.PowerController',[]) +@alexa('TurnOff', 'TurnOff', 'powerState','Alexa.PowerController',[],"3") def TurnOff(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -154,7 +300,7 @@ def TurnOff(self, directive): # Alexa-Doorlock Controller -@alexa('Lock', 'Lock', 'lockState','Alexa.LockController',[]) +@alexa('Lock', 'Lock', 'lockState','Alexa.LockController',[],"3") def Lock(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -166,10 +312,10 @@ def Lock(self, directive): item( on, "alexa4p3" ) self.response_Value = None self.response_Value = 'LOCKED' - + return self.p3_respond(directive) -@alexa('Unlock', 'Unlock', 'lockState','Alexa.LockController',[]) +@alexa('Unlock', 'Unlock', 'lockState','Alexa.LockController',[],"3") def Unlock(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -181,17 +327,17 @@ def Unlock(self, directive): item( on, "alexa4p3" ) self.response_Value = None self.response_Value = 'UNLOCKED' - + return self.p3_respond(directive) -# Alexa-Brightness-Controller +# Alexa-Brightness-Controller -@alexa('AdjustBrightness', 'AdjustBrightness', 'brightness','Alexa.BrightnessController',[]) +@alexa('AdjustBrightness', 'AdjustBrightness', 'brightness','Alexa.BrightnessController',[],"3") def AdjustBrightness(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) - + percentage_delta = float( directive['payload']['brightnessDelta'] ) for item in items: @@ -204,10 +350,10 @@ def AdjustBrightness(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = int(percentage_new) - + return self.p3_respond(directive) -@alexa('SetBrightness', 'SetBrightness', 'brightness','Alexa.BrightnessController',[]) +@alexa('SetBrightness', 'SetBrightness', 'brightness','Alexa.BrightnessController',[],"3") def SetBrightness(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -227,11 +373,11 @@ def SetBrightness(self, directive): # Alexa-Percentage-Controller -@alexa('AdjustPercentage', 'AdjustPercentage', 'percentage','Alexa.PercentageController',[]) +@alexa('AdjustPercentage', 'AdjustPercentage', 'percentage','Alexa.PercentageController',[],"3") def AdjustPercentage(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) - + percentage_delta = float( directive['payload']['percentageDelta'] ) for item in items: @@ -244,10 +390,10 @@ def AdjustPercentage(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = int(percentage_new) - + return self.p3_respond(directive) - -@alexa('SetPercentage', 'SetPercentage', 'percentage','Alexa.PercentageController',[]) + +@alexa('SetPercentage', 'SetPercentage', 'percentage','Alexa.PercentageController',[],"3") def SetPercentage(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -260,17 +406,17 @@ def SetPercentage(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = int(new_percentage) - + return self.p3_respond(directive) # Alexa.PowerLevelController -@alexa('AdjustPowerLevel', 'AdjustPowerLevel', 'powerLevel','Alexa.PowerLevelController',[]) +@alexa('AdjustPowerLevel', 'AdjustPowerLevel', 'powerLevel','Alexa.PowerLevelController',[],"3") def AdjustPowerLevel(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) - + percentage_delta = float( directive['payload']['powerLevelDelta'] ) for item in items: @@ -283,10 +429,10 @@ def AdjustPowerLevel(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = int(percentage_new) - + return self.p3_respond(directive) -@alexa('SetPowerLevel', 'SetPowerLevel', 'powerLevel','Alexa.PowerLevelController',[]) +@alexa('SetPowerLevel', 'SetPowerLevel', 'powerLevel','Alexa.PowerLevelController',[],"3") def SetPowerLevel(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -299,13 +445,13 @@ def SetPowerLevel(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = int(new_percentage) - + return self.p3_respond(directive) # Scene Controller -@alexa('Activate', 'Activate', 'ActivationStarted','Alexa.SceneController',[]) +@alexa('Activate', 'Activate', 'ActivationStarted','Alexa.SceneController',[],"3") def Activate(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -318,35 +464,119 @@ def Activate(self, directive): item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = item_new - + return self.p3_respond(directive) +# Playback-Controller -@alexa('Play', 'Play', '','Alexa.PlaybackController',[]) +@alexa('Play', 'Play', '','Alexa.PlaybackController',[],"3") def Play(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC Play received ({}, {})".format(item.id(), item_new)) + item( item_new ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) -@alexa('Stop', 'Stop', '','Alexa.PlaybackController',[]) +@alexa('Stop', 'Stop', '','Alexa.PlaybackController',[],"3") def Stop(self, directive): device_id = directive['endpoint']['endpointId'] items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC Stop received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) +@alexa('FastForward', 'FastForward', '','Alexa.PlaybackController',[],"3") +def FastForward(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC FastForward received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) +@alexa('Next', 'Next', '','Alexa.PlaybackController',[],"3") +def Next(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) for item in items: - item_new = 1 - self.logger.info("Alexa P3: PBC Stop received ({}, {})".format(item.id(), item_new)) + item_new = 1 + self.logger.info("Alexa P3: PBC Next received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) + +@alexa('Pause', 'Pause', '','Alexa.PlaybackController',[],"3") +def Pause(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC Pause received ({}, {})".format(item.id(), item_new)) item( item_new, "alexa4p3" ) self.response_Value = None self.response_Value = item_new + + return self.p3_respond(directive) +@alexa('Previous', 'Previous', '','Alexa.PlaybackController',[],"3") +def Previous(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC Previous received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) + +@alexa('Rewind', 'Rewind', '','Alexa.PlaybackController',[],"3") +def Rewind(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC Rewind received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + + return self.p3_respond(directive) + +@alexa('StartOver', 'StartOver', '','Alexa.PlaybackController',[],"3") +def StartOver(self, directive): + device_id = directive['endpoint']['endpointId'] + items = self.items(device_id) + for item in items: + item_new = 1 + self.logger.info("Alexa P3: PBC StartOver received ({}, {})".format(item.id(), item_new)) + item( item_new, "alexa4p3" ) + self.response_Value = None + self.response_Value = item_new + return self.p3_respond(directive) # CameraStreamController -@alexa('InitializeCameraStreams', 'InitializeCameraStreams', 'cameraStreamConfigurations','Alexa.CameraStreamController',[]) +@alexa('InitializeCameraStreams', 'InitializeCameraStreams', 'cameraStreamConfigurations','Alexa.CameraStreamController',[],"3") def InitializeCameraStreams(self, directive): - p3tools.DumpStreamInfo(directive) device_id = directive['endpoint']['endpointId'] items = self.items(device_id) @@ -360,7 +590,7 @@ def InitializeCameraStreams(self, directive): return self.p3_respond(directive) # Authorization Interface -@alexa('AcceptGrant', 'AcceptGrant', 'AcceptGrant.Response','Alexa.Authorization',[]) +@alexa('AcceptGrant', 'AcceptGrant', 'AcceptGrant.Response','Alexa.Authorization',[],"3") def AcceptGrant(self, directive): self.logger.info("Alexa P3: AcceptGrant received ({})") myResponse = { @@ -380,7 +610,7 @@ def AcceptGrant(self, directive): # Scene Controller -@alexa('SetColor', 'SetColor', 'color','Alexa.ColorController',[]) +@alexa('SetColor', 'SetColor', 'color','Alexa.ColorController',[],"3") def SetColor(self, directive): new_hue = float( directive['payload']['color']['hue'] ) new_saturation = float( directive['payload']['color']['saturation'] ) @@ -390,19 +620,19 @@ def SetColor(self, directive): r,g,b = p3tools.hsv_to_rgb(new_hue,new_saturation,new_brightness) except Exception as err: self.logger.error("Alexa P3: SetColor Calculate to RGB failed ({}, {})".format(item.id(), err)) - + retValue=[] retValue.append(r) retValue.append(g) retValue.append(b) - + device_id = directive['endpoint']['endpointId'] items = self.items(device_id) - - + + for item in items: - + if 'alexa_color_value_type' in item.conf: colortype = item.conf['alexa_color_value_type'] if colortype == 'HSB': @@ -417,27 +647,27 @@ def SetColor(self, directive): retValue.append(1.0) else: retValue.append(new_brightness) - + self.logger.info("Alexa P3: SetColor ({}, {})".format(item.id(), str(retValue))) item( retValue, "alexa4p3" ) self.response_Value = None self.response_Value = directive['payload']['color'] - + return self.p3_respond(directive) #====================================================== # No directives only Responses for Reportstate #====================================================== -@alexa('ReportTemperature', 'ReportTemperature', 'temperature','Alexa.TemperatureSensor',[]) +@alexa('ReportTemperature', 'ReportTemperature', 'temperature','Alexa.TemperatureSensor',[],"3") def ReportTemperature(self, directive): print ("") -@alexa('ReportLockState', 'ReportLockState', 'lockState','Alexa.LockController',[]) +@alexa('ReportLockState', 'ReportLockState', 'lockState','Alexa.LockController',[],"3") def ReportLockState(self, directive): print ("") -@alexa('ReportContactState', 'ReportContactState', 'detectionState','Alexa.ContactSensor',[]) +@alexa('ReportContactState', 'ReportContactState', 'detectionState','Alexa.ContactSensor',[],"3") def ReportContactState(self, directive): print ("") #====================================================== diff --git a/alexa4p3/p3_tools.py b/alexa4p3/p3_tools.py index c4ae02aca..6738bc618 100644 --- a/alexa4p3/p3_tools.py +++ b/alexa4p3/p3_tools.py @@ -12,6 +12,7 @@ import json + def CreateStreamSettings(myItemConf): myRetVal = [] for k,v in myItemConf.camera_setting.items(): @@ -39,8 +40,13 @@ def CreateStreamPayLoad(myItemConf): i=0 for k,v in myItemConf.camera_setting.items(): - if myItemConf.alexa_auth_cred != '': + + if myItemConf.alexa_auth_cred != '' and myItemConf.alexa_proxy_credentials == '': uri = v['protocols'][0].lower()+"://"+myItemConf.alexa_auth_cred+'@'+cameraUri[i] + + elif myItemConf.alexa_proxy_credentials != '': + uri = v['protocols'][0].lower()+"://"+myItemConf.alexa_proxy_credentials+'@'+cameraUri[i] + else: uri = v['protocols'][0].lower()+"://"+cameraUri[i] # Find highest resolution @@ -67,6 +73,8 @@ def CreateStreamPayLoad(myItemConf): i +=1 response = {"cameraStreams": cameraStream} response.update({ "imageUri":imageuri}) + # only interesting for debugging + #DumpStreamInfo(response) return response def DumpStreamInfo(directive): diff --git a/alexa4p3/plugin.yaml b/alexa4p3/plugin.yaml old mode 100644 new mode 100755 index 2dc346799..7542610c5 --- a/alexa4p3/plugin.yaml +++ b/alexa4p3/plugin.yaml @@ -8,10 +8,10 @@ plugin: maintainer: andrek tester: psilo909, cannon, ASSR85, Juergen keywords: Amazon Echo Alexa Voicecontrol Sprachsteuerung Show Spot FireTV - documentation: https://www.smarthomeng.de/user/plugins/alexa4p3/user_doc.html # url of documentation (wiki) page + documentation: https://www.smarthomeng.de/user/plugins/alexa4p3/user_doc.html # url of documentation support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1021150-amazon-alexa-plugin - version: 1.0.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.0.2 # Plugin version + sh_minversion: 1.5.2 # minimum shNG version to use this plugin multi_instance: False # plugin supports multi instance classname: Alexa4P3 # class containing the plugin state: develop # State of the Plugin diff --git a/alexa4p3/requirements.txt b/alexa4p3/requirements.txt old mode 100644 new mode 100755 index 8b1378917..7491ce9ce --- a/alexa4p3/requirements.txt +++ b/alexa4p3/requirements.txt @@ -1 +1 @@ - +#requests requirement moved to core diff --git a/alexa4p3/service.py b/alexa4p3/service.py index 6a78da1e8..04da7ae4d 100644 --- a/alexa4p3/service.py +++ b/alexa4p3/service.py @@ -17,27 +17,51 @@ from . import p3_tools as p3tools - +ALEXA_Range_Controller={} DEFAULT_RANGE = (0, 100) DEFAULT_RANGE_LOGIC = (True, False) + +## Start - Definitions for global used Utterances from the global catalog +ALEXA_Range_Controller["capabilityResources"] = {"friendlyNames":[{"@type":"asset","value":{"assetId":"Alexa.Setting.Opening"}}]} +ALEXA_Range_Controller["configuration"] = {"supportedRange":{"minimumValue":0,"maximumValue":100,"precision":1},"unitOfMeasure":"Alexa.Unit.Percent"} +ALEXA_Range_Controller["semantics"] = {"actionMappings":[{"@type":"ActionsToDirective","actions":["Alexa.Actions.Close"],"directive":{"name":"SetRangeValue","payload":{"rangeValue":100}}},{"@type":"ActionsToDirective","actions":["Alexa.Actions.Open"],"directive":{"name":"SetRangeValue","payload":{"rangeValue":0}}},{"@type":"ActionsToDirective","actions":["Alexa.Actions.Lower"],"directive":{"name":"AdjustRangeValue","payload":{"rangeValueDelta":999,"rangeValueDeltaDefault":False}}},{"@type":"ActionsToDirective","actions":["Alexa.Actions.Raise"],"directive":{"name":"AdjustRangeValue","payload":{"rangeValueDelta":-999,"rangeValueDeltaDefault":False}}}],"stateMappings":[{"@type":"StatesToValue","states":["Alexa.States.Closed"],"value":0},{"@type":"StatesToRange","states":["Alexa.States.Open"],"range":{"minimumValue":1,"maximumValue":100}}]} + +## End - Definitions for global used Utterances from the global catalog + + + def what_percentage(value, range): _min, _max = range return ( (value - _min) / (_max - _min) ) * 100 - +def percent_2_kelvin(value, device_range): + _minDevice, _maxDevice = device_range + range2Set = _maxDevice - _minDevice + + kelvin2Add = (value/100.0*range2Set) + + value_new = round((kelvin2Add+_minDevice)/10)*10 + if value_new > _maxDevice: + value_new = _maxDevice + if value_new < _minDevice: + value_new = _minDevice + + + return value_new class AlexaService(object): - def __init__(self, logger, version, devices, actions, host, port, auth=None, https_certfile=None, https_keyfile=None): + def __init__(self, Proto, logger, version, devices, actions, host, port, auth=None, https_certfile=None, https_keyfile=None): self.logger = logger self.version = version self.devices = devices self.actions = actions - + self._proto = Proto + self._protocol = Proto self.logger.info("Alexa: service setup at {}:{}".format(host, port)) - handler_factory = lambda *args: AlexaRequestHandler(logger, version, devices, actions, *args) + handler_factory = lambda *args: AlexaRequestHandler(self._proto,logger, version, devices, actions, *args) self.server = HTTPServer((host, port), handler_factory) if https_certfile: # https://www.piware.de/2011/01/creating-an-https-server-in-python/ @@ -47,18 +71,23 @@ def __init__(self, logger, version, devices, actions, host, port, auth=None, htt def start(self): self.logger.info("Alexa: service starting") + self._proto.addEntry('INFO ', "Alexa - service starting") self.server.serve_forever() + def stop(self): self.logger.info("Alexa: service stopping") self.server.shutdown() + + class AlexaRequestHandler(BaseHTTPRequestHandler): - def __init__(self, logger, version, devices, actions, *args): + def __init__(self,Proto, logger, version, devices, actions, *args): self.logger = logger self.version = version self.devices = devices self.actions = actions + self._proto = Proto BaseHTTPRequestHandler.__init__(self, *args) @@ -212,12 +241,14 @@ def handle_discovery(self, header, payload): def p3_handle_discovery(self, header, payload): directive = header['name'] self.logger.debug("AlexaP3: discovery-directive '{}' received".format(directive)) + self._proto.addEntry('INFO ', "AlexaP3: discovery-directive '{}' received".format(directive)) if directive == 'Discover': self.respond(self.p3_discover_appliances()) else: msg = "unknown `header.name` '{}'".format(directive) self.logger.error(msg) + self._proto.addEntry('ERROR ', msg) self.send_error(400, explain=msg) def p3_discover_appliances(self): @@ -235,7 +266,7 @@ def p3_discover_appliances(self): newcapa = {"type": "AlexaInterface", "interface": "Alexa.EndpointHealth", "version": "3", - "properties": { + "properties" : { "supported": [ { "name": "connectivity" @@ -284,6 +315,7 @@ def p3_discover_appliances(self): } } # Check of special NameSpace + if NameSpace == 'Alexa.ThermostatController': AlexaItem = self.devices.get(device.id) myModeList = self.GenerateThermoList(AlexaItem.thermo_config, 2) @@ -295,7 +327,7 @@ def p3_discover_appliances(self): "supportedModes": myModes } - newcapa['properties']['configuation'] = mysupported + newcapa['properties']['configuration'] = mysupported mysupported=[ {"name" : 'thermostatMode'}, {"name" : 'targetSetpoint'} @@ -331,15 +363,41 @@ def p3_discover_appliances(self): "version": "3", "supportedOperations" : myModes } + + + if NameSpace == 'Alexa.RangeController': + try: + if hasattr(device, "alexa_range_delta"): + alexa_range_delta = device.alexa_range_delta + else: + alexa_range_delta = 20 # default + + myConfig = json.dumps(ALEXA_Range_Controller["semantics"]) + myConfig = myConfig.replace("-999",str(int(alexa_range_delta)*-1)) + myConfig = myConfig.replace("999",str(int(alexa_range_delta))) + myConfig = json.loads(myConfig) + + if ("EXTERIOR_BLIND" in device.icon or "INTERIOR_BLIND" in device.icon): + newcapa["instance"] = device.id + newcapa["capabilityResources"] = ALEXA_Range_Controller["capabilityResources"] + newcapa["configuration"] = ALEXA_Range_Controller["configuration"] + newcapa["semantics"] = myConfig + + except Exception as e: + pass + # End Check special Namespace mycapabilities.append(newcapa) if device.icon == None: device.icon = ["SWITCH"] + + + appliance = { "endpointId": device.id, "friendlyName": device.name, - "description": "SmartHomeNG", + "description": device.description + " by SmartHomeNG", "manufacturerName": "SmarthomeNG", "displayCategories": device.icon, @@ -351,6 +409,7 @@ def p3_discover_appliances(self): } discovered.append(appliance) + return { "event": { "header": { @@ -406,146 +465,175 @@ def p3_ReportState(self, directive): AlexaItem = self.devices.get(device_id) myItems = AlexaItem.backed_items() + self._proto.addEntry('INFO ', "received ReportState for '{}' ".format(device_id)) Properties = [] differentNameSpace = '' myValue = None alreadyReportedControllers = [] # walk over all Items examp..: Item: OG.Flur.Spots.dimmen / Item: OG.Flur.Spots for Item in myItems: + msg = "" # Get all Actions for this item action_names = list( map(str.strip, Item.conf['alexa_actions'].split(' ')) ) # über alle Actions für dieses item for myActionName in action_names: - myAction = self.actions.by_name(myActionName) - differentNameSpace = '' - # all informations colletec (Namespace, ResponseTyp, ..... - # check if capabilitie is alredy in - self.logger.info("Alexa: ReportState", Item._name) - if myAction.response_type not in str(alreadyReportedControllers): - #if myAction.namespace not in str(alreadyReportedControllers): - #if myAction.response_type not in str(Properties): - Propertyname = myAction.response_type - if myAction.namespace == "Alexa.PowerController": - myValue=Item() - if myValue == 0: - myValue = 'OFF' - elif myValue == 1: - myValue = 'ON' - - elif myAction.namespace == "Alexa.BrightnessController": - item_range = Item.alexa_range - item_now = Item() - myValue = int(what_percentage(item_now, item_range)) - - elif myAction.namespace == "Alexa.PowerLevelController": - item_range = Item.alexa_range - item_now = Item() - myValue = int(what_percentage(item_now, item_range)) + try: + myAction = self.actions.by_name(myActionName) + differentNameSpace = '' + # all informations colletec (Namespace, ResponseTyp, ..... + # check if capabilitie is alredy in + self.logger.debug("Alexa: ReportState for {}".format(Item._name)) + if myAction.response_type not in str(alreadyReportedControllers): + #if myAction.namespace not in str(alreadyReportedControllers): + #if myAction.response_type not in str(Properties): + Propertyname = myAction.response_type + if myAction.namespace == "Alexa.PowerController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + myValue=Item() + if myValue == 0: + myValue = 'OFF' + elif myValue == 1: + myValue = 'ON' - elif myAction.namespace == "Alexa.LockController" and myAction.name != 'ReportLockState': - continue - - elif myAction.namespace == "Alexa.LockController" and myAction.name == 'ReportLockState': - item_new = Item() - if item_new == 0: - myValue = 'UNLOCKED' - elif item_new == 1: - myValue = 'LOCKED' - elif item_new == 254: - myValue = 'JAMMED' - else: - myValue = 'JAMMED' # no known value -> blocked - - - elif myAction.namespace == "Alexa.PercentageController": - item_range = Item.alexa_range - item_now = Item() - myValue = int(what_percentage(item_now, item_range)) - - elif myAction.namespace == "Alexa.ThermostatController" and myAction.response_type == 'targetSetpoint': - item_now = Item() - myValue = { - "value": item_now, - "scale": "CELSIUS" - } - elif myAction.namespace == "Alexa.ThermostatController" and myAction.response_type == 'thermostatMode': - item_now = Item() - myModes = AlexaItem.thermo_config - myValueList = self.GenerateThermoList(myModes,1) - myIntMode = int(item_now) - myMode = self.search(myValueList, str(myIntMode)) + elif myAction.namespace == "Alexa.RangeController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_range = Item.alexa_range + item_now = Item() + myValue = int(what_percentage(item_now, item_range)) - myValue = myMode + elif myAction.namespace == "Alexa.ColorTemperatureController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_now = Item() + myPercentage = int(what_percentage(item_now, [0,255])) + myValue = percent_2_kelvin(myPercentage,Item.alexa_range) + + elif myAction.namespace == "Alexa.BrightnessController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_range = Item.alexa_range + item_now = Item() + myValue = int(what_percentage(item_now, item_range)) - elif myAction.namespace == 'Alexa.TemperatureSensor': - item_now = Item() - myValue = { - "value": item_now, - "scale": "CELSIUS" - } - - elif myAction.namespace == 'Alexa.ContactSensor': - myValue=Item() - if myValue == 0: - myValue = 'DETECTED' # means Contact is open - elif myValue == 1: - myValue = 'NOT_DETECTED' # means Contact is closed - - elif myAction.namespace == 'Alexa.ColorController': - myValue=Item() - if len(myValue) == 0: - myValue.append(0) - myValue.append(0) - myValue.append(0) - try: - myColorTyp = Item.conf['alexa_color_value_type'] - except Exception as err: - # default = RGB - myColorTyp = 'RGB' - if myColorTyp == 'HSB': - myHSB = myValue - else: + elif myAction.namespace == "Alexa.PowerLevelController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_range = Item.alexa_range + item_now = Item() + myValue = int(what_percentage(item_now, item_range)) + + elif myAction.namespace == "Alexa.LockController" and myAction.name != 'ReportLockState': + continue + + elif myAction.namespace == "Alexa.LockController" and myAction.name == 'ReportLockState': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_new = Item() + if item_new == 0: + myValue = 'UNLOCKED' + elif item_new == 1: + myValue = 'LOCKED' + elif item_new == 254: + myValue = 'JAMMED' + else: + myValue = 'JAMMED' # no known value -> blocked + + + elif myAction.namespace == "Alexa.PercentageController": + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_range = Item.alexa_range + item_now = Item() + myValue = int(what_percentage(item_now, item_range)) + + elif myAction.namespace == "Alexa.ThermostatController" and myAction.response_type == 'targetSetpoint': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_now = Item() + myValue = { + "value": item_now, + "scale": "CELSIUS" + } + elif myAction.namespace == "Alexa.ThermostatController" and myAction.response_type == 'thermostatMode': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_now = Item() + myModes = AlexaItem.thermo_config + myValueList = self.GenerateThermoList(myModes,1) + myIntMode = int(item_now) + myMode = self.search(myValueList, str(myIntMode)) + + myValue = myMode + + elif myAction.namespace == 'Alexa.TemperatureSensor': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + item_now = Item() + myValue = { + "value": item_now, + "scale": "CELSIUS" + } + + elif myAction.namespace == 'Alexa.ContactSensor': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + myValue=Item() + if myValue == 0: + myValue = 'DETECTED' # means Contact is open + elif myValue == 1: + myValue = 'NOT_DETECTED' # means Contact is closed + + elif myAction.namespace == 'Alexa.ColorController': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + myValue=Item() + if len(myValue) == 0: + myValue.append(0) + myValue.append(0) + myValue.append(0) try: - myHSB = p3tools.rgb_to_hsv(myValue[0], myValue[1], myValue[2]) + myColorTyp = Item.conf['alexa_color_value_type'] except Exception as err: - print(err) - - myValue ={ - "hue": myHSB[0], - "saturation": myHSB[1], - "brightness": myHSB[2] - } - elif myAction.namespace == 'Alexa.PlaybackController': - print('Playback arrived') - differentNameSpace = 'Alexa.PlaybackStateReporter' - myValue ={ - "state": "PLAYING" - } - if differentNameSpace == '': - differentNameSpace = myAction.namespace - - #==================================================== - # Add default values if nothing is reported - #==================================================== - if myAction.namespace not in alreadyReportedControllers: - if myAction.namespace == 'Alexa.LockController' and myValue == None: - myValue = 'LOCKED' - - MyNewProperty = { - "namespace":differentNameSpace, - "name":Propertyname, - "value":myValue, - - "timeOfSample":myTimeStamp, - "uncertaintyInMilliseconds":5000 - } - + # default = RGB + myColorTyp = 'RGB' + if myColorTyp == 'HSB': + myHSB = myValue + else: + try: + myHSB = p3tools.rgb_to_hsv(myValue[0], myValue[1], myValue[2]) + except Exception as err: + print(err) + + myValue ={ + "hue": myHSB[0], + "saturation": myHSB[1], + "brightness": myHSB[2] + } + elif myAction.namespace == 'Alexa.PlaybackController': + msg="calculating Value for Controller'{}' - Item : {} ".format(Item.property.name,myAction.namespace) + differentNameSpace = 'Alexa.PlaybackStateReporter' + myValue ={ + "state": "PLAYING" + } + if differentNameSpace == '': + differentNameSpace = myAction.namespace + + #==================================================== + # Add default values if nothing is reported + #==================================================== + if myAction.namespace not in alreadyReportedControllers: + if myAction.namespace == 'Alexa.LockController' and myValue == None: + myValue = 'LOCKED' + + MyNewProperty = { + "namespace":differentNameSpace, + "name":Propertyname, + "value":myValue, + "timeOfSample":myTimeStamp, + "uncertaintyInMilliseconds":5000 + } + + # Take care for Controllers with instances + if differentNameSpace == 'Alexa.RangeController': + MyNewProperty["instance"] = device_id - Properties.append(MyNewProperty) - alreadyReportedControllers.append(myAction.response_type) + + Properties.append(MyNewProperty) + alreadyReportedControllers.append(myAction.response_type) #alreadyReportedControllers.append(myAction.namespace) - + except: + self._proto.addEntry('ERROR ', msg) # Add the EndpointHealth Property MyNewProperty ={ "namespace": "Alexa.EndpointHealth", @@ -590,13 +678,17 @@ def p3_ReportState(self, directive): } - + self._proto.addEntry('INFO ', "respondig ReportState for '{}' ".format(myEndPointID)) return myResponse def p3_handle_control(self, header, payload,mydirective): directive = header['name'] - + try: + device_id = mydirective["endpoint"]["endpointId"] + except : + device_id = 'unknown' self.logger.debug("Alexa: control-directive '{}' received".format(directive)) + if header['name'] == 'ReportState': directive = header['namespace']+header['name'] try: @@ -604,24 +696,28 @@ def p3_handle_control(self, header, payload,mydirective): return except Exception as e: self.logger.error("Alexa P3: execution of control-directive '{}' failed: {}".format(directive, e)) + self._proto.addEntry('ERROR ', "Alexa P3: execution of control-directive '{}' failed: {}".format(directive, e)) self.respond({ 'header': self.header('DriverInternalError', 'Alexa.ConnectedHome.Control'), 'payload': {} }) return - + self._proto.addEntry('INFO ', "received Directive {} for '{}' Payload : {}".format(directive, device_id, json.dumps(mydirective))) action = self.actions.for_directive(directive) if action: try: + self._proto.addEntry('INFO ', "response Payload : {}".format(json.dumps(mydirective))) self.respond( action(mydirective) ) except Exception as e: self.logger.error("Alexa P3: execution of control-directive '{}' failed: {}".format(directive, e)) + self._proto.addEntry('ERROR ', "Alexa P3: execution of control-directive '{}' failed: {}".format(directive, e)) self.respond({ 'header': self.header('DriverInternalError', 'Alexa.ConnectedHome.Control'), 'payload': {} }) else: self.logger.error("Alexa P3: no action implemented for directive '{}'".format(directive)) + self._proto.addEntry('ERROR ', "Alexa P3: no action implemented for directive '{}'".format(directive)) self.respond({ 'header': self.header('UnexpectedInformationReceivedError', 'Alexa.ConnectedHome.Control'), 'payload': {} diff --git a/alexa4p3/user_doc.rst b/alexa4p3/user_doc.rst old mode 100644 new mode 100755 index ede81222a..e218bf6aa --- a/alexa4p3/user_doc.rst +++ b/alexa4p3/user_doc.rst @@ -1,7 +1,7 @@ -.. index:: Plugins; Alexa4P3 (Unterstützung von Amazon Echo/Alexa Geräten -.. index:: Alexa +.. index:: Plugins; Alexa4P3 (Unterstützung von Amazon Echo/Alexa Geräten) +.. index:: Alexa4P3 -alexa4p3 +Alexa4P3 ### Konfiguration @@ -12,14 +12,16 @@ Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/co .. important:: - Das Alexa-Plugin kann nicht mit **SmartHomeNG-Versionen vor v1.3.0** genutzt werden. + Das Alexa-Plugin kann nicht mit **SmartHomeNG-Versionen vor v1.5.2** genutzt werden. Es wird dann nicht geladen. Webinterface ------------------------ -aktuell gibt es kein Webinterface für das Plugin +Das Alexa4P3-Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzt übersichtlich dargestellt werden. +Das Web-Interface enthält ein selbst rotierendes Protokoll für die Kommunikation mit dem Amazon-Servern. +Mehr Funktionen des Web-Interfaces siehe unten. Beispielfunktionen @@ -28,15 +30,12 @@ Beispielfunktionen Beleuchtung einschalten : **Alexa, schalte das Küchenlicht ein** - **Alexa, dimme das Küchenlicht um 10 Prozent** - **Alexa, stelle das Küchenlicht auf 40 Prozent** Temperatur einstellen: **Alexa, stelle die Temperatur in der Küche auf 25 Grad** - **Alexa, erhöhe die Temperatur in der Küche um 2 Grad** Temperatur abfragen : @@ -54,4 +53,27 @@ Kameras zeigen (nur Show / Spot / FireTV-Geräte) +Webinterface-Funktionen +------------------------ + +Auf der ersten Seite werden alle Alexa-Geräte, die definierten Actions sowie die jeweiligen Aliase angezeigt. Actions in Payload-Version 3 werden grün angezeigt. Actions in Payload-Version 2 werden in rot angezeigt. +Eine Zusammenfassung wird oben rechts dargestellt. Durch anklicken eine Zeile kann ein Alexa-Geräte für die Testfunktionen auf Seite 3 des Web-Interfaces auswewählt werden + +.. image:: assets/Alexa4P3_Seite1.jpg + :class: screenshot + +Auf der Zweiten Seite wird ein Kommunikationsprotokoll zur Alexa-Cloud angezeigt. + +.. image:: assets/Alexa4P3_Seite2.jpg + :class: screenshot + +Auf Seite drei können “Directiven” ähnlich wie in der Lambda-Test-Funktion der Amazon-Cloud ausgeführt werden. Der jeweilige Endpunkt ist auf Seite 1 duch anklicken zu wählen. Die Kommunikation wird auf Seite 2 protokolliert. +So könnne einzelne Geräte und “Actions” getestet werden. + +.. image:: assets/Alexa4P3_Seite3.jpg + :class: screenshot + +Auf Seite 4 kann interaktiv ein YAML-Eintrag für einen Alexa-Kamera erzeugt werden. Der fertige YAML-Eintrag wird unten erzeugt und kann via Cut & Paste in die Item-Definition von shNG übernommen werden. +.. image:: assets/Alexa4P3_Seite4.jpg + :class: screenshot diff --git a/alexa4p3/webif/static/img/favicon.ico b/alexa4p3/webif/static/img/favicon.ico new file mode 100755 index 000000000..8a22cecf1 Binary files /dev/null and b/alexa4p3/webif/static/img/favicon.ico differ diff --git a/alexa4p3/webif/static/img/logo_big.png b/alexa4p3/webif/static/img/logo_big.png new file mode 100755 index 000000000..93865a18e Binary files /dev/null and b/alexa4p3/webif/static/img/logo_big.png differ diff --git a/alexa4p3/webif/static/img/plugin_logo.png b/alexa4p3/webif/static/img/plugin_logo.png new file mode 100644 index 000000000..5e89dcc89 Binary files /dev/null and b/alexa4p3/webif/static/img/plugin_logo.png differ diff --git a/alexa4p3/webif/static/img/plugin_logo_old.png b/alexa4p3/webif/static/img/plugin_logo_old.png new file mode 100755 index 000000000..31b76039a Binary files /dev/null and b/alexa4p3/webif/static/img/plugin_logo_old.png differ diff --git a/alexa4p3/webif/static/js/handler.js b/alexa4p3/webif/static/js/handler.js new file mode 100644 index 000000000..8265a1a67 --- /dev/null +++ b/alexa4p3/webif/static/js/handler.js @@ -0,0 +1,314 @@ +//************************************************************* +// JS-Handler-Script for Alexa4P3 +// +// (C) Andre Kohler andre.kohler01@googlemail.com +// +// Change-Log +// +// 2020-03-28 - added Test-Functions to Web-IF +// 2019-12-18 - Change-Log eingeführt +//************************************************************* + + + + +//************************************************************* +// check Auto-Updates for protocols +//************************************************************* +setInterval(Checkupdate4Protocolls, 5000); + + +//************************************************************* +// delete Protocols +//************************************************************* + +function DeleteProto(btn_Name) +{ + + if (btn_Name == "btn_clear_proto_states") + { proto_Name = "webif.state_protocoll"} + + $.ajax({ + url: "clear_proto.html", + type: "GET", + data: { proto_Name : proto_Name + }, + contentType: "application/json; charset=utf-8", + success: function (response) { + ClearProto(proto_Name); + }, + error: function () { + console.log("Error - while clearing Protocol :"+proto_Name) + } + }); +}; + +//************************************************************* +// clear Protocol +//************************************************************* +function ClearProto(proto_Name) +{ + + if (proto_Name == 'webif.state_protocoll') + { + statelogCodeMirror.setValue("") + } +} + + +//************************************************************* +// check Auto-Updates for protocols +//************************************************************* +function Checkupdate4Protocolls() +{ + states_checked = document.getElementById("proto_states_check").checked + if (states_checked == true) + { + UpdateProto('state_log_file') + } +} + + +//************************************************************* +// actualisation of Protocol +//************************************************************* +function actProto(response,proto_Name) +{ + myProto = document.getElementById(proto_Name) + myProto.value = "" + myText = "" + var objResponse = JSON.parse(response) + for (x in objResponse) + { + myText += objResponse[x]+"\n" + } + myProto.value = myText + if (proto_Name == 'state_log_file') + { + statelogCodeMirror.setValue(myText) + } +} + +//************************************************************* +// Auto-Update-Timer for protocol - States +//************************************************************* + +function UpdateProto(proto_Name) +{ + $.ajax({ + url: "get_proto.html", + type: "GET", + data: { proto_Name : proto_Name + }, + contentType: "application/json; charset=utf-8", + success: function (response) { + actProto(response,proto_Name); + }, + error: function () { + console.log("Error - while updating Protocol :"+proto_Name) + } + }); +}; + +//******************************************* +// set color for missing values +//******************************************* +function setColor2White(_elemtenID) +{ + document.getElementById(_elemtenID).style.background = "white" +} + +//*********************************************** +// set color to white by restart creating YAML +//*********************************************** +function setColor2WhiteForAll() +{ +myInputs=document.getElementsByTagName("INPUT"); +for (i = 0; i < myInputs.length; i++) + { + myInputs[i].style.backgroundColor = "white"; + } +} + +//******************************************* +// GetValues for Credentials +//******************************************* + +function GetValueFromID(ID) +{ +myValue = document.getElementById(ID).value + if (myValue == "") + { + myError = true + document.getElementById(ID).style.background = "red" + } + return myValue +} +//******************************************* +// Button Handler CreateYaml +//******************************************* + +function CreateYaml() +{ +setColor2WhiteForAll() +// Definition for Alexa-Response + CrLf = '\n' + indent = " " + YAML = "$ItemName:" + CrLf + YAML +=indent + "alexa_description: $description" + CrLf + YAML +=indent + "alexa_name: $alexa_name" + CrLf + YAML +=indent + "alexa_device: $alexa_device" + CrLf + YAML +=indent + "alexa_auth_cred: '$cam_credentials'" + CrLf + YAML +=indent + "alexa_icon: CAMERA" + CrLf + YAML +=indent + "alexa_actions: InitializeCameraStreams" + CrLf + YAML +=indent + "alexa_camera_imageUri: $Image_Uri " + CrLf + YAML +=indent + "alexa_csc_proxy_uri: $proxy_Url" + ":443" + CrLf + YAML +=indent + "alexa_proxy_credentials: '$proxy_credentials'" + +// Definition for Stream + myStream = '{ "protocols":["$protocol"], "resolutions":[{"width":$width,"height":$height}], "authorizationTypes":["$authorization"], "videoCodecs":["$video"], "audioCodecs":["$audio"] }' + +// Defintion for URL's + myUrl = 'Stream#:$Stream_IP' + myStreamURLs = indent + 'alexa_csc_uri: "$Stream_URLs"'+ CrLf + + // Get Values from WebPage + if (document.getElementById("enable_stream_1").checked != true) + { + alert("You have to activate minimum one stream") + return + } + + Values2Replace = { + '$ItemName':'Cam_Name', + '$description':'Alexa_Description', + '$alexa_name':'Alexa_Name', + '$alexa_device':'Alexa_Device', + '$Image_Uri':'Image_Url', + '$proxy_Url':'Proxy_Url', + '$credentials':'user' + } + + Stream2Replace = { + '$width':'width_#', + '$height':'height_#' + } + + StreamSelect2Replace = { + '$video':'video_#', + '$audio':'audio_#', + '$authorization':'authorization_#', + '$protocol':'protocol_#' + } + + +myStreamUrls = {} + +myError = false + + +// Create Credentials + +myCamUser = GetValueFromID("user_cam") +myCamPwd = GetValueFromID("pwd_cam") +cam_Credentials = String(btoa(myCamUser+":"+myCamPwd)) +YAML = YAML.replace("$cam_credentials", cam_Credentials) + + +myProxyUser = GetValueFromID("user") +myProxyPwd = GetValueFromID("pwd") +proxy_Credentials = String(btoa(myProxyUser+":"+myProxyPwd)) +YAML = YAML.replace("$proxy_credentials", proxy_Credentials) + + + +for ( var key in Values2Replace) + { + if (Values2Replace.hasOwnProperty(key)) + { + myValue = document.getElementById(Values2Replace[key]).value + if (myValue == "") + { + myError = true + document.getElementById(Values2Replace[key]).style.background = "red" + } + YAML = YAML.replace(key, myValue) + } + } + + + for ( i=1; i <=3; i++) + { + myNewStream = myStream + myNewStream = myNewStream.replace('#', String(i)) + // Skip not enabled Streams + if (document.getElementById("enable_stream_"+String(i)).checked == true) + { + // Create Stream-String + console.log("Got info to setup Streamstring for : "+ String(i)) + for ( var key in Stream2Replace) + { + if (myNewStream.hasOwnProperty(key)) + { + myValue = document.getElementById(Stream2Replace[key]).value + if (myValue == "") + { + myError = true + document.getElementById(Stream2Replace[key]).style.background = "red" + } + myNewStream = myNewStream.replace(key, myValue) + } + } + // Now the Select-Boxes + myNewStreamSelect2Replace = StreamSelect2Replace + myNewStream = myStream + myNewStreamSelect2Replace = JSON.parse(JSON.stringify(myNewStreamSelect2Replace).split('#').join(String(i))) + for ( var key in myNewStreamSelect2Replace) + { + if (myNewStreamSelect2Replace.hasOwnProperty(key)) + { + myValue = document.getElementById(myNewStreamSelect2Replace[key]).value + + myNewStream = myNewStream.replace(key, myValue) + } + } + console.log(myNewStream) + // Now the Settings for Resolution + myNewResulotion2Replace = Stream2Replace + myNewResulotion2Replace = JSON.parse(JSON.stringify(myNewResulotion2Replace).split('#').join(String(i))) + for ( var key in myNewResulotion2Replace) + { + if (Stream2Replace.hasOwnProperty(key)) + { + myValue = document.getElementById(myNewResulotion2Replace[key]).value + if (myValue == "") + { + myError = true + document.getElementById(myNewResulotion2Replace[key]).style.background = "red" + } + myNewStream = myNewStream.replace(key, myValue) + } + } + YAML += CrLf + indent + "alexa_stream_"+String(i) + ": "+ "'" + myNewStream +"'" + // Now Get the Url for this Stream + myValue = document.getElementById("real_IP_"+String(i)).value + if (myValue == "") + { + myError = true + document.getElementById("real_IP_"+String(i)).style.background = "red" + } + myStreamUrls["Stream"+String(i)]=myValue + } + } + YAML += CrLf + indent + "alexa_csc_uri: "+"'"+JSON.stringify(myStreamUrls)+"'" + if (myError == true) + { + alert("Some Values are missing \r\n See red Fields") + return + } + + yaml_resultCodeMirror.setValue(YAML) + +} + + diff --git a/alexa4p3/webif/static/js/test_handler.js b/alexa4p3/webif/static/js/test_handler.js new file mode 100644 index 000000000..b1329fa06 --- /dev/null +++ b/alexa4p3/webif/static/js/test_handler.js @@ -0,0 +1,447 @@ +var selectedDevice; + +//******************************************* +// Button Handler for saving Commandlet +//******************************************* + +function BtnSave(result) +{ + document.getElementById("txtresult").value = ""; + + + if (document.getElementById("txtCmdName").value == "") + { + alert ("No Name given for CommandLet, please enter one"); + return; + } + if (document.getElementById("txtApiUrl").value == "") + { + alert ("No API-URL given for CommandLet, please enter one"); + return; + } + + document.getElementById("txtButton").value ="BtnSave"; + + myPayload = myCodeMirrorConf.getValue(); + StoreCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + myPayload, + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); + +} + +//******************************************* +// Button Handler for checking Json +//******************************************* + +function BtnCheck(result) +{ + + document.getElementById("txtButton").value ="BtnCheck"; + try { + // Block of code to try + myValue = document.getElementById("txtValue").value + myPayload = myCodeMirrorConf.getValue(); + myPayload = myPayload.replace("",myValue); + var myTest = JSON.stringify(JSON.parse(myPayload),null,2) + myCodeMirrorConf.setValue(myTest); + myCodeMirrorConf.focus; + myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + document.getElementById("txtresult").value = "JSON-Structure is OK"; + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + + } + catch(err) { + // Block of code to handle errors + document.getElementById("txtresult").value = "JSON-Structure is not OK\n"+err; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } +} + +//******************************************* +// Button Handler for testing +//******************************************* + +function BtnTest(result) +{ + selectedDevice = document.getElementById("selectedDevice").value; + + if (selectedDevice == "no Device selected") + { + alert ("No Device selected for Test, first select one"); + return; + } + txtValue = document.getElementById("txtValue").value; + if (txtValue == "" && myCodeMirrorConf.getValue().search("nValue") > 0) + { + alert ("No Value set to send, please enter value"); + return; + } + + document.getElementById("txtButton").value ="BtnTest"; + myPayload = myCodeMirrorConf.getValue(); + + TestCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + myPayload, + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); +} + +//******************************************* +// Button Handler for deleting +//******************************************* + +function BtnDelete(result) +{ + buildCmdSequence(); + return + var filetodelete = document.getElementById("txtCmdName").value; + if (filetodelete == "") { + alert ("No Command selected to delete, first select one"); + return; + } + filetodelete=filetodelete+".cmd"; + var r = confirm("Your really want to delete\n\n"+ filetodelete + "\n\nContinue ?"); + if (r == false) { + return; + } + document.getElementById("txtButton").value ="BtnDelete"; + DeleteCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + "", + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); +} + + +//************************************************************* +// ValidateLoginResponse -checks the login-button +//************************************************************* + +function ValidateLoginResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse) + { + temp = temp + objResponse[x]+"\n"; + } + +document.getElementById("txt_Result").innerHTML = temp; +} + +//************************************************************* +// ValidateEncodeResponse -checks the login-button +//************************************************************* + +function ValidateEncodeResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse) + { + if (x == "0") + { + document.getElementById("txtEncoded").value = objResponse[x].substr(8); + } + else + { + temp = temp + objResponse[x]+"\n"; + } + } + +document.getElementById("txt_Result").value = temp; +} + + + +//************************************************************* +// ValidateResponse - checks the response for button-Actions +//************************************************************* + +function ValidateResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse[0]) + { + if (x == "Status") { + myResult = objResponse[0][x]; + } + else { + temp = temp + objResponse[0][x]+"\n"; + } + } + +document.getElementById("txtresult").value = temp; +if (myResult == "OK") +{ + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + reloadCmds(); +} +else +{ + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; +} + +} + +//******************************************* +// Function to Test Command-Let +//******************************************* + +function TestCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : txtValue, + selectedDevice:selectedDevice, + txtButton : "BtnTest", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Function to Save Command-Let +//******************************************* + +function StoreCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:selectedDevice, + txtButton : "BtnSave", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Function to Delete Command-Let +//******************************************* + +function DeleteCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:selectedDevice, + txtButton : "BtnDelete", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + + +//************************************************ +// OnClick-function for Command-List +//************************************************ + +function SelectCmd() +{ + +//$("#AlexaDevices").on("click", "tr",function() + + var value = $(this).closest("tr").find("td").first().text(); + + if (value != "") { + alert(value); + } + +} + +//************************************************ +// builds and show table with saves Commandlets +//************************************************ + + +function build_cmd_list(result) +{ + + var temp =""; + temp = "
"; + temp = temp + ""; + temp = temp + ""; + + $.each(result, function(index, element) { + temp = temp + ""; + + }) + temp = temp + "
Command-Name
"+ element.Name + "
"; + $('#Cmds').html(temp); + + $('#tableCommands').on("click", "tr",function() + { + var value = $(this).closest("tr").find("td").first().text(); + if (value != "") { + LoadCommand(value); + } + }); + + + +} + + +//******************************************* +// reloads the list with the Command-Lets +//******************************************* + +function reloadCmds() +{ + $("#refresh-element").addClass("fa-spin"); + $("#reload-element").addClass("fa-spin"); + $("#cardOverlay").show(); + $.getJSON("build_cmd_list_html", function(result) + { + build_cmd_list(result); + window.setTimeout(function() + { + $("#refresh-element").removeClass("fa-spin"); + $("#reload-element").removeClass("fa-spin"); + $("#cardOverlay").hide(); + }, 300); + + }); + +} + +//******************************************* +// Load Commandlet to Web-Site +//******************************************* +function LoadCommand(txtCmdName) +{ + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:"", + txtButton : "BtnLoad", + txt_payload : "", + txtCmdName : txtCmdName, + txtApiUrl : "", + txtDescription : ""} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ShowCommand(response,txtCmdName); + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Load 2 Fields +//******************************************* +function ShowCommand(response,txtCmdName) +{ + var myResult = "" + var temp = "" + var objResponse = JSON.parse(response) + document.getElementById("txtCmdName").value = txtCmdName; + for (x in objResponse[0]) + { + if (x == "Status") + { + myResult = objResponse[0][x]; + } + else if (x == "Description") + { + console.log(objResponse[0][x]) + var test = objResponse[0][x]; + + document.getElementById("txtDescription").value = objResponse[0][x]; + } + else if (x == "myUrl") + { + console.log(objResponse[0][x]) + document.getElementById("txtApiUrl").value = objResponse[0][x]; + } + else if (x == "payload") + { + myjson = objResponse[0][x].split("'").join("\""); + myjson = myjson.split("\\").join(""); + var myTest = JSON.stringify(JSON.parse(myjson),null,2) + myCodeMirrorConf.setValue(myTest); + myCodeMirrorConf.focus; + myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + } + } + document.getElementById("txtresult").value = myResult; + if (myResult == "OK") + { + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + reloadCmds(); + } + else + { + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } +} + + diff --git a/alexa4p3/webif/templates/index.html b/alexa4p3/webif/templates/index.html new file mode 100755 index 000000000..63be7e50f --- /dev/null +++ b/alexa4p3/webif/templates/index.html @@ -0,0 +1,560 @@ + + + + + + + + + + + +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} +{% set use_bodytabs = true %} +{% set tabcount = 4 %} +{% set tab1title = _('Alexa-Devices') ~ "(" ~ item_count ~ ")" %} +{% set tab2title = _('Protocol') %} +{% set tab3title = _('Test-functions') %} +{% set tab4title = _('Create CamProxy Settings') %} + + + +{% block headtable %} + + + + + + + + + + + + + + + + +
{{ _('Service Port') }}{{ p.service_port }}
{{ _('used Actions') }}{{ payload_action }}
{{ _('selected Endpoint') }} {{ _('no Device selected') }}
+{% endblock headtable %} + +{% block buttons %} +{% endblock buttons %} + +{% set tabcount = 4 %} + +{% block bodytab1 %} +
+
+ + + + + + + + + + + + {% for item in items %} + + + + + + + + + + {% endfor %} + +
{{ _('Device-Name') }}{{ _('Device-ID') }}{{ _('implemented Actions') }}{{ _('linked to Item') }}{{ _('alias for Device') }}
{{ item.AlexaName }}{{ item.DeviceID }}{{ item.Actions }}{{ item.Items }}{{ item.Alias_for }}
+
+
+ + + +{% endblock bodytab1 %} + +{% block bodytab2 %} + + + + + + + +
+ + +
+ + +
+
+
+
+ + {% if state_log_lines %}{% else %}{{ _('no data available') }}{% endif %} +
+
+
+
+
+
+
+
+ + +{% endblock bodytab2 %} + +{% block bodytab3 %} + + + + + + + + + + + + +
+ +
+ +
+
+
+
+
+
+ + {{ _('existing Directives') }} +
+
+
+
+
+ +
+
+
+ {{ cmd_list }} +
+
+
+
+
+ + +
+ + +
+ + + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
{{ _('(Payload for Api-Call in json-Format') }}
+ +
+ +
+ +
+
+
+
+
+
+ + +
+ +
+
+ + + +{% endblock bodytab3 %} + +{% block bodytab4 %} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Camera Properties') }}
{{ _('Item-Name :') }}
{{ _('Alexa-Name of the Camera :') }}
{{ _('Alexa-Device of the Camera :') }}
{{ _('Alexa-Description :') }}
Proxy-URL (https) :
{{ _('Image-URL :') }}
{{ _('Credentials for the AlexaCamProxy4P3:') }}{{ _('User :' ) }} {{ _('Password :') }} +
{{ _('Credentials for the Cam himself:') }}{{ _('User :') }} {{ _('Password :') }} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Stream -Properties') }}
+
+ + +
+
+
+ + +
+
+
+ + +
+
{{ _('Resolution of the Stream :' ) }}
{{ _('Stream URL in your LAN :') }}
Video-Codec : + + + + + +
Audio-Codec : + + + + + +
{{ _('Stream-Protocol :' ) }} + + + + + +
{{ _('Authorization Type :') }} + + + + + +
+
+ +
+
+
+
+ + + {% if yaml_result %}{% else %}{{ _('no data available') }}{% endif %} +
+
+
+ +{% endblock bodytab4 %} + + + + diff --git a/alexarc4shng/.idea/.gitignore b/alexarc4shng/.idea/.gitignore new file mode 100644 index 000000000..5c98b4288 --- /dev/null +++ b/alexarc4shng/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/alexarc4shng/.idea/alexarc4shng.iml b/alexarc4shng/.idea/alexarc4shng.iml new file mode 100644 index 000000000..671160631 --- /dev/null +++ b/alexarc4shng/.idea/alexarc4shng.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/inspectionProfiles/Project_Default.xml b/alexarc4shng/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..6bacd1e29 --- /dev/null +++ b/alexarc4shng/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/markdown-navigator.xml b/alexarc4shng/.idea/markdown-navigator.xml new file mode 100644 index 000000000..6b82ce2ab --- /dev/null +++ b/alexarc4shng/.idea/markdown-navigator.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/markdown-navigator/profiles_settings.xml b/alexarc4shng/.idea/markdown-navigator/profiles_settings.xml new file mode 100644 index 000000000..db0626632 --- /dev/null +++ b/alexarc4shng/.idea/markdown-navigator/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/misc.xml b/alexarc4shng/.idea/misc.xml new file mode 100644 index 000000000..6c993b77f --- /dev/null +++ b/alexarc4shng/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/modules.xml b/alexarc4shng/.idea/modules.xml new file mode 100644 index 000000000..4c62daab7 --- /dev/null +++ b/alexarc4shng/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/alexarc4shng/.idea/vcs.xml b/alexarc4shng/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/alexarc4shng/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/alexarc4shng/README.md b/alexarc4shng/README.md new file mode 100755 index 000000000..cff9dd7f9 --- /dev/null +++ b/alexarc4shng/README.md @@ -0,0 +1,431 @@ +# AlexaRc4shNG + +#### Version 1.0.2 + +The plugin gives the possibilty to control an Alexa-Echo-Device remote by smartHomeNG. So its possible to switch on an TuneIn-Radio Channel, send some messages via Text2Speech when an event happens on the knx-bus or on the Visu. On the Web-Interface you can define your own commandlets (functions). The follwing functions are available on the Web-Interface : + +- Store a cookie-file to get access to the Alexa-WebInterface +- manually Login with your credentials (stored in the /etc/plugin.yaml) +- See all available devices, select one to send Test-Functions +- define Commandlets - you can load,store,delete, check and test Commandlets +- the Commandlets can be loaded to the webinterface by clicking on the list +- the Json-Structure can be checked on the WebInterface + +In the API-URL and in the json-payload you have to replace the real values from the Alexa-Webinterface with the following placeholders. For testing functions its not really neccessary to use the placeholders. + +This plugin for smarthomeNG is mainly based on the informations of +[Lötzimmer](https://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) ,[Apollon77](https://github.com/Apollon77/alexa-remote) and the [openhab2](https://community.openhab.org/t/released-openhab2-amazon-echo-control-binding-controlling-alexa-from-openhab2/37844) + +Special thanks to Jonofe from the [Edomi-Forum](https://knx-user-forum.de/forum/projektforen/edomi/1240964-alexa-smarthome-skill-payload-version-3) who spent a nigth and half an evenning to support me with SSML. +#### !! So many thanks for the very good research and development in the past !! + +## table of content + +1. [PlaceHolders](#placeholders) +2. [Change Log](#changelog) +3. [Requirements](#requirements) +4. [Cookie](#cookie) +5. [Configuration](#config) +6. [functions](#functions) +7. [Web-Interface](#webinterface) +8. [How to implement new Commands](#newCommand) +9. [Tips for existing Command-Lets](#tipps) + +### Existing Command-Lets + + +- Play (Plays the last paused Media) +- Pause (pauses the actual media) +- Text2Speech (sends a Text to the echo, echo will speak it) +- StartTuneInStation (starts a TuneInRadiostation with the guideID you send) +- SSML (Speak to Text with[Speech Synthesis Markup Language](https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)) +- VolumeAdj (adjusts the volume during playing some media not working from webinterface test functions) +- VolumeSet (sets the volume to value from 0-100 percent) + +
+### Placeholders : +```yaml + = Value to send as alpha + = Value to send as numeric +#item.path/# = item-path of the value that should be inserted into text or ssml + = SerialNo. of the device where the command should go to + = device family + = deviceType + = OwnerID of the device +``` +#### !! Please keep in mind to use the "<", ">", "#" and "/#" to qualify the placeholders !! + +## ChangeLog + +#### 2020.03.20 Version 1.0.2 + +- changed public function "send_cmd_by_curl" to "send_cmd" +- removed pycurl +- changed Communication to Python Requests +- added translation for the Web-Interface +- added public function "get_last_alexa" + +#### 2018.07.26 Version 1.0.1 +- Encoding credentials now possible on the Web-Interface (for security reason use this function to encode you credentials) + +#### 2018.05.20 Version 1.0.1 +- replaced lib.scheduler with self.scheduler_add + +#### 2018.05.19 - Version 1.0.1 +- changed version to 1.0.1 +- changed to lib.item and lib.scheduler +- the credentials have to be stored in base64 encoded +- added Login / LogOff Button to the Web-Interface +- added Auto-Login function - when there is no cookie-file with correct values and credentials are specicified, the plugin automaticaly logs in +- the log-in (the cookie) will be refreshed after the login_update_cycle +- changed methods-names and parameters to lower case and underscore separated names + + +#### 2018.04.30 - Version 1.0.0 +- added CommandLet for SSML-Support +- added CommandLet for Play (Plays the paused media) +- added CommandLet for Pause (pauses media) +- added CommandLet for VolumeAdj (only working while media is playing, not working from test functions on the webinterface) +- added CommandLet for VolumeSet (working all the time) +- added CommandLet for LoadPlayerInfo (right now loaded but nowhere stored) +- added Item to enable AlexaRemoteControl by UZSU + +### Changes Since version 1.x.x + +- no Changes, first Version + + + +## Requirements + + +### Needed software + +* smarthomeNg 1.5.2 and above for the web-interface +* needs Python requests +* a valid [Cookie](#cookie) from an alexa.amazon-Web-Site Session +* if you work with Autologin the credentials have to be entered "base64"-encoded. You can encode you credentials on the web-interface of the plugin "user.test@gmail.com:your_pwd" you will get ```dXNlci50ZXN0QGdtYWlsLmNvbTp5b3VyX3B3ZA==``` . +So please enter ```dXNlci50ZXN0QGdtYWlsLmNvbTp5b3VyX3B3ZA==``` in the /etc/plugin.yaml + +If you don trust the website for encoding you credential, you can do it in the python-console. +Open a terminal and try the following code. + +``` +python3 +import base64 +base64.b64encode('user.test@gmail.com:your_pwd'.encode('utf-8')) + +you will get + +b'dXNlci50ZXN0QGdtYWlsLmNvbTp5b3VyX3B3ZA==' + +use + +dXNlci50ZXN0QGdtYWlsLmNvbTp5b3VyX3B3ZA== + +for your credentials +``` + + + +### Supported Hardware + +* all Amazon Echo-Devices + +## Cookie + +First possibility - without Credentials : + +Plugins are available for most of the common browsers. +After installing the plugin you have to login to your alexa.amazon-Web console. Now Export the cookie by using the plugin. +Open the cookie-file with a Texteditor select all and copy it to the clipboard. +Go to the Webinterface of the plugin and paste the content of the cookie-file to the textarea on Tab "Cookie-Handling". Store the cookie. +When the cookie was successfull stored you can find you Echo-Devices on the Tab with the Alexa-devices. + +Second possibility - with Credentials : + +When the plugin will be started and credentials are found in plugin.yaml, the plugin tests if the informations in the cookie-file are still guilty. If not the plugin tries to login with the credentials himself and stores the informations in the cookie-file. The cookie will updated in the cycle specified in "login_update_cycle" in the plugin.yaml + +## Configuration + +### plugin.yaml + +The plugin needs to be defined in the /etc/plugin.yaml in this way.

+The attributes are :
+plugin_name -> fix AlexaRc4shNG
+ +cookiefile -> the path to the cookie-file. Here it will stored from the Web-Interfache. Take care that you have write-permissions
+host -> the adress of you Alexa-WebInterface
+Item2EnableAlexaRC->Item controlled by UZSU or something else which enables the communication to Alexa-Amazon-devices. if you leave it blank the communication is enabled all the time 24/7. This item is only checked during update_item in smarthomeNG. If you use the API directly from a logic or from the Webinterface the item will not be checked. In logics you have to check it yourself.

AlexaCredentials->User and Password for the Amazon-Alex-WebSite for automtic login
+alexa_credentials-> user:pwd (base64 encoded)
+item_2_enable_alexa_rc -> Item to allow smarthomeNG to send Commands to Echo's
+login_update_cycle->seconds to wait for automatic Login in to refresh the cookie + + + +```yaml +AlexaRc4shNG: + plugin_name: AlexaRc4shNG + cookiefile: /usr/local/smarthome/plugins/alexarc4shng/cookies.txt + host: alexa.amazon.de + item_2_enable_alexa_rc: Item_to_enable_Alexaremote + alexa_credentials: : + login_update_cycle: 432000 +``` + + + +### items.yaml + +The configuration of the item are done in the following way : +

+alexa_cmd_01: comparison:EchoDevice:Commandlet:Value_to_Send +
+ +### supported comparisons are : + +"True", "False" and for numeric values "<=",">=","=","<",">" + +#### Sample to switch on a Radiostation by using TuneIN

+```yaml +Value = True means the item() becomes "ON" +EchodotKueche = Devicename where the Command should be send to StartTuneInStaion = Name of the Commandlet +s96141 = Value of the Radiostation (here S96141 = baden.fm) +``` + +example: +` +alexa_cmd_01: True:EchoDotKueche:StartTuneInStation:s96141 +` +#### Sample to send Text with item-value included based on value lower then 20 degrees

+ +```yaml +Value = <20.0 - send command when value of the item becomes less then 20.0 +EchodotKueche = Devicename where the Command should be send to +Text2Speech = Name of the Commandlet +Value_to_Send = Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt #test.testzimmer.temperature.actual/# Grad #test.testzimmer.temperature.actual/# = item-path of the value that should be inserted +``` + +example:
+` +alexa_cmd_01: <20.0:EchoDotKueche:Text2Speech:Die Temperatur in der Kueche ist niedriger als 20 Grad Die Temperatur ist jetzt \#test.testzimmer.temperature.actual/\# Grad +` + +You can find the paths of the items on the backend-WebInterface - section items. + +#### alexa_cmd_XX + +You can specify up to 99 Commands per shng-item. The plugin scanns the item.conf/item.yaml during initialization for commands starting with 01 up to 99. + +Please start all the time with 01 per item, the command-numbers must be serial, dont forget one. The scan of commands stops when there is no command found with the next number + +#### Example + +Example for settings in an item.yaml file : + +```yaml +# items/my.yaml +%YAML 1.1 +--- + +OG: + + Buero: + name: Buero + Licht: + type: bool + alexa_name: Licht Büro + alexa_description: Licht Büro + alexa_actions: TurnOn TurnOff + alexa_icon: LIGHT + alexa_cmd_01: True:EchoDotKueche:StartTuneInStation:s96141 + alexa_cmd_02: True:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist eingeschalten + alexa_cmd_03: False:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist aus + alexa_cmd_04: 'False:EchoDotKueche:Pause: ' + visu_acl: rw + knx_dpt: 1 + knx_listen: 1/1/105 + knx_send: 1/1/105 + enforce_updates: 'true' + +``` +Example for settings in an item.conf file : + +```yaml +# items/my.conf + +[OG] + [[Buero]] + name = Buero + [[[Licht]]] + type = bool + alexa_name = "Licht Büro" + alexa_description = "Licht Büro" + alexa_actions = "TurnOn TurnOff" + alexa_icon = "LIGHT" + alexa_cmd_01 = '"True:EchoDotKueche:StartTuneInStation:s96141" + alexa_cmd_02 ="True:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist eingeschalten" + alexa_cmd_03 = "False:EchoDotKueche:Text2Speech:Hallo das Licht im Buero ist aus" + alexa_cmd_04 = "False:EchoDotKueche:Pause: " + visu_acl = rw + knx_dpt = 1 + knx_listen = 1/1/105 + knx_send = 1/1/105 + enforce_updates = truey_attr: setting +``` + +### logic.yaml +Right now no logics are implemented. But you can trigger the functions by your own logic + + +## Plugin-functions + +The plugin provides the following publich functions. You can use it for example in logics. + +### send_cmd(dvName, cmdName, mValue) + +example how to use in logics: + +```yaml +sh.alexarc4shng.send_cmd("yourDevice", "Text2Speech", "yourValue") +--- +sh.alexarc4shng.send_cmd('Kueche','Text2Speech','Der Sensor der Hebenlage signalisiert ein Problem.') +``` +Sends a command to the device. "dvName" is the name of the device, "cmdName" is the name of the CommandLet, mValue is the value you would send. +You can find all this informations on the Web-Interface. +You can also user the [placeholders](#placeholders) + +- the result will be the HTTP-Status of the request as string (str) + +### get_last_alexa() + +This function returns the Device-Name of the last Echo Device which got a voice command. You can use it in logics to trigger events based on the last used Echo device. + +```yaml +myLastDevice = sh.alexarc4shng.get_last_alexa() + +``` +# Web-Interface + +The Webinterface is reachable on you smarthomeNG server here :
+ +yourserver:8383/alexarc4shng/ + +## Cookie-Handling + +On the Webinterface you can store you cookie-file to the shng-Server. +Export it with a cookie.txt AddOn from the browser. Copy it to the clipboard. +Paste it to the textarea in the Web-Interface and Store it. + +Now the available devices from your alexa-account will be discoverd an shown on the second tab. + +You can also login / logoff when credentials are available. Please see results in the textarea on the right. Please refresh page manually after successfull login via the Web-Interface. + +![PlaceHolder](./assets/webif1.jpg "jpg") + +## Alexa devices + +By click on one device the device will be selected as acutal device for tests. +![PlaceHolder](./assets/webif2.jpg "jpg") + +## Command-Handling + +The Web-Interface gives help to define new Command-Lets. How you get the informations for new Commands see [section New Commands](#newCommand) + +Here you can define new Command-Lets, test them, save and delete them. +You can check the JSON-Structure of you payload. + +When you click on an existing Command-Let it will be load to the Web-Interface. + +You can enter test values in the field for the values. Press Test and the command will be send to the device. You get back the HTTP-Status of the Request. + +For test dont modify the payload, just use the test-value-field + +![PlaceHolder](./assets/webif3.jpg "jpg") +
+ +## How to create new Command-Lets (spy out the Amazon-Web-Interface) + +#### This documentation is based on Google-Chrome, but it's also possible to do this with other browsers. + +Open the Web-Interface for Alexa on Amazon. Select the page you want to spy out. Before click the command open the Debugger of the browser (F12). Select the network tab. +When you click the command that you want to spy out the network traffic will be displayed in the debugger. Here you can get all the informations you need. +Normally information will be send to amazon. So you have to concentrate on Post - Methods. + + +![PlaceHolder](./assets/pic1.jpg "jpg") +
+
+As example to spy out the station-id of a TuneIn Radio Station-ID you will it see it directly on context when you move your mouse to the post-command. +You can copy the URL to the Clipboard an use is it in the AlexaRc4shNG. + +You can also copy it as cUrl Paste it into an editor and can find the payload in the --data section of the curl + +
+
+ +![PlaceHolder](./assets/pic2.jpg "jpg") + +For some commands you need to now the payload. You can get this by spying out the data. You have to select the network command. Then select the tab with Headers. In the bottom you will find the form-data. You can copy the payload to the clipboard an paste it into the AlexaRcshNG-WebInterface. + + +![PlaceHolder](./assets/pic3.jpg "jpg") + +#### !! Dont forget to replace the values for deviceOwnerCustomerIdcustomerID serialNumber, serialNumber, family with the placeholders !! +``` + + + + + +!! for the Values !! + + (for alpha Values) + (for numeric Values ) + +``` + +## Tips for existing Command-Lets : + +#### TuneIn +You have to specify the guideID from Amazom as stationID "mValue". Station-Names are not supported right now. +for example try the following: + +To locate your station ID, search for your station on TuneIn.com. Access your page and use the last set of digits of the resulting URL for your ID. For example: +If your TuneIn.com URL is 'http://tunein.com/radio/tuneinstation-s######/', then your station ID would be 's######' +(https://help.tunein.com/what-is-my-station-or-program-id-SJbg90quwz) + +#### SSML +You have to put the SSML-Values into +``` + +``` + +Find complete documentation to SSML [here](https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html) + +example : +``` + +I want to tell you a secret.I am not a real human.. + Can you believe it? + +``` + +You can also use [SpeechCons](https://developer.amazon.com/docs/custom-skills/speechcon-reference-interjections-german.html#including-a-speechcon-in-the-text-to-speech-response) + +example +``` + + Here is an example of a speechcon. + ach du liebe zeit.. + +``` +## Credits + +The idea for writing this plugin came from henfri. Got most of the informations from : http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html (German). Thank you Alex! A lot of code came from Ingo. He has done the alexa iobrokern implementation https://github.com/Apollon77 Thank you Ingo ! Also a lot of informations come from for the open-hab2 implemenation! Thank you [Michael](https://community.openhab.org/t/released-openhab2-amazon-echo-control-binding-controlling-alexa-from-openhab2/37844) + +Special thanks to Jonofe from the Edomi-Forum who spent a nigth and half an evenning to support my with SSML. Thank you Andre. +#### !! So many thanks for the very good research and development) +## Trademark Disclaimer + +TuneIn, Amazon Echo, Amazon Echo Spot, Amazon Echo Show, Amazon Music, Amazon Prime, Alexa and all other products and Amazon, TuneIn and other companies are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. diff --git a/alexarc4shng/__init__.py b/alexarc4shng/__init__.py new file mode 100755 index 000000000..1adf74d95 --- /dev/null +++ b/alexarc4shng/__init__.py @@ -0,0 +1,1454 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020 AndreK andre.kohler01@googlemail.com +######################################################################### +# This file is part of SmartHomeNG. +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.5.2 and +# upwards. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +from lib.module import Modules +from lib.model.smartplugin import * +from lib.item import Items +from lib.shtime import Shtime + + +from datetime import datetime +from io import BytesIO + +from subprocess import Popen, PIPE +import json +import sys +import os +import re +import urllib3 +import time +import base64 +import requests + + + + + + + +class shngObjects(object): + def __init__(self): + self.Devices = {} + + def exists(self, id): + return id in self.Devices + + def get(self, id): + return self.Devices[id] + + def put(self, newID): + self.Devices[newID] = Device() + + def all(self): + return list( self.Devices.values() ) + +class Device(object): + def __init__(self): + self.Commands=[] + +class Cmd(object): + def __init__(self, id): + self.id = id + self.command = '' + self.ItemValue = '' + self.EndPoint = '' + self.Action = '' + self.Value = '' + + +############################################################################## +class EchoDevices(object): + def __init__(self): + self.devices = {} + + def exists(self, id): + return id in self.devices + + def get(self, id): + return self.devices[id] + + def put(self, device): + self.devices[device.id] = device + + def all(self): + return list( self.devices.values() ) + + def get_Device_by_Serial(self, serialNo): + for device in self.devices: + if (serialNo == self.devices[device].serialNumber): + return self.devices[device].id +class Echo(object): + def __init__(self, id): + self.id = id + self.name = "" + self.serialNumber = "" + self.family = "" + self.deviceType = "" + self.deviceOwnerCustomerId = "" + self.playerinfo = {} + self.queueinfo = {} + +############################################################################## + +class AlexaRc4shNG(SmartPlugin): + PLUGIN_VERSION = '1.0.2' + ALLOW_MULTIINSTANCE = False + """ + Main class of the Plugin. Does all plugin specific stuff and provides + the update functions for the items + """ + + def __init__(self, sh, *args, **kwargs): + # get Instances + self.logger = logging.getLogger(__name__) + self.sh = self.get_sh() + self.items = Items.get_instance() + self.shngObjects = shngObjects() + self.shtime = Shtime.get_instance() + + # Init values + self.header = '' + self.cookie = {} + self.csrf = 'N/A' + self.postfields='' + self.login_state = False + self.last_update_time = '' + self.next_update_time = '' + # get parameters + self.cookiefile = self.get_parameter_value('cookiefile') + self.host = self.get_parameter_value('host') + self.AlexaEnableItem = self.get_parameter_value('item_2_enable_alexa_rc') + self.credentials = self.get_parameter_value('alexa_credentials').encode('utf-8') + self.credentials = base64.decodebytes(self.credentials).decode('utf-8') + self.LoginUpdateCycle = self.get_parameter_value('login_update_cycle') + self.update_file=self.sh.get_basedir()+"/plugins/alexarc4shng/lastlogin.txt" + self.rotating_log = [] + + if not self.init_webinterface(): + self._init_complete = False + + return + + def run(self): + """ + Run method for the plugin + """ + self.logger.info("Plugin '{}': start method called".format(self.get_fullname())) + # get additional parameters from files + self.csrf = self.parse_cookie_file(self.cookiefile) + + # Check login-state - if logged off and credentials are availabel login in + if os.path.isfile(self.cookiefile): + self.login_state=self.check_login_state() + self.check_refresh_login() + + if (self.login_state == False and self.credentials != ''): + try: + os.remove(self.update_file) + except: + pass + self.check_refresh_login() + self.login_state=self.check_login_state() + + # Collect all devices + if (self.login_state): + self.Echos = self.get_devices_by_request() + else: + self.Echos = None + # enable scheduler if Login should be updated automatically + + if self.credentials != '': + self.scheduler_add('check_login', self.check_refresh_login,cycle=300) + #self.scheduler.add('plugins.alexarc4shng.check_login', self.check_refresh_login,cycle=300,from_smartplugin=True) + self.alive = True + + # if you want to create child threads, do not make them daemon = True! + # They will not shutdown properly. (It's a python bug) + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.debug("Plugin '{}': stop method called".format(self.get_fullname())) + self.scheduler_remove('check_login') + self.alive = False + + def parse_item(self, item): + itemFound=False + i=1 + + myValue = 'alexa_cmd_{}'.format( '%0.2d' %(i)) + while myValue in item.conf: + + + self.logger.debug("Plugin '{}': parse item: {} Command {}".format(self.get_fullname(), item,myValue)) + + CmdItem_ID = item._name + try: + myCommand = item.conf[myValue].split(":") + + + if not self.shngObjects.exists(CmdItem_ID): + self.shngObjects.put(CmdItem_ID) + + actDevice = self.shngObjects.get(CmdItem_ID) + actDevice.Commands.append(Cmd(myValue)) + + actCommand = len(actDevice.Commands)-1 + + actDevice.Commands[actCommand].command = item.conf[myValue] + myCommand = actDevice.Commands[actCommand].command.split(":") + self.logger.info("Plugin '{}': parse item: {}".format(self.get_fullname(), item.conf[myValue])) + + actDevice.Commands[actCommand].ItemValue = myCommand[0] + actDevice.Commands[actCommand].EndPoint = myCommand[1] + actDevice.Commands[actCommand].Action = myCommand[2] + actDevice.Commands[actCommand].Value = myCommand[3] + itemFound=True + + except Exception as err: + print("Error:" ,err) + i += 1 + myValue = 'alexa_cmd_{}'.format( '%0.2d' %(i)) + + # todo + # if interesting item for sending values: + # return update_item + if itemFound == True: + return self.update_item + else: + return None + + def parse_logic(self, logic): + pass + + def update_item(self, item, caller=None, source=None, dest=None): + + # Item was not changed but double triggered the Upate_Item-Function + if (self.AlexaEnableItem != ""): + AlexaEnabledItem = self.items.return_item(self.AlexaEnableItem) + if AlexaEnabledItem() != True: + return + + if item._type == "str": + newValue=str(item()) + oldValue=str(item.prev_value()) + elif item._type =="num": + newValue=float(item()) + oldValue=float(item.prev_value()) + else: + newValue=str(item()) + oldValue=str(item.prev_value()) + + # Nur bei Wertänderung, sonst nix wie raus hier + if(oldValue == newValue): + return + + + try: + myEchos = self.sh.alexarc4shng.Echos.all() + + except Exception as err: + self.logger.debug("Error while getting Echos :",err) + # End Test + + + + CmdItem_ID = item._name + + + if self.shngObjects.exists(CmdItem_ID): + self.logger.debug("Plugin '{}': update_item ws called with item '{}' from caller '{}', source '{}' and dest '{}'".format(self.get_fullname(), item, caller, source, dest)) + + actDevice = self.shngObjects.get(CmdItem_ID) + + for myCommand in actDevice.Commands: + + newValue2Set = myCommand.Value + myItemBuffer = myCommand.ItemValue + # Spezialfall auf bigger / smaller + if myCommand.ItemValue.find("<=") >=0: + actValue = "<=" + myCompValue = myCommand.ItemValue.replace("<="," ") + myCompValue = myCompValue.replace(".",",") + myCompValue = float(myCompValue) + myCommand.ItemValue = actValue + if newValue > myCompValue: + return + elif myCommand.ItemValue.find(">=") >=0: + actValue = ">=" + myCompValue = myCommand.ItemValue.replace(">="," ") + myCompValue = myCompValue.replace(".",",") + myCompValue = float(myCompValue) + myCommand.ItemValue = actValue + if newValue < myCompValue: + return + elif myCommand.ItemValue.find("=") >=0 : + actValue = "=" + myCompValue = myCommand.ItemValue.replace("="," ") + myCompValue = myCompValue.replace(".",",") + myCompValue = float(myCompValue) + myCommand.ItemValue = actValue + if newValue != myCompValue: + return + elif myCommand.ItemValue.find("<") >=0: + actValue = "<" + myCompValue = myCommand.ItemValue.replace("<"," ") + myCompValue = myCompValue.replace(".",",") + myCompValue = float(myCompValue) + myCommand.ItemValue = actValue + if newValue >= myCompValue : + return + elif myCommand.ItemValue.find(">") >=0: + actValue = ">" + myCompValue = myCommand.ItemValue.replace(">"," ") + myCompValue = myCompValue.replace(".",",") + myCompValue = float(myCompValue) + myCommand.ItemValue = actValue + if newValue <= myCompValue: + return + else: + actValue = str(item()) + + if ("volume" in myCommand.Action.lower()): + httpStatus, myPlayerInfo = self.receive_info_by_request(myCommand.EndPoint,"LoadPlayerInfo","") + # Store Player-Infos to Device + if httpStatus == 200: + try: + myActEcho = self.Echos.get(myCommand.EndPoint) + myActEcho.playerinfo = myPlayerInfo['playerInfo'] + actVolume = self.search(myPlayerInfo, "volume") + actVolume = self.search(actVolume, "volume") + except: + actVolume = 50 + else: + try: + actVolume = int(item()) + except: + actVolume = 50 + + if ("volumeadj" in myCommand.Action.lower()): + myDelta = int(myCommand.Value) + if actVolume+myDelta < 0: + newValue2Set = 0 + elif actVolume+myDelta > 100: + newValue2Set = 100 + else: + newValue2Set =actVolume+myDelta + + # neuen Wert speichern in item + if ("volume" in myCommand.Action.lower()): + item._value = newValue2Set + + + if (actValue == str(myCommand.ItemValue) and myCommand): + myCommand.ItemValue = myItemBuffer + self.send_cmd(myCommand.EndPoint,myCommand.Action,newValue2Set) + + + + # find Value for Key in Json-structure + + def search(self,p, strsearch): + if type(p) is dict: + if strsearch in p: + tokenvalue = p[strsearch] + if not tokenvalue is None: + return tokenvalue + else: + for i in p: + tokenvalue = self.search(p[i], strsearch) + if not tokenvalue is None: + return tokenvalue + + # handle Protocoll Entries + def _insert_protocoll_entry(self, entry): + if len(self.rotating_log) > 400: + del self.rotating_log[400:] + self.rotating_log.insert (0,entry) + + # Check if update of login is needed + def check_refresh_login(self): + my_file= self.update_file + try: + with open (my_file, 'r') as fp: + for line in fp: + last_update_time = float(line) + fp.close() + except: + last_update_time = 0 + + mytime = time.time() + if (last_update_time + self.LoginUpdateCycle < mytime): + self.log_off() + self.auto_login_by_request() + + # set actual values for web-interface + self.last_update_time = datetime.fromtimestamp(mytime).strftime('%Y-%m-%d %H:%M:%S') + self.next_update_time = datetime.fromtimestamp(mytime+self.LoginUpdateCycle).strftime('%Y-%m-%d %H:%M:%S') + self.logger.info('refreshed Login/Cookie: %s' % self.last_update_time) + else: + self.last_update_time = datetime.fromtimestamp(last_update_time).strftime('%Y-%m-%d %H:%M:%S') + self.next_update_time = datetime.fromtimestamp(last_update_time+self.LoginUpdateCycle).strftime('%Y-%m-%d %H:%M:%S') + + + + + def replace_mutated_vowel(self,mValue): + search = ["ä" , "ö" , "ü" , "ß" , "Ä" , "Ö", "Ü", "&" , "é", "á", "ó", "ß"] + replace = ["ae", "oe", "ue", "ss", "Ae", "Oe","Ue", "und", "e", "a", "o", "ss"] + + counter = 0 + myNewValue = mValue + try: + for Replacement in search: + myNewValue = myNewValue.replace(search[counter],replace[counter]) + counter +=1 + except: + pass + + return myNewValue + + + + ############################################## + # Amazon API - Calls + ############################################## + + + def check_login_state(self): + try: + myHeader={ + "DNT":"1", + "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0", + "Connection":"keep-alive" + } + mySession = requests.Session() + mySession.cookies.update(self.cookie) + response= mySession.get('https://'+self.host+'/api/bootstrap?version=0', + headers=myHeader,allow_redirects=True) + + myContent= response.content.decode() + myHeader = response.headers + myDict=json.loads(myContent) + mySession.close() + + self.logger.info('Status of check_login_state: %d' % response.status_code) + + logline = str(self.shtime.now())[0:19] +' Status of check_login_state: %d' % response.status_code + self._insert_protocoll_entry(logline) + + myAuth =myDict['authentication']['authenticated'] + if (myAuth == True): + self.logger.info('Login-State checked - Result: Logged ON' ) + logline = str(self.shtime.now())[0:19] +' Login-State checked - Result: Logged ON' + self._insert_protocoll_entry(logline) + return True + else: + self.logger.info('Login-State checked - Result: Logged OFF' ) + logline = str(self.shtime.now())[0:19] +' Login-State checked - Result: Logged OFF' + self._insert_protocoll_entry(logline) + return False + + + + except Exception as err: + self.logger.error('Login-State checked - Result: Logged OFF - try to login again') + return False + + + def receive_info_by_request(self,dvName,cmdName,mValue): + actEcho = self.Echos.get(dvName) + myUrl='https://'+self.host + myDescriptions = '' + myDict = {} + # replace the placeholders in URL + myUrl=self.parse_url(myUrl, + mValue, + actEcho.serialNumber, + actEcho.family, + actEcho.deviceType, + actEcho.deviceOwnerCustomerId) + + myDescription,myUrl,myDict = self.load_command_let(cmdName,None) + + myHeader = { "Host": "alexa.amazon.de", + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Connection": "keep-alive", + "Content-Type": "application/json; charset=UTF-8", + "Accept-Language": "en-US,en;q=0.5", + "Referer": "https://alexa.amazon.de/spa/index.html", + "Origin":"https://alexa.amazon.de", + "DNT": "1" + } + mySession = requests.Session() + mySession.cookies.update(self.cookie) + response= mySession.get(myUrl,headers=myHeader,allow_redirects=True) + + myResult = response.status_code + myContent= response.content.decode() + myHeader = response.headers + myDict=json.loads(myContent) + mySession.close() + + + return myResult,myDict + + + + def get_last_alexa(self): + myHeader = { "Host": "alexa.amazon.de", + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Connection": "keep-alive", + "Content-Type": "application/json; charset=UTF-8", + "Accept-Language": "en-US,en;q=0.5", + "Referer": "https://alexa.amazon.de/spa/index.html", + "Origin":"https://alexa.amazon.de", + "DNT": "1" + } + mySession = requests.Session() + mySession.cookies.update(self.cookie) + response= mySession.get('https://'+self.host+'/api/activities?startTime=&size=10&offset=0', + headers=myHeader,allow_redirects=True) + + myContent= response.content.decode() + myHeader = response.headers + myDict=json.loads(myContent) + mySession.close() + myDevice = myDict["activities"][0]["sourceDeviceIds"][0]["serialNumber"] + myLastDevice = self.Echos.get_Device_by_Serial(myDevice) + return myLastDevice + + def send_cmd(self,dvName, cmdName,mValue,path=None): + # Parse the value field for dynamic content + if (str(mValue).find("#") >= 0 and str(mValue).find("/#") >0): + FirstPos = str(mValue).find("#") + LastPos = str(mValue).find("/#",FirstPos) + myItemName = str(mValue)[FirstPos+1:LastPos] + myItem=self.items.return_item(myItemName) + + if myItem._type == "num": + myValue = str(myItem()) + myValue = myValue.replace(".", ",") + elif myitem._type == "bool": + myValue = str(myItem()) + else: + myValue = str(myItem()) + mValue = mValue[0:FirstPos]+myValue+mValue[LastPos:LastPos-2]+mValue[LastPos+2:len(mValue)] + + mValue = self.replace_mutated_vowel(mValue) + + + buffer = BytesIO() + actEcho = None + try: + actEcho = self.Echos.get(dvName) + except: + self.logger.warning('found no Echo with Name : {}'.format(dvname)) + self._insert_protocoll_entry('found no Echo with Name : {}'.format(dvname)) + return + if actEcho == None: + self.logger.warning('found no Echo with Name : {}'.format(dvname)) + self._insert_protocoll_entry('found no Echo with Name : {}'.format(dvname)) + return + + myUrl='https://'+self.host + + myDescriptions = '' + myDict = {} + + + myDescription,myUrl,myDict = self.load_command_let(cmdName,path) + # complete the URL + myUrl='https://'+self.host+myUrl + + # replace the placeholders in URL + myUrl=self.parse_url(myUrl, + mValue, + actEcho.serialNumber, + actEcho.family, + actEcho.deviceType, + actEcho.deviceOwnerCustomerId) + + # replace the placeholders in Payload + myHeaders=self.create_request_header() + + + postfields = self.parse_json(myDict, + mValue, + actEcho.serialNumber, + actEcho.family, + actEcho.deviceType, + actEcho.deviceOwnerCustomerId) + + + myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(myUrl,myHeaders,self.cookie,postfields) + + + myResult = myStatus + + + self.logger.info('Status of send_cmd: %d' % myResult) + + + return myResult + + def get_devices_by_request(self): + try: + myHeader={ + "DNT":"1", + "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0", + "Connection":"keep-alive" + } + mySession = requests.Session() + mySession.cookies.update(self.cookie) + response= mySession.get('https://alexa.amazon.de/api/devices-v2/device?cached=false', + headers=myHeader,allow_redirects=True) + + myContent= response.content.decode() + myHeader = response.headers + myDict=json.loads(myContent) + mySession.close() + myDevices = EchoDevices() + + self.logger.info('Status of get_devices_by_request: %d' % response.status_code) + + + + except Exception as err: + self.logger.error('Error while getting Devices: %s' %err) + return None + + for device in myDict['devices']: + deviceFamily=device['deviceFamily'] + #if deviceFamily == 'WHA' or deviceFamily == 'VOX' or deviceFamily == 'FIRE_TV' or deviceFamily == 'TABLET': + # continue + try: + actName = device['accountName'] + myDevices.put(Echo(actName)) + + actDevice = myDevices.get(actName) + actDevice.serialNumber=device['serialNumber'] + actDevice.deviceType=device['deviceType'] + actDevice.family=device['deviceFamily'] + actDevice.name=device['accountName'] + actDevice.deviceOwnerCustomerId=device['deviceOwnerCustomerId'] + except Exception as err: + self.logger.debug('Error while getting Devices: %s' %err) + myDevices = None + + return myDevices + + + + + def parse_cookie_file(self,cookiefile): + self.cookie = {} + csrf = 'N/A' + try: + with open (cookiefile, 'r') as fp: + for line in fp: + if line.find('amazon.de')<0: + continue + + lineFields = line.strip().split('\t') + if len(lineFields) >= 7: + # add Line to self.cookie + if lineFields[2] == '/': + self.cookie[lineFields[5]]=lineFields[6] + + + if lineFields[5] == 'csrf': + csrf = lineFields[6] + fp.close() + except Exception as err: + self.logger.debug('Cookiefile could not be opened %s' % cookiefile) + + return csrf + + + def parse_url(self,myDummy,mValue,serialNumber,familiy,deviceType,deviceOwnerCustomerId): + + myDummy = myDummy.strip() + myDummy=myDummy.replace(' ','') + # for String + try: + myDummy=myDummy.replace('',mValue) + except Exception as err: + print("no String") + # for Numbers + try: + myDummy=myDummy.replace('""',mValue) + except Exception as err: + print("no Integer") + + # Inject the Device informations + myDummy=myDummy.replace('',serialNumber) + myDummy=myDummy.replace('',familiy) + myDummy=myDummy.replace('',deviceType) + myDummy=myDummy.replace('',deviceOwnerCustomerId) + + return myDummy + + + def parse_json(self,myDict,mValue,serialNumber,familiy,deviceType,deviceOwnerCustomerId): + + myDummy = json.dumps(myDict, sort_keys=True) + + count = 0 + for char in myDummy: + if char == '{': + count = count + 1 + + + if count > 1: + # Find First Pos for inner Object + FirstPos = myDummy.find("{",1) + + # Find last Pos for inner Object + LastPos = 0 + pos1 = 1 + while pos1 > 0: + pos1 = myDummy.find("}",LastPos+1) + if (pos1 >= 0): + correctPos = LastPos + LastPos = pos1 + LastPos = correctPos + + + innerJson = myDummy[FirstPos+1:LastPos] + innerJson = innerJson.replace('"','\\"') + + myDummy = myDummy[0:FirstPos]+'"{'+innerJson+'}"'+myDummy[LastPos+1:myDummy.__len__()] + + + myDummy = myDummy.strip() + myDummy=myDummy.replace(' ','') + # for String + try: + myDummy=myDummy.replace('',mValue) + except Exception as err: + print("no String") + # for Numbers + try: + myDummy=myDummy.replace('""',str(mValue)) + except Exception as err: + print("no Integer") + + # Inject the Device informations + myDummy=myDummy.replace('',serialNumber) + myDummy=myDummy.replace('',familiy) + myDummy=myDummy.replace('',deviceType) + myDummy=myDummy.replace('',deviceOwnerCustomerId) + + return myDummy + + + def create_request_header(self): + myheaders= {"Host": "alexa.amazon.de", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:60.0) Gecko/20100101 Firefox/60.0", + "Accept": "*/*", + "Accept-Encoding": "deflate, gzip", + "DNT": "1", + "Content-Type": "application/x-www-form-urlencoded", + "Accept-Language": "de,nl-BE;q=0.8,en-US;q=0.5,en;q=0.3", + "Referer": "https://alexa.amazon.de/spa/index.html", + "Origin": "https://alexa.amazon.de", + "csrf": self.csrf, + "Cache-Control": "no-cache" + } + return myheaders + + def load_command_let(self,cmdName,path=None): + myDescription = '' + myUrl = '' + myJson = '' + retJson = {} + + if path==None: + path=self.sh.get_basedir()+"/plugins/alexarc4shng/cmd/" + + try: + file=open(path+cmdName+'.cmd','r') + for line in file: + line=line.replace("\r\n","") + line=line.replace("\n","") + myFields=line.split("|") + if (myFields[0]=="apiurl"): + myUrl=myFields[1] + pass + if (myFields[0]=="description"): + myDescription=myFields[1] + pass + if (myFields[0]=="json"): + myJson=myFields[1] + retJson=json.loads(myJson) + pass + file.close() + except: + self.logger.error("Error while loading Commandlet : {}".format(cmdName)) + return myDescription,myUrl,retJson + + + + def load_cmd_list(self): + retValue=[] + + files = os.listdir(self.sh.get_basedir()+'/plugins/alexarc4shng/cmd/') + for line in files: + try: + line=line.split(".") + if line[1] == "cmd": + newCmd = {'Name':line[0]} + retValue.append(newCmd) + except: + pass + + return json.dumps(retValue) + + def check_json(self,payload): + try: + myDump = json.loads(payload) + return 'Json OK' + except Exception as err: + return 'Json - Not OK - '+ err.args[0] + + def delete_cmd_let(self,name): + result = "" + try: + os.remove(self.sh.get_basedir()+"/plugins/alexarc4shng/cmd/"+name+'.cmd') + result = "Status:OK\n" + result += "value1:File deleted\n" + except Exception as err: + result = "Status:failure\n" + result += "value1:Error - "+err.args[1]+"\n" + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + def test_cmd_let(self,selectedDevice,txtValue,txtDescription,txt_payload,txtApiUrl): + result = "" + if (txtApiUrl[0:1] != "/"): + txtApiUrl = "/"+txtApiUrl + + JsonResult = self.check_json(txt_payload) + if (JsonResult != 'Json OK'): + result = "Status:failure\n" + result += "value1:"+JsonResult+"\n" + else: + try: + self.save_cmd_let("test", txtDescription, txt_payload, txtApiUrl, "/tmp/") + retVal = self.send_cmd(selectedDevice,"test",txtValue,"/tmp/") + result = "Status:OK\n" + result += "value1: HTTP "+str(retVal)+"\n" + except Exception as err: + result = "Status:failure\n" + result += "value1:"+err.args[0]+"\n" + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + def load_cmd_2_webIf(self,txtCmdName): + try: + myDescription,myUrl,myDict = self.load_command_let(txtCmdName,None) + result = "Status|OK\n" + result += "Description|"+myDescription+"\n" + result += "myUrl|"+myUrl+"\n" + result += "payload|"+str(myDict)+"\n" + except Exception as err: + result = "Status|failure\n" + result += "value1|"+err.args[0]+"\n" + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split("|") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + + def save_cmd_let(self,name,description,payload,ApiURL,path=None): + if path==None: + path=self.sh.get_basedir()+"/plugins/alexarc4shng/cmd/" + + result = "" + mydummy = ApiURL[0:1] + if (ApiURL[0:1] != "/"): + ApiURL = "/"+ApiURL + + JsonResult = self.check_json(payload) + if (JsonResult != 'Json OK'): + result = "Status:failure\n" + result += "value1:"+JsonResult+"\n" + + else: + try: + myDict = json.loads(payload) + myDump = json.dumps(myDict) + description=description.replace("\r"," ") + description=description.replace("\n"," ") + file=open(path+name+".cmd","w") + file.write("apiurl|"+ApiURL+"\r\n") + file.write("description|"+description+"\r\n") + file.write("json|"+myDump+"\r\n") + file.close + + result = "Status:OK\n" + result += "value1:"+JsonResult + "\n" + result += "value2:Saved Commandlet\n" + except Exception as err: + print (err) + + ################## + # prepare Response + ################## + myResult = result.splitlines() + myResponse=[] + newEntry=dict() + for line in myResult: + myFields=line.split(":") + newEntry[myFields[0]] = myFields[1] + + myResponse.append(newEntry) + ################## + return json.dumps(myResponse,sort_keys=True) + + def send_get_request(self,url="", myHeader="",Cookie=""): + mySession = requests.Session() + mySession.cookies.update(Cookie) + response=mySession.get(url, + headers=myHeader, + allow_redirects=True) + return response.status_code, response.headers, response.cookies, response.content.decode(),response.url + + def send_post_request(self,url="", myHeader="",Cookie="",postdata=""): + mySession = requests.Session() + mySession.cookies.update(Cookie) + response=mySession.post(url, + headers=myHeader, + data=postdata, + allow_redirects=True) + mySession.close() + return response.status_code, response.headers, mySession.cookies, response.content.decode() + + def parse_response_cookie_2_txt(self, cookie, CollectingTxtCookie): + for c in cookie: + if c.domain != '': + CollectingTxtCookie += c.domain+"\t"+str(c.domain_specified)+"\t"+ c.path+"\t"+ str(c.secure)+"\t"+ str(c.expires)+"\t"+ c.name+"\t"+ c.value+"\r\n" + return CollectingTxtCookie + + def parse_response_cookie(self, cookie, CollectingCookie): + for c in cookie: + CollectingCookie[c.name] = c.value + return CollectingCookie + + def collect_postdata(self,content): + content = str(content.replace('hidden', '\r\nhidden')) + postdata = {} + myFile = content.splitlines() + for myLine in myFile: + if 'hidden' in myLine: + data = re.findall(r'hidden.*name="([^"]+).*value="([^"]+).*/',myLine) + if len(data) >0: + postdata[data[0][0]]= data[0][1] + + + postdata['showPasswordChecked'] = 'false' + return postdata + + + def auto_login_by_request(self): + if self.credentials == '': + return False + if self.credentials != '': + dummy = self.credentials.split(":") + user = dummy[0] + pwd = dummy[1] + myResults= [] + myCollectionTxtCookie = "" + myCollectionCookie = {} + #################################################### + # Start Step 1 - get Page without Post-Fields + #################################################### + myHeaders={ + "Accept-Language":"de,en-US;q=0.7,en;q=0.3", + "DNT" : "1", + "Upgrade-Insecure-Requests" : "1", + "Connection":"keep-alive", + "Content-Type" : "text/plain;charset=UTF-8", + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Connection" : "keep-alive", + "Accept-Encoding" : "gzip, deflate, br" + } + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request('https://'+self.host,myHeaders) + myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) + myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) + PostData = self.collect_postdata(myContent) + + actSessionID = myRespCookie['session-id'] + + self.logger.info('Status of Auto-Login First Step: %d' % myStatus) + myResults.append('HTTP : ' + str(myStatus)+'- Step 1 - get Session-ID') + #################################################### + # Start Step 2 - login with form + #################################################### + myHeaders={ + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Accept-Language":"de,en-US;q=0.7,en;q=0.3", + "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "DNT" : "1", + "Upgrade-Insecure-Requests":"1", + "Connection":"keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "Accept-Encoding" : "gzip, deflate, br", + "Referer": myLocation + } + newUrl = "https://www.amazon.de"+"/ap/signin/"+actSessionID + postfields = urllib3.request.urlencode(PostData) + + myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) + myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) + myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) + PostData = self.collect_postdata(myContent) + + #actSessionID = myRespCookie['session-id'] + + self.logger.info('Status of Auto-Login Second Step: %d' % myStatus) + myResults.append('HTTP : ' + str(myStatus)+'- Step 2 - login blank to get referer') + + #################################################### + # Start Step 3 - login with form + #################################################### + myHeaders ={ + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Accept-Language" :"de,en-US;q=0.7,en;q=0.3", + "Accept" : "*/*", + "DNT" : "1", + "Accept-Encoding" : "gzip, deflate, br", + "Connection":"keep-alive", + "Upgrade-Insecure-Requests":"1", + "Content-Type": "application/x-www-form-urlencoded", + "Host":"www.amazon.de", + "Referer":"https://www.amazon.de/ap/signin/" + actSessionID + } + + newUrl = "https://www.amazon.de/ap/signin" + + PostData['email'] =user + PostData['password'] = pwd + + postfields = urllib3.request.urlencode(PostData) + myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) + myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) + myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) + PostData = self.collect_postdata(myContent) + + self.logger.info('Status of Auto-Login third Step: %d' % myStatus) + myResults.append('HTTP : ' + str(myStatus)+'- Step 3 - login with credentials') + file=open("/tmp/alexa_step2.html","w") + file.write(myContent) + file.close + + ################################################################# + ## done - third Step - logged in now go an get the goal (csrf) + ################################################################# + + myHeaders ={ + "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0", + "Accept-Language" : "de,en-US;q=0.7,en;q=0.3", + "DNT" : "1", + "Connection" : "keep-alive", + "Accept-Encoding" : "gzip, deflate", + "Referer" : "https://"+self.host+ "/spa/index.html", + "Origin":"https://"+self.host + } + Url = 'https://'+self.host+'/templates/oobe/d-device-pick.handlebars' + #Url = 'https://'+self.host+'/api/language' + + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request(Url,myHeaders,myCollectionCookie) + myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) + myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) + + myResults.append('HTTP : ' + str(myStatus)+'- Step 4 - get csrf') + self.logger.info('Status of Auto-Login fourth Step: %d' % myStatus) + + #################################################### + # check the csrf + #################################################### + myCsrf = self.search(myCollectionCookie, "csrf") + if myCsrf != None: + myResults.append('check CSRF- Step 5 - got good csrf') + self.logger.info('Status of Auto-Login fifth Step - got CSRF: %s' % myCsrf) + self.csrf = myCsrf + else: + myResults.append('check CSRF- Step 5 - got no CSRF') + self.logger.info('Status of Auto-Login fifth Step - got no CSRF') + + #################################################### + # store the new Cookie-File + #################################################### + try: + with open (self.cookiefile, 'w') as myFile: + + + myFile.write("# AlexaRc4shNG HTTP Cookie File"+"\r\n") + myFile.write("# https://www.smarthomeng.de/user/"+"\r\n") + myFile.write("# This file was generated by alexarc4shng@smarthomeNG! Edit at your own risk."+"\r\n") + myFile.write("\r\n") + for line in myCollectionTxtCookie.splitlines(): + myFile.write(line+"\r\n") + myFile.close() + + myResults.append('cookieFile- Step 6 - creation done') + self.cookie = myCollectionCookie + self.login_state= self.check_login_state() + mytime = time.time() + file=open(self.update_file,"w") + file.write(str(mytime)+"\r\n") + file.close() + + myResults.append('login state : %s' % self.login_state) + except: + myResults.append('cookieFile- Step 6 - error while writing new cookie-File') + + for entry in myResults: + logline = str(self.shtime.now())[0:19] + ' ' + entry + self._insert_protocoll_entry(logline) + return myResults + + + + + + def log_off(self): + myUrl='https://'+self.host+"/logout" + myHeaders={"DNT" :"1", + "Connection":"keep-alive", + "User-Agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0" + } + + myStatus,myRespHeader, myRespCookie, myContent,myLocation = self.send_get_request(myUrl,myHeaders, self.cookie) + + self.logger.info('Status of log_off: {}'.format(myStatus)) + + if myStatus == 200: + logline = str(self.shtime.now())[0:19] +' successfully logged off' + self._insert_protocoll_entry(logline) + return "HTTP - " + str(myStatus)+" successfully logged off" + else: + logline = str(self.shtime.now())[0:19] +' Error while logging off' + return "HTTP - " + str(myStatus)+" Error while logging off" + + + + ############################################## + # Web-Interface + ############################################## + + def init_webinterface(self): + """" + Initialize the web interface for this plugin + + This method is only needed if the plugin is implementing a web interface + """ + try: + self.mod_http = Modules.get_instance().get_module( + 'http') # try/except to handle running in a core version that does not support modules + except: + self.mod_http = None + if self.mod_http == None: + self.logger.error("Plugin '{}': Not initializing the web interface".format(self.get_shortname())) + return False + + # set application configuration for cherrypy + webif_dir = self.path_join(self.get_plugin_dir(), 'webif') + config = { + '/': { + 'tools.staticdir.root': webif_dir, + }, + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static' + } + } + + # Register the web interface as a cherrypy app + self.mod_http.register_webif(WebInterface(webif_dir, self), + self.get_shortname(), + config, + self.get_classname(), self.get_instance_name(), + description='') + + return True + + + + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +from jinja2 import Environment, FileSystemLoader + +class WebInterface(SmartPluginWebIf): + + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = logging.getLogger(__name__) + self.webif_dir = webif_dir + self.plugin = plugin + self.tplenv = self.init_template_environment() + + + def render_template(self, tmpl_name, **kwargs): + """ + + Render a template and add vars needed gobally (for navigation, etc.) + + :param tmpl_name: Name of the template file to be rendered + :param **kwargs: keyworded arguments to use while rendering + + :return: contents of the template after beeing rendered + + """ + tmpl = self.tplenv.get_template(tmpl_name) + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), p=self.plugin, + **kwargs) + + def set_cookie_pic(self,CookieOK=False): + dstFile = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/plugin_logo.png' + srcGood = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/alexa_cookie_good.png' + srcBad = self.plugin.sh.get_basedir()+'/plugins/alexarc4shng/webif/static/img/alexa_cookie_bad.png' + if os.path.isfile(dstFile): + os.remove(dstFile) + if CookieOK==True: + if os.path.isfile(srcGood): + os.popen('cp '+srcGood + ' ' + dstFile) + else: + if os.path.isfile(srcBad): + os.popen('cp '+srcBad + ' ' + dstFile) + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + + if (self.plugin.login_state != 'N/A'): + self.set_cookie_pic(True) + else: + self.set_cookie_pic(False) + + log_file = '' + for line in self.plugin.rotating_log: + log_file += str(line)+'\n' + + myDevices = self.get_device_list() + alexa_device_count = len(myDevices) + + login_info = self.plugin.last_update_time + '('+ self.plugin.next_update_time + ')' + return self.render_template('index.html',device_list=myDevices,csrf_cookie=self.plugin.csrf,alexa_device_count=alexa_device_count,time_auto_login=login_info, log_file=log_file) + + + @cherrypy.expose + def log_off_html(self,txt_Result=None): + txt_Result=self.plugin.log_off() + return json.dumps(txt_Result) + + @cherrypy.expose + def log_in_html(self,txt_Result=None): + txt_Result=self.plugin.auto_login_by_request() + return json.dumps(txt_Result) + + + @cherrypy.expose + def handle_buttons_html(self,txtValue=None, selectedDevice=None,txtButton=None,txt_payload=None,txtCmdName=None,txtApiUrl=None,txtDescription=None): + if txtButton=="BtnSave": + result = self.plugin.save_cmd_let(txtCmdName,txtDescription,txt_payload,txtApiUrl) + elif txtButton =="BtnCheck": + pass + elif txtButton =="BtnLoad": + result = self.plugin.load_cmd_2_webIf(txtCmdName) + pass + elif txtButton =="BtnTest": + result = self.plugin.test_cmd_let(selectedDevice,txtValue,txtDescription,txt_payload,txtApiUrl) + elif txtButton =="BtnDelete": + result = self.plugin.delete_cmd_let(txtCmdName) + else: + pass + + #return self.render_template("index.html",txtresult=result) + return result + + + @cherrypy.expose + def build_cmd_list_html(self,reload=None): + myCommands = self.plugin.load_cmd_list() + return myCommands + + + def get_device_list(self): + if (self.plugin.login_state == True): + self.plugin.Echos = self.plugin.get_devices_by_request() + + Device_items = [] + try: + myDevices = self.plugin.Echos.devices + for actDevice in myDevices: + newEntry=dict() + Echo2Add=self.plugin.Echos.devices.get(actDevice) + newEntry['name'] = Echo2Add.id + newEntry['serialNumber'] = Echo2Add.serialNumber + newEntry['family'] = Echo2Add.family + newEntry['deviceType'] = Echo2Add.deviceType + newEntry['deviceOwnerCustomerId'] = Echo2Add.deviceOwnerCustomerId + Device_items.append(newEntry) + + except Exception as err: + self.logger.debug('No devices found',err) + + return Device_items + + @cherrypy.expose + def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config=None): + txt_Result = [] + myCredentials = user+':'+pwd + byte_credentials = base64.b64encode(myCredentials.encode('utf-8')) + encoded = byte_credentials.decode("utf-8") + txt_Result.append("encoded:"+encoded) + txt_Result.append("Encoding done") + conf_file=self.plugin.sh.get_basedir()+'/etc/plugin.yaml' + if (store_2_config == 'true'): + new_conf = "" + with open (conf_file, 'r') as myFile: + for line in myFile: + if line.find('alexa_credentials') > 0: + line = ' alexa_credentials: '+encoded+ "\r\n" + new_conf += line + myFile.close() + txt_Result.append("replaced credentials in temporary file") + with open (conf_file, 'w') as myFile: + for line in new_conf.splitlines(): + myFile.write(line+'\r\n') + myFile.close() + txt_Result.append("stored new config to filesystem") + return json.dumps(txt_Result) + + @cherrypy.expose + def storecookie_html(self, save=None, cookie_txt=None, txt_Result=None, txtUser=None, txtPwd=None, txtEncoded=None, store_2_config=None): + myLines = cookie_txt.splitlines() + # + # Problem - different Handling of Cookies by Browser + + file=open("/tmp/cookie.txt","w") + for line in myLines: + file.write(line+"\r\n") + file.close() + value1 = self.plugin.parse_cookie_file("/tmp/cookie.txt") + self.plugin.login_state = self.plugin.check_login_state() + + if (self.plugin.login_state == True): + self.set_cookie_pic(True) + else: + self.set_cookie_pic(False) + + + if (self.plugin.login_state == False) : + # Cookies not found give back an error + tmpl = self.tplenv.get_template('index.html') + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), p=self.plugin, + txt_Result=' Cookies are not saved missing csrf', + cookie_txt=cookie_txt, + csrf_cookie=value1) + + # Store the Cookie-file for permanent use + file=open(self.plugin.cookiefile,"w") + for line in myLines: + file.write(line+"\r\n") + file.close() + + self.plugin.csrf = value1 + + + myDevices = self.get_device_list() + alexa_device_count = len(myDevices) + + + tmpl = self.tplenv.get_template('index.html') + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), p=self.plugin, + txt_Result=' Cookies were saved - everything OK', + cookie_txt=cookie_txt, + csrf_cookie=value1, + device_list=myDevices, + alexa_device_count=alexa_device_count) + + + + diff --git a/alexarc4shng/assets/pic1.jpg b/alexarc4shng/assets/pic1.jpg new file mode 100755 index 000000000..ad22e5d2a Binary files /dev/null and b/alexarc4shng/assets/pic1.jpg differ diff --git a/alexarc4shng/assets/pic2.jpg b/alexarc4shng/assets/pic2.jpg new file mode 100755 index 000000000..7ebc36e76 Binary files /dev/null and b/alexarc4shng/assets/pic2.jpg differ diff --git a/alexarc4shng/assets/pic3.jpg b/alexarc4shng/assets/pic3.jpg new file mode 100755 index 000000000..dfc7a365d Binary files /dev/null and b/alexarc4shng/assets/pic3.jpg differ diff --git a/alexarc4shng/assets/webif1.jpg b/alexarc4shng/assets/webif1.jpg new file mode 100755 index 000000000..6d9c14224 Binary files /dev/null and b/alexarc4shng/assets/webif1.jpg differ diff --git a/alexarc4shng/assets/webif2.jpg b/alexarc4shng/assets/webif2.jpg new file mode 100755 index 000000000..f82816d5a Binary files /dev/null and b/alexarc4shng/assets/webif2.jpg differ diff --git a/alexarc4shng/assets/webif3.jpg b/alexarc4shng/assets/webif3.jpg new file mode 100755 index 000000000..5e01d62a1 Binary files /dev/null and b/alexarc4shng/assets/webif3.jpg differ diff --git a/alexarc4shng/cmd/LastAlexa.cmd b/alexarc4shng/cmd/LastAlexa.cmd new file mode 100755 index 000000000..5aee38405 --- /dev/null +++ b/alexarc4shng/cmd/LastAlexa.cmd @@ -0,0 +1,3 @@ +apiurl|/api/activities?startTime=&size=10&offset=0 +description|Get the last Alexa +json|{} diff --git a/alexarc4shng/cmd/LoadPlayerInfo.cmd b/alexarc4shng/cmd/LoadPlayerInfo.cmd new file mode 100755 index 000000000..b18aa5090 --- /dev/null +++ b/alexarc4shng/cmd/LoadPlayerInfo.cmd @@ -0,0 +1,3 @@ +apiurl|/api/np/player?deviceSerialNumber=&deviceType= +description|Load Player informations into dict +json|{} diff --git a/alexarc4shng/cmd/Pause.cmd b/alexarc4shng/cmd/Pause.cmd new file mode 100755 index 000000000..b440d4650 --- /dev/null +++ b/alexarc4shng/cmd/Pause.cmd @@ -0,0 +1,3 @@ +apiurl|/api/np/command?deviceSerialNumber=&deviceType= +description|pauses the device - silence +json|{"type": "PauseCommand"} diff --git a/alexarc4shng/cmd/Play.cmd b/alexarc4shng/cmd/Play.cmd new file mode 100755 index 000000000..08732dc33 --- /dev/null +++ b/alexarc4shng/cmd/Play.cmd @@ -0,0 +1,3 @@ +apiurl|/api/np/command?deviceSerialNumber=&deviceType= +description|Play the actual paused media +json|{"type": "PlayCommand"} diff --git a/alexarc4shng/cmd/SSML.cmd b/alexarc4shng/cmd/SSML.cmd new file mode 100755 index 000000000..483fdbc50 --- /dev/null +++ b/alexarc4shng/cmd/SSML.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|Use SSML to speak-Example: +json|{"behaviorId": "PREVIEW", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": {"operationPayload": {"customerId": "", "content": [{"display": {"title": "smartHomeNG", "body": ""}, "speak": {"type": "ssml", "value": ""}, "locale": "de-DE"}], "expireAfter": "PT5S", "target": {"customerId": "", "devices": [{"deviceSerialNumber": "", "deviceTypeId": ""}]}}, "type": "AlexaAnnouncement", "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"}}, "status": "ENABLED"} diff --git a/alexarc4shng/cmd/StartTuneInStation.cmd b/alexarc4shng/cmd/StartTuneInStation.cmd new file mode 100755 index 000000000..8a87e97d6 --- /dev/null +++ b/alexarc4shng/cmd/StartTuneInStation.cmd @@ -0,0 +1,3 @@ +apiurl|/api/tunein/queue-and-play?deviceSerialNumber=&deviceType=&guideId=&contentType=station&callSign=&mediaOwnerCustomerId= +description|Startet einen TuneIn Radio-Kanel +json|{} diff --git a/alexarc4shng/cmd/Text2Speech.cmd b/alexarc4shng/cmd/Text2Speech.cmd new file mode 100755 index 000000000..c31f92904 --- /dev/null +++ b/alexarc4shng/cmd/Text2Speech.cmd @@ -0,0 +1,3 @@ +apiurl|/api/behaviors/preview +description|Text to speach +json|{"status": "ENABLED", "sequenceJson": {"@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": {"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", "type": "Alexa.Speak", "operationPayload": {"textToSpeak": "", "locale": "de-DE", "customerId": "", "deviceSerialNumber": "", "deviceType": ""}}}, "behaviorId": "PREVIEW"} diff --git a/alexarc4shng/cmd/VolumeAdj.cmd b/alexarc4shng/cmd/VolumeAdj.cmd new file mode 100755 index 000000000..11e105be6 --- /dev/null +++ b/alexarc4shng/cmd/VolumeAdj.cmd @@ -0,0 +1,3 @@ +apiurl|/api/np/command?deviceSerialNumber=&deviceType= +description|The volume will be set +xx / -xx percent from the actual volume +json|{"type": "VolumeLevelCommand", "volumeLevel": ""} diff --git a/alexarc4shng/cmd/VolumeSet.cmd b/alexarc4shng/cmd/VolumeSet.cmd new file mode 100755 index 000000000..5f86caa00 --- /dev/null +++ b/alexarc4shng/cmd/VolumeSet.cmd @@ -0,0 +1,3 @@ +apiurl|/api/np/command?deviceSerialNumber=&deviceType= +description|Sets the volume to a value from 0-100 percent +json|{"type": "VolumeLevelCommand", "volumeLevel": ""} diff --git a/alexarc4shng/locale.yaml b/alexarc4shng/locale.yaml new file mode 100755 index 000000000..712460ac6 --- /dev/null +++ b/alexarc4shng/locale.yaml @@ -0,0 +1,48 @@ +plugin_translations: + # Translations for the plugin specially for the web interface + 'allowed IP': {'de': 'erlaubte IP', 'en': '=', 'fr': ''} + 'last Session': {'de': 'letzte Sitzung', 'en': '=', 'fr': ''} + 'Stream-Modifiers': {'de': 'Stream-Modikatoren', 'en': '=', 'fr': ''} + 'last Session duration': {'de': 'letzte Sitzungs- dauer', 'en': '=', 'fr': ''} + 'Sessions total': {'de': 'Sitzungen gesamt', 'en': '=', 'fr': ''} + 'Settings': {'de': 'Einstellungen', 'en': '=', 'fr': ''} + 'Credentials:': {'de': 'Zugangsdaten:', 'en': '=', 'fr': ''} + 'delete Protocol': {'de': 'Protokoll löschen:', 'en': '=', 'fr': ''} + 'Real-URL': {'de': 'tatsächliche URL', 'en': '=', 'fr': ''} + 'Commit Changes': {'de': 'Änderungen speichern', 'en': '=', 'fr': ''} + 'Store to Config': {'de': 'in Konfiguration speichern', 'en': '=', 'fr': ''} + 'Settings / Cam-Info': {'de': 'Einstellungen / Kamera-Infos', 'en': '=', 'fr': ''} + 'Communication-Log': {'de': 'Kommunikations-Log', 'en': '=', 'fr': ''} + 'active Camera Threads': {'de': 'aktive Kamera-Threads', 'en': '=', 'fr': ''} + 'SSL Certificate Info': {'de': 'SSL Zertifikas Info', 'en': '=', 'fr': ''} + 'Proxy-Credentials': {'de': 'Proxy-Zugangsdaten', 'en': '=', 'fr': ''} + 'Proxy-Authorization': {'de': 'Proxy-Authorisierungs-Typ', 'en': '=', 'fr': ''} + 'Video-Buffer-Size :': {'de': 'Video-Puffer-Grösse', 'en': '=', 'fr': ''} + 'Authorization :': {'de': 'Authorisierungs-Typ', 'en': '=', 'fr': ''} + 'Encode': {'de': 'enkodieren', 'en': '=', 'fr': ''} + 'encoded Cred.:': {'de': 'enkodierte Zugangsdaten', 'en': '=', 'fr': ''} + 'Result :': {'de': 'Ergebnis', 'en': '=', 'fr': ''} + 'Value': {'de': 'Wert', 'en': '=', 'fr': ''} + 'Property': {'de': 'Eigenschaft', 'en': '=', 'fr': ''} + 'Threads existing ...': {'de': 'existierende Threads', 'en': '=', 'fr': ''} + 'Auto Update ( 2 sec.)': {'de': 'Auto Update ( 2 Sek.)', 'en': '=', 'fr': ''} + 'last/next Auto-Login' : {'de': 'letztes/nächstes Auto-Login', 'en': '=', 'fr': ''} + 'selected Device' : {'de': 'gwähltes Gerät', 'en': '=', 'fr': ''} + 'No. of Alexa-Devices': {'de': 'Anzahl Alexa-Geräte', 'en': '=', 'fr': ''} + 'LogOff': {'de': 'Ausloggen', 'en': '=', 'fr': ''} + 'LogIn': {'de': 'Einloggen', 'en': '=', 'fr': ''} + 'Store Cookie': {'de': 'Cookie speichern', 'en': '=', 'fr': ''} + 'Paste the Cookie-File here': {'de': 'Cookie File hier einfügen', 'en': '=', 'fr': ''} + 'existing Commands': {'de': 'existierende Kommandos', 'en': '=', 'fr': ''} + 'Command-Name': {'de': 'Kommando-Name', 'en': '=', 'fr': ''} + + +# '': {'de': 'Proxy-Authorisierungs-Typ', 'en': '=', 'fr': ''} + + + + + + + + diff --git a/alexarc4shng/plugin.yaml b/alexarc4shng/plugin.yaml new file mode 100755 index 000000000..984f597e3 --- /dev/null +++ b/alexarc4shng/plugin.yaml @@ -0,0 +1,97 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: interface # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Plugin zur Steuerung von Amazon Echo Geräten Zugriff via Web-Browser API und Cookie' + en: 'Plugin to remote control Echo Show/Spot/Fire' + maintainer: AndreK + tester: henfri, juergen, psilo + documentation: https://www.smarthomeng.de/user/plugins/alexarc4shng/user_doc.html # url of documentation + version: 1.0.2 # Plugin version + sh_minversion: 1.5.2 # minimum shNG version to use this plugin + multi_instance: False # plugin supports multi instance + classname: AlexaRc4shNG # class containing the plugin + keywords: Alexa Amazon Remote Control + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1336416-alexa-text-to-speach + state: develop # State of the Plugin + restartable: True # Plugin is restartable + +plugin_functions: + # Definition of function interface of the plugin + + send_cmd: + type: str + description: + de: "Sendet einen Befehl an Alexa." + en: "Sends a command to Alexa." + parameters: + dvName: + type: str + description: + de: "Name des Alexa Devices." + en: "Name of Alexa device." + cmdName: + type: str + description: + de: "Name des Befehls, z.b. Text2Speech." + en: "Name of command, e.g. Text2Speech." + mValue: + type: str + description: + de: "Wert, der gesendet werden soll, numerische Werte ohne Hochkomma als Zahl" + en: "Value to send, numeric Values without Quotes as Num" + + get_last_alexa: + type: str + description: + de: "Liefert die Geräte-ID des zuletzt verwendeten Alexa-Gerätes zurück" + en: "delivers the Device-ID of the last used Alexa-Device" + +logic_parameters: NONE # No logic parameters for this plugin +item_structs: NONE # no item structure needed +item_attributes: + # Definition of item attributes defined by this plugin + alexa_cmd_XX: + type: str + mandatory: True + description: + de: 'String um die Befehle zu definieren' + en: 'string to define orders' + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml + cookiefile: + type: str + default: '' + description: + de: 'Cookiefile mit komplettem Pfad' + en: 'Cookiefile with complete path' + host: + type: str + default: 'alexa.amazon.de' + description: + de: 'Amazon-Host z.b. alexa.amazon.de ohne Protokoll (https)' + en: 'Amazon-Host a.e. alexa.amazon.de without protocoll (https)' + + item_2_enable_alexa_rc: + type: str + default: '' + description: + de: 'Ein Item welches verwendet wird um die Freigabe für die Kommunikation zu erteilen (USZU)' + en: 'An Item to give the plugin permission to remote control the echo-devices (USZU)' + + alexa_credentials: + type: str + default: '' + description: + de: 'Zugangsdaten für das Amazon-Alexa-Web-Site :, base64 encodiert' + en: 'credentials for the amazon-alexa-website :, base64 encoded' + + login_update_cycle: + type: num + default: 432000 + description: + de: 'Sekunden bis zum automatischen refreshen des Cookie-files' + en: 'seconds till the next automatic login to get a new cookie' + diff --git a/alexarc4shng/requirements.txt b/alexarc4shng/requirements.txt new file mode 100755 index 000000000..7491ce9ce --- /dev/null +++ b/alexarc4shng/requirements.txt @@ -0,0 +1 @@ +#requests requirement moved to core diff --git a/alexarc4shng/user_doc.rst b/alexarc4shng/user_doc.rst new file mode 100755 index 000000000..84b35f223 --- /dev/null +++ b/alexarc4shng/user_doc.rst @@ -0,0 +1,57 @@ +.. index:: Plugins; Remote Control for Alexa devices +.. index:: alexarc4shng + +AlexaRc4shNG +### + +Konfiguration +============= + +Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/alexarc4shng` beschrieben. + + +Web Interface +============= + +Das AlexaRc4shNG Plugin verfügt über ein Webinterface.Hier werden die Zugangsdaten zur Amazon-Web-Api (Cookie) gepflegt. +Es können neue Kommandos erstellt werden + +.. important:: + + Das Webinterface des Plugins kann mit SmartHomeNG v1.5.2 und davor **nicht** genutzt werden. + Es wird dann nicht geladen. Diese Einschränkung gilt nur für das Webinterface. Ansonsten gilt + für das Plugin die in den Metadaten angegebene minimale SmartHomeNG Version. + + +Aufruf des Webinterfaces +------------------------ + +Das Plugin kann aus dem backend aufgerufen werden. Dazu auf der Seite Plugins in der entsprechenden +Zeile das Icon in der Spalte **Web Interface** anklicken. + +Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/alexarc4shng`` aufgerufen werden. + + +Beispiele +--------- + +Folgende Informationen können im Webinterface angezeigt werden: + +Oben rechts werden allgemeine Parameter zum Plugin angezeigt. + +Im ersten Tab kann das Cookie File gespeichert werden - in die Textarea via Cut & Paste einfügen und speichern: + +.. image:: assets/webif1.jpg + :class: screenshot + +Im zweiten Tab werden die verfügbaren Geräte angezeigt - Durch click auf ein Gerät wird dieses selektiert und steht für Tests zur Verfügung: + +.. image:: assets/webif2.jpg + :class: screenshot + +Im dritten Tab werden die Commandlets verwaltet - mit Click auf die Liste der Commandlets wird dieses ins WebIF geladen: + +.. image:: assets/webif3.jpg + :class: screenshot + + diff --git a/alexarc4shng/webif/static/img/alexa_cookie_bad.jpg b/alexarc4shng/webif/static/img/alexa_cookie_bad.jpg new file mode 100755 index 000000000..e0148bb35 Binary files /dev/null and b/alexarc4shng/webif/static/img/alexa_cookie_bad.jpg differ diff --git a/alexarc4shng/webif/static/img/alexa_cookie_bad.png b/alexarc4shng/webif/static/img/alexa_cookie_bad.png new file mode 100755 index 000000000..59f3408ce Binary files /dev/null and b/alexarc4shng/webif/static/img/alexa_cookie_bad.png differ diff --git a/alexarc4shng/webif/static/img/alexa_cookie_bad2.png b/alexarc4shng/webif/static/img/alexa_cookie_bad2.png new file mode 100755 index 000000000..4fa6e1fe1 Binary files /dev/null and b/alexarc4shng/webif/static/img/alexa_cookie_bad2.png differ diff --git a/alexarc4shng/webif/static/img/alexa_cookie_good.png b/alexarc4shng/webif/static/img/alexa_cookie_good.png new file mode 100755 index 000000000..75a6edfa7 Binary files /dev/null and b/alexarc4shng/webif/static/img/alexa_cookie_good.png differ diff --git a/alexarc4shng/webif/static/img/alexa_cookie_good1.png b/alexarc4shng/webif/static/img/alexa_cookie_good1.png new file mode 100755 index 000000000..449af7b5c Binary files /dev/null and b/alexarc4shng/webif/static/img/alexa_cookie_good1.png differ diff --git a/alexarc4shng/webif/static/img/favicon.ico b/alexarc4shng/webif/static/img/favicon.ico new file mode 100755 index 000000000..8a22cecf1 Binary files /dev/null and b/alexarc4shng/webif/static/img/favicon.ico differ diff --git a/alexarc4shng/webif/static/img/logo_big.png b/alexarc4shng/webif/static/img/logo_big.png new file mode 100755 index 000000000..93865a18e Binary files /dev/null and b/alexarc4shng/webif/static/img/logo_big.png differ diff --git a/alexarc4shng/webif/static/img/plugin_logo.jpg b/alexarc4shng/webif/static/img/plugin_logo.jpg new file mode 100755 index 000000000..c12056236 Binary files /dev/null and b/alexarc4shng/webif/static/img/plugin_logo.jpg differ diff --git a/alexarc4shng/webif/static/img/plugin_logo.png b/alexarc4shng/webif/static/img/plugin_logo.png new file mode 100755 index 000000000..75a6edfa7 Binary files /dev/null and b/alexarc4shng/webif/static/img/plugin_logo.png differ diff --git a/alexarc4shng/webif/static/img/plugin_logo_old.png b/alexarc4shng/webif/static/img/plugin_logo_old.png new file mode 100755 index 000000000..31b76039a Binary files /dev/null and b/alexarc4shng/webif/static/img/plugin_logo_old.png differ diff --git a/alexarc4shng/webif/static/js/handler.js b/alexarc4shng/webif/static/js/handler.js new file mode 100755 index 000000000..1734601ab --- /dev/null +++ b/alexarc4shng/webif/static/js/handler.js @@ -0,0 +1,519 @@ +var selectedDevice; + +//******************************************* +// Button Handler for Encoding credentials +//******************************************* + +function BtnEncode(result) +{ + user = document.getElementById("txtUser").value; + pwd = document.getElementById("txtPwd").value; + store2config = document.getElementById("store_2_config").checked; + encoded=user+":"+pwd; + encoded=btoa(encoded); + //document.getElementById("txtEncoded").value = encoded; + $.ajax({ + url: "store_credentials.html", + type: "GET", + data: { encoded : encoded, + user : user, + pwd : pwd, + store_2_config : store2config + }, + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateEncodeResponse(response); + }, + error: function () { + document.getElementById("txt_Result").innerHTML = "Error while Communication !"; + } + }); + return +} +//******************************************* +// Button Handler LogIn to Amazon-Site +//******************************************* + +function BtnLogIn(result) +{ + $.ajax({ + url: "log_in.html", + type: "GET", + data: {} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateLoginResponse(response); + }, + error: function () { + document.getElementById("txt_Result").innerHTML = "Error while Communication !"; + } + }); + return +} + +//******************************************* +// Button Handler LogOff from Amazon-Site +//******************************************* + +function BtnLogOff(result) +{ + $.ajax({ + url: "log_off.html", + type: "GET", + data: {} , + contentType: "application/json; charset=utf-8", + success: function (response) { + document.getElementById("txt_Result").innerHTML = response; + }, + error: function () { + document.getElementById("txt_Result").innerHTML = "Error while Communication !"; + } + }); + return +} + +//******************************************* +// Button Handler for saving Commandlet +//******************************************* + +function BtnSave(result) +{ + document.getElementById("txtresult").value = ""; + + + if (document.getElementById("txtCmdName").value == "") + { + alert ("No Name given for CommandLet, please enter one"); + return; + } + if (document.getElementById("txtApiUrl").value == "") + { + alert ("No API-URL given for CommandLet, please enter one"); + return; + } + + document.getElementById("txtButton").value ="BtnSave"; + + myPayload = myCodeMirrorConf.getValue(); + StoreCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + myPayload, + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); + +} + +//******************************************* +// Button Handler for checking Json +//******************************************* + +function BtnCheck(result) +{ + + document.getElementById("txtButton").value ="BtnCheck"; + try { + // Block of code to try + myValue = document.getElementById("txtValue").value + myPayload = myCodeMirrorConf.getValue(); + myPayload = myPayload.replace("",myValue); + var myTest = JSON.stringify(JSON.parse(myPayload),null,2) + myCodeMirrorConf.setValue(myTest); + myCodeMirrorConf.focus; + myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + document.getElementById("txtresult").value = "JSON-Structure is OK"; + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + + } + catch(err) { + // Block of code to handle errors + document.getElementById("txtresult").value = "JSON-Structure is not OK\n"+err; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } +} + +//******************************************* +// Button Handler for testing +//******************************************* + +function BtnTest(result) +{ + selectedDevice = document.getElementById("selectedDevice").value; + + if (selectedDevice == "no Device selected") + { + alert ("No Device selected for Test, first select one"); + return; + } + txtValue = document.getElementById("txtValue").value; + if (txtValue == "") + { + alert ("No Value set to send, please enter value"); + return; + } + + document.getElementById("txtButton").value ="BtnTest"; + myPayload = myCodeMirrorConf.getValue(); + + TestCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + myPayload, + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); +} + +//******************************************* +// Button Handler for deleting +//******************************************* + +function BtnDelete(result) +{ + buildCmdSequence(); + return + var filetodelete = document.getElementById("txtCmdName").value; + if (filetodelete == "") { + alert ("No Command selected to delete, first select one"); + return; + } + filetodelete=filetodelete+".cmd"; + var r = confirm("Your really want to delete\n\n"+ filetodelete + "\n\nContinue ?"); + if (r == false) { + return; + } + document.getElementById("txtButton").value ="BtnDelete"; + DeleteCMD + ( + document.getElementById("txtValue").value, + document.getElementById("selectedDevice").value, + "", + document.getElementById("txtCmdName").value, + document.getElementById("txtApiUrl").value, + document.getElementById("txtDescription").value + ); +} + + +//************************************************************* +// ValidateLoginResponse -checks the login-button +//************************************************************* + +function ValidateLoginResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse) + { + temp = temp + objResponse[x]+"\n"; + } + +document.getElementById("txt_Result").innerHTML = temp; +} + +//************************************************************* +// ValidateEncodeResponse -checks the login-button +//************************************************************* + +function ValidateEncodeResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse) + { + if (x == "0") + { + document.getElementById("txtEncoded").value = objResponse[x].substr(8); + } + else + { + temp = temp + objResponse[x]+"\n"; + } + } + +document.getElementById("txt_Result").value = temp; +} + + + +//************************************************************* +// ValidateResponse - checks the response for button-Actions +//************************************************************* + +function ValidateResponse(response) +{ +var myResult = "" +var temp = "" +var objResponse = JSON.parse(response) +for (x in objResponse[0]) + { + if (x == "Status") { + myResult = objResponse[0][x]; + } + else { + temp = temp + objResponse[0][x]+"\n"; + } + } + +document.getElementById("txtresult").value = temp; +if (myResult == "OK") +{ + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + reloadCmds(); +} +else +{ + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; +} + +} + +//******************************************* +// Function to Test Command-Let +//******************************************* + +function TestCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : txtValue, + selectedDevice:selectedDevice, + txtButton : "BtnTest", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Function to Save Command-Let +//******************************************* + +function StoreCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:selectedDevice, + txtButton : "BtnSave", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Function to Delete Command-Let +//******************************************* + +function DeleteCMD(txtValue,selectedDevice,txt_payload,txtCmdName,txtApiUrl,txtDescription) { + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:selectedDevice, + txtButton : "BtnDelete", + txt_payload : txt_payload, + txtCmdName : txtCmdName, + txtApiUrl : txtApiUrl, + txtDescription : txtDescription} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ValidateResponse(response) + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + + +//************************************************ +// OnClick-function for Command-List +//************************************************ + +function SelectCmd() +{ + +//$("#AlexaDevices").on("click", "tr",function() + + var value = $(this).closest("tr").find("td").first().text(); + + if (value != "") { + alert(value); + } + +} + +//************************************************ +// builds and show table with saves Commandlets +//************************************************ + + +function build_cmd_list(result) +{ + + var temp =""; + temp = "
"; + temp = temp + ""; + temp = temp + ""; + + $.each(result, function(index, element) { + temp = temp + ""; + + }) + temp = temp + "
Command-Name
"+ element.Name + "
"; + $('#Cmds').html(temp); + + $('#tableCommands').on("click", "tr",function() + { + var value = $(this).closest("tr").find("td").first().text(); + if (value != "") { + LoadCommand(value); + } + }); + + + +} + + +//******************************************* +// reloads the list with the Command-Lets +//******************************************* + +function reloadCmds() +{ + $("#refresh-element").addClass("fa-spin"); + $("#reload-element").addClass("fa-spin"); + $("#cardOverlay").show(); + $.getJSON("build_cmd_list_html", function(result) + { + build_cmd_list(result); + window.setTimeout(function() + { + $("#refresh-element").removeClass("fa-spin"); + $("#reload-element").removeClass("fa-spin"); + $("#cardOverlay").hide(); + }, 300); + + }); + +} + +//******************************************* +// Load Commandlet to Web-Site +//******************************************* +function LoadCommand(txtCmdName) +{ + $.ajax({ + url: "handle_buttons.html", + type: "GET", + data: { txtValue : "", + selectedDevice:"", + txtButton : "BtnLoad", + txt_payload : "", + txtCmdName : txtCmdName, + txtApiUrl : "", + txtDescription : ""} , + contentType: "application/json; charset=utf-8", + success: function (response) { + ShowCommand(response,txtCmdName); + }, + error: function () { + document.getElementById("txtresult").value = "Error while Communication !"; + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } + }); + return +} + +//******************************************* +// Load 2 Fields +//******************************************* +function ShowCommand(response,txtCmdName) +{ + var myResult = "" + var temp = "" + var objResponse = JSON.parse(response) + document.getElementById("txtCmdName").value = txtCmdName; + for (x in objResponse[0]) + { + if (x == "Status") + { + myResult = objResponse[0][x]; + } + else if (x == "Description") + { + console.log(objResponse[0][x]) + var test = objResponse[0][x]; + + document.getElementById("txtDescription").value = objResponse[0][x]; + } + else if (x == "myUrl") + { + console.log(objResponse[0][x]) + document.getElementById("txtApiUrl").value = objResponse[0][x]; + } + else if (x == "payload") + { + myjson = objResponse[0][x].split("'").join("\""); + myjson = myjson.split("\\").join(""); + var myTest = JSON.stringify(JSON.parse(myjson),null,2) + myCodeMirrorConf.setValue(myTest); + myCodeMirrorConf.focus; + myCodeMirrorConf.setCursor(myCodeMirrorConf.lineCount(),0); + } + } + document.getElementById("txtresult").value = myResult; + if (myResult == "OK") + { + document.getElementById("resultOK").style.visibility="visible"; + document.getElementById("resultNOK").style.visibility="hidden"; + reloadCmds(); + } + else + { + document.getElementById("resultOK").style.visibility="hidden"; + document.getElementById("resultNOK").style.visibility="visible"; + } +} + + diff --git a/alexarc4shng/webif/templates/index.html b/alexarc4shng/webif/templates/index.html new file mode 100755 index 000000000..71e8201bc --- /dev/null +++ b/alexarc4shng/webif/templates/index.html @@ -0,0 +1,396 @@ + + + + +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} +{% set use_bodytabs = true %} +{% set tabcount = 4 %} +{% set tab1title = 'Cookie Handling' %} +{% set tab2title = 'Alexa Devices' %} +{% set tab3title = 'Command Handling' %} +{% set tab4title = 'Communication-Protocol' %} + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + +
csrf Cookie{{ csrf_cookie }}
{{ _('No. of Alexa-Devices') }}{{ alexa_device_count }}
{{ _('last/next Auto-Login') }}{{ time_auto_login }}
{{ _('selected Device') }}no Device selected
+{% endblock headtable %} + +{% block buttons %} +{% endblock buttons %} + + +{% block bodytab1 %} +
+ + + + + + + + + + + + + + +
{{ _('Credentials:') }} + + + + +
+ + +
+
+ + + {{ _('encoded Cred.:') }} + + +
+ +
+ +
+
+ + + + + + + + + +
+
+
+ + + + Result : + + + +
+ +
+
+
+
{{ _('Paste the Cookie-File here') }}
+ + + + +
+
+
+
+
+ +{% endblock bodytab1 %} + +{% block bodytab2 %} +
+
+ + + + + + + + + + + + {% for item in device_list %} + + + + + + + + {% endfor %} + +
{{ _('Alexa-Device-Name') }}{{ _('deviceFamily') }}{{ 'deviceSerial' }}deviceTypedeviceOwnerCustomerId
{{ item.name }}{{ item.family }}{{ item.serialNumber }}{{ item.deviceType }}{{ item.deviceOwnerCustomerId }}
+
+
+ + + + + + +{% endblock bodytab2 %} + +{% block bodytab3 %} + + + + + + + + + + + + +
+ +
+ +
+
+
+
+
+
+ + {{ _('existing Commands') }} +
+
+
+
+
+ +
+
+
+ {{ cmd_list }} +
+
+
+
+
+ + +
+ + +
+ + + +
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ +
{{ _('(Payload for Api-Call in json-Format - if no Payload enter {})') }}
+ +
+ +
+
+
+
+
+
+
+ + +
+ +
+
+ + + +{% endblock bodytab3 %} + + + +{% block bodytab4 %} + +
+
+ {% if log_file %}{% else %}{{ _('no data available') }}{% endif %} +
+
+ + + +{% endblock bodytab4 %} + + + + diff --git a/avdevice/plugin.yaml b/avdevice/plugin.yaml index cb284dc2c..8d8e6a4ed 100755 --- a/avdevice/plugin.yaml +++ b/avdevice/plugin.yaml @@ -25,7 +25,7 @@ plugin: en: 'pyserial python module' maintainer: onkelandy tester: Foxi352 # Who tests this plugin? - state: develop + state: ready keywords: av denon pioneer epson oppo player amp receiver projector rs232 telnet tcpip remote control # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page diff --git a/buderus/plugin.yaml b/buderus/plugin.yaml index 1c5fae00c..11bad20f4 100755 --- a/buderus/plugin.yaml +++ b/buderus/plugin.yaml @@ -8,7 +8,7 @@ plugin: en: 'Control Buderus heating through a Logamatic web KM200 module (still under development)' maintainer: rthill # tester: # Who tests this plugin? - state: develop + state: ready # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py diff --git a/co2meter/__init__.py b/co2meter/__init__.py index 173f3ed42..e89eb4304 100644 --- a/co2meter/__init__.py +++ b/co2meter/__init__.py @@ -35,7 +35,7 @@ class CO2Meter(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.3.1" + PLUGIN_VERSION = "1.3.2" CO2METER_CO2 = 0x50 CO2METER_TEMP = 0x42 @@ -48,7 +48,7 @@ class CO2Meter(SmartPlugin): _file = "" _running = True - def __init__(self, smarthome, device="/dev/hidraw0", time_sleep=5): + def __init__(self, sh, *args, **kwargs): """ Initalizes the plugin. The parameters described for this method are pulled from the entry in plugin.conf. @@ -56,13 +56,14 @@ def __init__(self, smarthome, device="/dev/hidraw0", time_sleep=5): :param device: Path where the raw usb data is retreived from (default: /dev/hidraw0) :param time_sleep: The time in seconds to sleep after a multicast was received """ - self._sh = smarthome - self.logger = logging.getLogger(__name__) + # Call init code of parent class (SmartPlugin or MqttPlugin) + super().__init__() + self._items = {} - self._time_sleep = int(time_sleep) + self._time_sleep = self.get_parameter_value('time_sleep') - self._device = device - self._file = open(device, "a+b", 0) + self._device = self.get_parameter_value('device') + self._file = open(self.get_parameter_value('device'), "a+b", 0) set_report = [0] + self._key fcntl.ioctl(self._file, self.HIDIOCSFEATURE_9, bytearray(set_report)) diff --git a/co2meter/plugin.yaml b/co2meter/plugin.yaml index d775dfc44..339681855 100644 --- a/co2meter/plugin.yaml +++ b/co2meter/plugin.yaml @@ -12,8 +12,8 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1165010-supportthread-f%C3%BCr-das-co2meter-plugin - version: 1.3.1 # Plugin version - sh_minversion: 1.3 # minimum shNG version to use this plugin + version: 1.3.2 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance restartable: unknown @@ -23,16 +23,16 @@ parameters: # Definition of parameters to be configured in etc/plugin.yaml device: type: str - mandatory: False + default: '/dev/hidraw0' description: de: 'Pfad zu den rohen USB Daten (optional, default: /dev/hidraw0)' en: 'Path to raw usb data (optional, default: /dev/hidraw0)' time_sleep: - type: str - mandatory: False + type: int + default: 5 description: - de: 'Wartezeit in Sekundento nach jeder Datenanfrage (optional, default: 5)' + de: 'Wartezeit in Sekunden nach jeder Datenanfrage (optional, default: 5)' en: 'Seconds to wait after each request (optional, default: 5)' item_attributes: diff --git a/comfoair/plugin.yaml b/comfoair/plugin.yaml index 3b50b7e19..806000e79 100755 --- a/comfoair/plugin.yaml +++ b/comfoair/plugin.yaml @@ -36,7 +36,7 @@ parameters: de: 'Netzwerverbindung: Hostname/IP des KWL Systems' en: 'Network connection: Hostname/IP of KWL system' port: - type: num + type: int description: de: 'Netzwerkverbindung: Port des KWL Systems' en: 'Network connection: Port of KWL system' diff --git a/database/__init__.py b/database/__init__.py index 97b26591a..453073cf9 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -60,7 +60,7 @@ class Database(SmartPlugin): ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = '1.5.8' + PLUGIN_VERSION = '1.5.9' # SQL queries: {item} = item table name, {log} = log table name # time, item_id, val_str, val_num, val_bool, changed @@ -78,28 +78,36 @@ class Database(SmartPlugin): '6': ["CREATE INDEX {item}_name ON {item} (name);", "DROP INDEX {item}_name;"] } - def __init__(self, smarthome, driver, connect, prefix="", cycle=60, precision=2): - self._sh = smarthome + def __init__(self, sh, *args, **kwargs): + # Call init code of parent class (SmartPlugin or MqttPlugin) + super().__init__() + self.shtime = Shtime.get_instance() self.items = Items.get_instance() - - self._dump_cycle = int(cycle) - self._precision = int(precision) + # driver, connect, prefix="", cycle=60, precision=2 + self._dump_cycle = self.get_parameter_value('cycle') + self._precision = self.get_parameter_value('precision') self._name = self.get_instance_name() - self._replace = {table: table if prefix == "" else prefix + "_" + table for table in ["log", "item"]} + self._replace = {table: table if (self.get_parameter_value('prefix') == "" or self.get_parameter_value( + 'prefix') is None) else self.get_parameter_value( + 'prefix') + "_" + table for table in ["log", "item"]} self._replace['item_columns'] = ", ".join(COL_ITEM) self._replace['log_columns'] = ", ".join(COL_LOG) self._buffer = {} self._buffer_lock = threading.Lock() self._dump_lock = threading.Lock() - self._db = lib.db.Database(("" if prefix == "" else prefix.capitalize() + "_") + "Database", driver, - Utils.string_to_list(connect)) + self._db = lib.db.Database(("" if (self.get_parameter_value('prefix') == "" or self.get_parameter_value( + 'prefix') is None) else self.get_parameter_value( + 'prefix').capitalize() + "_") + "Database", self.get_parameter_value('driver'), + Utils.string_to_list(self.get_parameter_value('connect'))) self._initialized = False self._initialize() - smarthome.scheduler.add('Database dump ' + self._name + ("" if prefix == "" else " [" + prefix + "]"), - self._dump, cycle=self._dump_cycle, prio=5) + self.scheduler_add('Database dump ' + self._name + ( + "" if (self.get_parameter_value('prefix') == "" or self.get_parameter_value( + 'prefix') is None) else " [" + self.get_parameter_value('prefix') + "]"), + self._dump, cycle=self._dump_cycle, prio=5) self.init_webinterface() return @@ -150,7 +158,7 @@ def update_item(self, item, caller=None, source=None, dest=None): start = self._timestamp(item.prev_change()) end = self._timestamp(item.last_change()) last = None if len(self._buffer[item]) == 0 or self._buffer[item][-1][1] is not None else \ - self._buffer[item][-1] + self._buffer[item][-1] if last: # update current value with duration self._buffer[item][-1] = (last[0], end - start, last[2]) else: # append new value with none duration @@ -226,7 +234,7 @@ def updateItem(self, id, time, duration=0, val=None, it=None, changed=None, cur= params.update(self._item_value_tuple(it, val)) self._execute(self._prepare( "UPDATE {item} SET time = :time, val_str = :val_str, val_num = :val_num, val_bool = :val_bool, changed = :changed WHERE id = :id;"), - params, cur=cur) + params, cur=cur) def readItem(self, id, cur=None): params = {'id': id} @@ -247,14 +255,14 @@ def insertLog(self, id, time, duration=0, val=None, it=None, changed=None, cur=N params.update(self._item_value_tuple(it, val)) self._execute(self._prepare( "INSERT INTO {log}(item_id, time, val_str, val_num, val_bool, duration, changed) VALUES (:id,:time,:val_str,:val_num,:val_bool,:duration,:changed);"), - params, cur=cur) + params, cur=cur) def updateLog(self, id, time, duration=0, val=None, it=None, changed=None, cur=None): params = {'id': id, 'time': time, 'changed': changed, 'duration': duration} params.update(self._item_value_tuple(it, val)) self._execute(self._prepare( "UPDATE {log} SET duration = :duration, val_str = :val_str, val_num = :val_num, val_bool = :val_bool, changed = :changed WHERE item_id = :id AND time = :time;"), - params, cur=cur) + params, cur=cur) def readLog(self, id, time, cur=None): params = {'id': id, 'time': time} @@ -465,9 +473,9 @@ def _series(self, func, start, end='now', count=100, ratio=1, update=False, step queries = { 'avg': 'MIN(time), ' + self._precision_query('AVG(val_num * duration) / AVG(duration)'), 'avg.order': 'ORDER BY time ASC', - 'integrate' : 'MIN(time), SUM(val_num * duration)', + 'integrate': 'MIN(time), SUM(val_num * duration)', 'count': 'MIN(time), SUM(CASE WHEN val_num{op}{value} THEN 1 ELSE 0 END)'.format(**expression['params']), - 'countall' : 'MIN(time), COUNT(*)', + 'countall': 'MIN(time), COUNT(*)', 'min': 'MIN(time), MIN(val_num)', 'max': 'MIN(time), MAX(val_num)', 'on': 'MIN(time), ' + self._precision_query('SUM(val_bool * duration) / SUM(duration)'), @@ -515,9 +523,9 @@ def _single(self, func, start, end='now', item=None): func, expression = self._expression(func) queries = { 'avg': self._precision_query('AVG(val_num * duration) / AVG(duration)'), - 'integrate' : 'SUM(val_num * duration)', + 'integrate': 'SUM(val_num * duration)', 'count': 'SUM(CASE WHEN val_num{op}{value} THEN 1 ELSE 0 END)'.format(**expression['params']), - 'countall' : 'COUNT(*)', + 'countall': 'COUNT(*)', 'min': 'MIN(val_num)', 'max': 'MAX(val_num)', 'on': self._precision_query('SUM(val_bool * duration) / SUM(duration)'), @@ -832,7 +840,7 @@ def item_csv(self, item_id): log_array.append(value_dict) reversed_arr = log_array[::-1] csv_file_path = '%s/var/db/%s_item_%s.csv' % ( - self.plugin._sh.base_dir, self.plugin.get_instance_name(), item_id) + self.plugin._sh.base_dir, self.plugin.get_instance_name(), item_id) with open(csv_file_path, 'w', encoding='utf-8') as f: writer = csv.writer(f, dialect="excel") diff --git a/database/plugin.yaml b/database/plugin.yaml index d5ecdf4e7..c3d7c14c6 100755 --- a/database/plugin.yaml +++ b/database/plugin.yaml @@ -12,8 +12,8 @@ plugin: documentation: http://smarthomeng.de/user/plugins/database/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1021844-neues-database-plugin - version: 1.5.8 # Plugin version - sh_minversion: 1.5b # minimum shNG version to use this plugin + version: 1.5.9 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance restartable: unknown @@ -34,6 +34,7 @@ parameters: en: 'specifies the connection parameters which is directly used to invoke the connect() function of the DB API 2 implementation (for SQLite lookup here, other databases depends on implementation). An example connect string for pymysql could be connect = host:127.0.0.1 | user:db_user | passwd:db_password | db:smarthome' prefix: type: str + default: '' description: de: 'Enthält ein Prefix welches vor die Datenbanktabellen des Plugins geschrieben wird' en: "if you want to log into an existing database with other tables you can specify a prefix for the plugins' tables" diff --git a/dmx/plugin.yaml b/dmx/plugin.yaml index 5e5bbcfab..ebda8affa 100755 --- a/dmx/plugin.yaml +++ b/dmx/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'Supports DMX interfaces NanoDMX and DMXking (RS-232)' maintainer: 'bmx' tester: nobody # Who tests this plugin? - state: develop # change to ready when done with development + state: ready # change to ready when done with development keywords: dmx gateway channel dimmer documentation: http://smarthomeng.de/user/plugins/dmx/user_doc.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py diff --git a/ebus/plugin.yaml b/ebus/plugin.yaml index 78be67744..78776ebf5 100755 --- a/ebus/plugin.yaml +++ b/ebus/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'Supports eBus heating systems (e.g. Vailant, Wolf, Kromschroeder) - The plugin connects to a ebusd damon (http://www.cometvisu.de/wiki/Ebusd) which is communicating with eBus heatings. Requirements: running ebusd deamon on the network (note: ebusd also requires an ebus-interface)' maintainer: '? (msinn)' tester: Sandman60 - state: develop + state: ready # keywords: kwd1 kwd2 # keywords, where applicable # documentation: https://github.com/smarthomeNG/plugins/blob/develop/mqtt/README.md # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py diff --git a/enigma2/README.md b/enigma2/README.md index a70bdc624..120db4cc5 100644 --- a/enigma2/README.md +++ b/enigma2/README.md @@ -210,7 +210,7 @@ enigma2: servicestream: type: str visu_acl: rw - eval: "'
\"Processing...\"'" + eval: "'\"Processing...\"'" eval_trigger: - init - enigma2.vusolo2.current.servicereference diff --git a/enigma2/__init__.py b/enigma2/__init__.py index b8f592cfb..d415cc22f 100644 --- a/enigma2/__init__.py +++ b/enigma2/__init__.py @@ -124,7 +124,7 @@ class Enigma2(SmartPlugin): Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the Enigma2Device """ ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.4.11" + PLUGIN_VERSION = "1.4.12" _url_suffix_map = dict([('about', '/web/about'), ('deviceinfo', '/web/deviceinfo'), @@ -145,8 +145,7 @@ class Enigma2(SmartPlugin): _key_event_information = ['current_eventtitle', 'current_eventdescription', 'current_eventdescriptionextended', 'e2servicereference', 'e2servicename'] - def __init__(self, smarthome, username='', password='', host='dreambox', port='80', ssl='True', verify='False', - cycle=300, fast_cycle=10): # , device_id='enigma2' + def __init__(self, sh, *args, **kwargs): """ Initalizes the plugin. The parameters describe for this method are pulled from the entry in plugin.conf. @@ -158,24 +157,26 @@ def __init__(self, smarthome, username='', password='', host='dreambox', port='8 :param verify: True or False => verification of SSL certificate :param cycle: Update cycle in seconds """ - # self.logger = logging.getLogger(__name__) - # self.logger.info('Init Enigma2 Plugin with device_id %s' % ) + # Call init code of parent class (SmartPlugin or MqttPlugin) + super().__init__() self._session = requests.Session() self._timeout = 10 - self._verify = self.to_bool(verify) + self._verify = self.to_bool(self.get_parameter_value('verify')) - ssl = self.to_bool(ssl) + ssl = self.to_bool(self.get_parameter_value('ssl')) if ssl and not self._verify: urllib3.disable_warnings() - self._enigma2_device = Enigma2Device(host, port, ssl, username, password) + self._enigma2_device = Enigma2Device(self.get_parameter_value('host'), self.get_parameter_value('port'), + self.get_parameter_value('ssl'), self.get_parameter_value('username'), + self.get_parameter_value('password')) - self._cycle = int(cycle) - self._fast_cycle = int(fast_cycle) - self._sh = smarthome + self._cycle = int(self.get_parameter_value('cycle')) + self._fast_cycle = int(self.get_parameter_value('fast_cycle')) - # Response Cache: Dictionary for storing the result of requests which is used for several different items, refreshed each update cycle. Please use distinct keys! + # Response Cache: Dictionary for storing the result of requests which is used for several different items, + # refreshed each update cycle. Please use distinct keys! self._response_cache = dict() self._response_cache_fast = dict() @@ -183,8 +184,6 @@ def run(self): """ Run method for the plugin """ -# self._sh.scheduler.add(__name__, self._update_loop, cycle=self._cycle) -# self._sh.scheduler.add(__name__ + "_fast", self._update_loop_fast, cycle=self._fast_cycle) self.scheduler_add('update', self._update_loop, cycle=self._cycle) self.scheduler_add('update_fast', self._update_loop_fast, cycle=self._fast_cycle) self.alive = True diff --git a/enigma2/plugin.yaml b/enigma2/plugin.yaml index be5e260a8..8033155e1 100755 --- a/enigma2/plugin.yaml +++ b/enigma2/plugin.yaml @@ -12,9 +12,9 @@ plugin: # documentation: http://smarthomeng.de/user/plugins/enigma2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/943871-enigma2-plugin - version: 1.4.11 # Plugin version - sh_minversion: 1.3c # minimum shNG version to use this plugin -# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) + version: 1.4.12 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin +# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance restartable: unknown classname: Enigma2 # class containing the plugin diff --git a/enocean/__init__.py b/enocean/__init__.py index 5d07e016d..d6ba67ed0 100644 --- a/enocean/__init__.py +++ b/enocean/__init__.py @@ -168,17 +168,18 @@ class EnOcean(SmartPlugin): PLUGIN_VERSION = "1.3.4" - def __init__(self, smarthome, serialport, tx_id=''): - self._sh = smarthome - self.port = serialport + def __init__(self, sh, *args, **kwargs): + self._sh = sh + self.port = self.get_parameter_value("serialport") self.logger = logging.getLogger(__name__) + tx_id = self.get_parameter_value("tx_id") if (len(tx_id) < 8): self.tx_id = 0 self.logger.warning('enocean: No valid enocean stick ID configured. Transmitting is not supported') else: self.tx_id = int(tx_id, 16) self.logger.info('enocean: Stick TX ID configured via plugin.conf to: {0}'.format(tx_id)) - self._tcm = serial.Serial(serialport, 57600, timeout=0.5) + self._tcm = serial.Serial(self.port, 57600, timeout=0.5) self._cmd_lock = threading.Lock() self._response_lock = threading.Condition() self._rx_items = {} diff --git a/enocean/plugin.yaml b/enocean/plugin.yaml index e01e1365a..96b970454 100755 --- a/enocean/plugin.yaml +++ b/enocean/plugin.yaml @@ -12,9 +12,9 @@ plugin: state: ready keywords: EnOcean, Eltako # url of documentation (wiki) page - #documentation: https://... - # url oof the support thread - #support: https://... + documentation: https://github.com/smarthomeNG/plugins/tree/master/enocean + # url of the support thread + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/26542-featurewunsch-enocean-plugin/page13 version: 1.3.4 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin diff --git a/garminconnect/plugin.yaml b/garminconnect/plugin.yaml index e0b84a2ae..a0832f058 100644 --- a/garminconnect/plugin.yaml +++ b/garminconnect/plugin.yaml @@ -7,6 +7,7 @@ plugin: en: 'Enables the retrieval of data (stats, heart rates) from Garmin Connect.' maintainer: Marc René Frieß # tester: # Who tests this plugin? + state: develop keywords: garmin documentation: 'http://smarthomeng.de/user/plugins_doc/config/garminconnect.html' # url of documentation (wiki) page support: 'https://knx-user-forum.de/forum/supportforen/smarthome-py/1451496-support-thread-f%C3%BCr-das-garminconnect-pluginn' diff --git a/homematic/__init__.py b/homematic/__init__.py index c712be395..ae2fe959c 100755 --- a/homematic/__init__.py +++ b/homematic/__init__.py @@ -3,7 +3,7 @@ ######################################################################### # Copyright 2018- Martin Sinn m.sinn@gmx.de ######################################################################### -# This file is part of SmartHomeNG. +# This file is part of SmartHomeNG. # # Sample plugin for new plugins to run with SmartHomeNG version 1.4 and # upwards. @@ -40,24 +40,24 @@ class Homematic(SmartPlugin): Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - + PLUGIN_VERSION = '1.5.0' # ALLOW_MULTIINSTANCE = False - + connected = False - + def __init__(self, sh, *args, **kwargs): """ Initalizes the plugin. The parameters descriptions for this method are pulled from the entry in plugin.yaml. - :param sh: **Deprecated**: The instance of the smarthome object. For SmartHomeNG versions **beyond** 1.3: **Don't use it**! + :param sh: **Deprecated**: The instance of the smarthome object. For SmartHomeNG versions **beyond** 1.3: **Don't use it**! :param *args: **Deprecated**: Old way of passing parameter values. For SmartHomeNG versions **beyond** 1.3: **Don't use it**! :param **kwargs:**Deprecated**: Old way of passing parameter values. For SmartHomeNG versions **beyond** 1.3: **Don't use it**! - + If you need the sh object at all, use the method self.get_sh() to get it. There should be almost no need for a reference to the sh object any more. - + The parameters *args and **kwargs are the old way of passing parameters. They are deprecated. They are imlemented to support older plugins. Plugins for SmartHomeNG v1.4 and beyond should use the new way of getting parameter values: use the SmartPlugin method get_parameter_value(parameter_name) instead. Anywhere within the Plugin you can get @@ -86,8 +86,8 @@ def __init__(self, sh, *args, **kwargs): self.hm_id += '_' + self.get_instance_name() # create HomeMatic object try: - self.hm = HMConnection(interface_id="myserver", autostart=False, - eventcallback=self.eventcallback, systemcallback=self.systemcallback, + self.hm = HMConnection(interface_id="myserver", autostart=False, + eventcallback=self.eventcallback, systemcallback=self.systemcallback, remotes={self.hm_id:{"ip": self.host, "port": self.port}}) # remotes={self.hm_id:{"ip": self.host, "port": self.port}, self.hmip_id:{"ip": self.host, "port": self.port_hmip}}) except: @@ -103,8 +103,8 @@ def __init__(self, sh, *args, **kwargs): self.hmip_id += '_' + self.get_instance_name() # create HomeMaticIP object try: - self.hmip = HMConnection(interface_id="myserver_ip", autostart=False, - eventcallback=self.eventcallback, systemcallback=self.systemcallback, + self.hmip = HMConnection(interface_id="myserver_ip", autostart=False, + eventcallback=self.eventcallback, systemcallback=self.systemcallback, remotes={self.hmip_id:{"ip": self.host, "port": self.port_hmip}}) except: self.logger.error("Unable to create HomeMaticIP object") @@ -112,7 +112,7 @@ def __init__(self, sh, *args, **kwargs): # return - # set the name of the thread that got created by pyhomematic to something meaningfull + # set the name of the thread that got created by pyhomematic to something meaningfull self.hm._server.name = self.get_fullname() # start communication with HomeMatic ccu @@ -123,7 +123,7 @@ def __init__(self, sh, *args, **kwargs): self.logger.error("Unable to start HomeMatic object - SmartHomeNG will be unable to terminate the thread vor this plugin (instance)") self.connected = False # self._init_complete = False - # stop the thread that got created by initializing pyhomematic + # stop the thread that got created by initializing pyhomematic # self.hm.stop() # return @@ -140,11 +140,11 @@ def __init__(self, sh, *args, **kwargs): if self.hm.devices.get(self.hm_id,{}) == {}: self.logger.error("Connection to ccu failed") # self._init_complete = False - # stop the thread that got created by initializing pyhomematic + # stop the thread that got created by initializing pyhomematic # self.hm.stop() # return - - self.hm_items = [] + + self.hm_items = [] if not self.init_webinterface(): # self._init_complete = False @@ -156,7 +156,7 @@ def __init__(self, sh, *args, **kwargs): def run(self): """ Run method for the plugin - """ + """ self.logger.debug("Run method called") self.alive = True # if you want to create child threads, do not make them daemon = True! @@ -230,17 +230,17 @@ def parse_item(self, item): hm_channel, hm_node = self.get_hmchannelforfunction(hm_function, hm_channel, dev.SENSORNODE, 'SE', item) if hm_node == '': hm_channel, hm_node = self.get_hmchannelforfunction(hm_function, hm_channel, dev.WRITENODE, 'WR', item) - + # self.logger.warning("parse_item {}: dev.ELEMENT='{}'".format(item, dev.ELEMENT)) - + self.logger.debug("{}, type='{}', address={}:{}, function='{}', devicetype='{}'".format(item, item.type(), hm_address, hm_channel, hm_function, hm_devicetype)) - + if hm_node == '': hm_node = None - + # store item and device information for plugin instance self.hm_items.append( [str(item), item, hm_address, hm_channel, hm_function, hm_node, dev_type] ) - + # Initialize item from HomeMatic if dev is not None: value = None @@ -267,7 +267,7 @@ def parse_item(self, item): else: if not init_error: self.logger.error("Not initializing {} from address={}:{}, function='{}'".format(item, hm_node, hm_address, hm_channel, hm_function)) - + return self.update_item @@ -295,9 +295,9 @@ def update_item(self, item, caller=None, source=None, dest=None): for i in self.hm_items: if item == i[1]: myitem = i - self.logger.warning("update_item: Test: item='{}', caller='{}', hm_function={}, itemvalue='{}'".format(item, caller, i[4], item())) - - self.logger.warning("update_item: Todo: Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) + self.logger.info("update_item: Test: item='{}', caller='{}', hm_function={}, itemvalue='{}'".format(item, caller, i[4], item())) + + self.logger.info("update_item: Todo: Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) dev_id = self.get_iattr_value(item.conf, 'hm_address') dev = self.hm.devices[self.hm_id].get(dev_id) @@ -308,7 +308,7 @@ def update_item(self, item, caller=None, source=None, dest=None): hm_channel = myitem[3] hm_function = myitem[4] dev.CHANNELS[int(hm_channel)].setValue(hm_function, item()) - self.logger.warning("update_item (hm): Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) + self.logger.info("update_item (hm): Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) else: dev = self.hmip.devices[self.hmip_id].get(dev_id) # Write item value to HomeMaticIP device @@ -317,18 +317,18 @@ def update_item(self, item, caller=None, source=None, dest=None): hm_channel = myitem[3] hm_function = myitem[4] dev.CHANNELS[int(hm_channel)].setValue(hm_function, item()) - self.logger.warning("update_item (hmIP): Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) + self.logger.info("update_item (hmIP): Called with value '{}' for item '{}' from caller '{}', source '{}' and dest '{}'".format(item(), item, caller, source, dest)) # ACTIONNODE: PRESS_LONG (action), PRESS_SHORT (action), [LEVEL (float 0.0-1.0)] - # LEVEL (float: 0.0-1.0), STOP (action), INHIBIT (bool), INSTALL_TEST (action), - # OLD_LEVEL (action), RAMP_TIME (float 0.0-85825945.6 s), RAMP_STOP (action) + # LEVEL (float: 0.0-1.0), STOP (action), INHIBIT (bool), INSTALL_TEST (action), + # OLD_LEVEL (action), RAMP_TIME (float 0.0-85825945.6 s), RAMP_STOP (action) # # Heizkörperthermostat (HM-CC-RT-DN): # SET_TEMPERATURE (float -10.0-50.0), AUTO_MODE (action), MANU_MODE (float 4.5-30.5), BOOST_MODE (action), COMFORT_MODE (action), LOWERING_MODE (action), # PARTY_MODE_SUBMIT (string), PARTY_TEMPERATURE (float 5.0-30.0), PARTY_START_TIME (int 0-1410), PARTY_START_DAY (int 0-31), PARTY_START_MONTH (int 1-12), PARTY_START_YEAR (int 0-99), # PARTY_STOP_TIME (int), PARTY_STOP_DAY (int), PARTY_STOP_MONTH (int), PARTY_STOP_YEAR (int) # - # Heizkörperthermostat (HM-CC-RT-DN-BoM): + # Heizkörperthermostat (HM-CC-RT-DN-BoM): # CLEAR_WINDOW_OPEN_SYMBOL (int 0-1), SET_SYMBOL_FOR_HEATING_PHASE (int 0-1), ... # # Funk- Wandthermostat ab V2.0 (CLIMATECONTROL_REGULATOR) @@ -343,11 +343,11 @@ def update_item(self, item, caller=None, source=None, dest=None): # Funk- Fernbedienung 19 Tasten mit Display # TEXT (string), BULB (action), SWITCH (action), WINDOW (action), DOOR (action), BLIND (action), SCENE (action), PHONE (action), # BELL (action), CLOCK (action), ARROW_UP (action), ARROW_DOWN (action), UNIT (option 0=NONE, 1=PERCENT, 2=WATT, 3=CELSIUS, 4=FAHRENHEIT), - # BEEP (option 0=NONE, 1-3=TONE1-3), BACKLIGHT (option 0=OFF, 1=ON, 2=BLINK_SLOW, 3=BLINK_FAST), + # BEEP (option 0=NONE, 1-3=TONE1-3), BACKLIGHT (option 0=OFF, 1=ON, 2=BLINK_SLOW, 3=BLINK_FAST), # SUBMIT (action), ALARM_COUNT (int 0-255), SERVICE_COUNT (int 0-255) # # ON_TIME (float 0.0-85825945.6 s), SUBMIT (string) - + # STATE, LEVEL, STOP (action) @@ -364,7 +364,7 @@ def init_webinterface(self): if self.mod_http == None: self.logger.error("Not initializing the web interface") return False - + import sys if not "SmartPluginWebIf" in list(sys.modules['lib.model.smartplugin'].__dict__): self.logger.warning("Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface") @@ -381,11 +381,11 @@ def init_webinterface(self): 'tools.staticdir.dir': 'static' } } - + # Register the web interface as a cherrypy app - self.mod_http.register_webif(WebInterface(webif_dir, self), - self.get_shortname(), - config, + self.mod_http.register_webif(WebInterface(webif_dir, self), + self.get_shortname(), + config, self.get_classname(), self.get_instance_name(), description='') return True @@ -399,10 +399,10 @@ def init_webinterface(self): def get_hmdevicetype(self, dev_id): """ get the devicetype for the given device id - + :param dev_id: HomeMatic device id :param type: str - + :return: device type :rtype: str """ @@ -422,7 +422,7 @@ def get_hmdevicetype(self, dev_id): def get_hmchannelforfunction(self, hm_function, hm_channel, node, hm_node, item): """ Returns the HomeMatic Channel of the deivce for the wanted function - + :param hm_function: Configured function (or STATE) :param hm_channel: Preconfigured channel or None :param node: the node attribute of the Homematic device (e.g. dev.BINARYNODE) @@ -439,7 +439,7 @@ def get_hmchannelforfunction(self, hm_function, hm_channel, node, hm_node, item) else: hm_node = '' return hm_channel, hm_node - + def systemcallback(self, src, *args): self.logger.info("systemcallback: src = '{}', args = '{}'".format(src, args)) @@ -449,7 +449,7 @@ def eventcallback(self, interface_id, address, value_key, value): """ Callback method for HomeMatic events - This method is called whenever the HomeMatic ccu processs an event + This method is called whenever the HomeMatic ccu processs an event """ defined = False for i in self.hm_items: @@ -466,8 +466,8 @@ def eventcallback(self, interface_id, address, value_key, value): if not defined: self.logger.debug("eventcallback: Ohne item Zuordnung: interface_id = '{}', address = '{}', {} = '{}'".format(interface_id, address, value_key, value)) - - + + # ------------------------------------------ # Webinterface of the plugin # ------------------------------------------ @@ -480,7 +480,7 @@ class WebInterface(SmartPluginWebIf): def __init__(self, webif_dir, plugin): """ Initialization of instance of class WebInterface - + :param webif_dir: directory where the webinterface of the plugin resides :param plugin: instance of the plugin :type webif_dir: str @@ -491,7 +491,7 @@ def __init__(self, webif_dir, plugin): self.plugin = plugin self.tplenv = self.init_template_environment() - + self.hm_id = self.plugin.hm_id self.hmip_id = self.plugin.hmip_id @@ -500,10 +500,10 @@ def __init__(self, webif_dir, plugin): def index(self, learn=None, reload=None): """ Build index.html for cherrypy - + Render the template and return the html file to be delivered to the browser - - :return: contents of the template after beeing rendered + + :return: contents of the template after beeing rendered """ if learn == 'on': self.plugin.hm.setInstallMode(self.plugin.hm_id) @@ -512,7 +512,7 @@ def index(self, learn=None, reload=None): host = self.plugin.host devices = [] ipdevices = [] - + try: interface = self.plugin.hm.listBidcosInterfaces(self.hm_id)[0] # [{'DEFAULT': True, 'DESCRIPTION': '', 'ADDRESS': 'OEQ1658621', 'TYPE': 'CCU2', 'DUTY_CYCLE': 1, 'CONNECTED': True, 'FIRMWARE_VERSION': '2.8.5'}] @@ -524,7 +524,7 @@ def index(self, learn=None, reload=None): # [{'DEFAULT': True, 'DESCRIPTION': '', 'ADDRESS': 'OEQ1658621', 'TYPE': 'CCU2', 'DUTY_CYCLE': 1, 'CONNECTED': True, 'FIRMWARE_VERSION': '2.8.5'}] except: interfaceip = None - + # get HomeMatic devices for dev_id in self.plugin.hm.devices[self.hm_id]: dev = self.plugin.hm.devices[self.hm_id][dev_id] @@ -548,10 +548,10 @@ def index(self, learn=None, reload=None): except: pass devices.append(d) - + d['dev'] = dev device_count = len(devices) - + # get HomeMaticIP devices for dev_id in self.plugin.hmip.devices[self.hmip_id]: dev = self.plugin.hmip.devices[self.hmip_id][dev_id] @@ -575,18 +575,18 @@ def index(self, learn=None, reload=None): except: pass ipdevices.append(d) - + d['dev'] = dev ipdevice_count = len(ipdevices) # self.logger.warning("ipdevice_count = {}, ipdevices = {}".format(ipdevice_count, ipdevices)) - + tmpl = self.tplenv.get_template('index.html') - # The first paramter for the render method has to be specified. the base template + # The first paramter for the render method has to be specified. the base template # for the web interface relys on the instance of the plugin to be passed as p - return tmpl.render(p=self.plugin, + return tmpl.render(p=self.plugin, interface=interface, interfaceip=interfaceip, - devices=devices, device_count=device_count, - ipdevices=ipdevices, ipdevice_count=ipdevice_count, + devices=devices, device_count=device_count, + ipdevices=ipdevices, ipdevice_count=ipdevice_count, items=sorted(self.plugin.hm_items), item_count=len(self.plugin.hm_items), hm=self.plugin.hm, hm_id=self.plugin.hm_id ) diff --git a/ical/README.md b/ical/README.md index d34280b1a..2d6d04e55 100755 --- a/ical/README.md +++ b/ical/README.md @@ -1,6 +1,12 @@ # iCal ## Changelog +1.5.4: +- added parameter handle_login to control logging of calendar uri login data. + +1.5.3: +- fixed conversion from calendar defined timezones in smarthomeNG configured timezone. + 1.5.2: - Use domain name as filename if no alias is defined - Parse calendars in plugin.yaml more robust diff --git a/ical/__init__.py b/ical/__init__.py index 505a5d097..c96aefa7e 100755 --- a/ical/__init__.py +++ b/ical/__init__.py @@ -23,6 +23,10 @@ import datetime import os import errno +import re +#from datetime import timezone, timedelta +from datetime import timezone + import dateutil.tz import dateutil.rrule @@ -34,7 +38,7 @@ class iCal(SmartPlugin): - PLUGIN_VERSION = "1.5.2" + PLUGIN_VERSION = "1.5.4" ALLOW_MULTIINSTANCE = False DAYS = ("MO", "TU", "WE", "TH", "FR", "SA", "SU") FREQ = ("YEARLY", "MONTHLY", "WEEKLY", "DAILY", "HOURLY", "MINUTELY", "SECONDLY") @@ -56,6 +60,7 @@ def __init__(self, smarthome): cycle = self.get_parameter_value('cycle') calendars = self.get_parameter_value('calendars') config_dir = self.get_parameter_value('directory') + self.handle_login = self.get_parameter_value('handle_login') except Exception as err: self.logger.error('Problems initializing: {}'.format(err)) self._init_complete = False @@ -79,10 +84,10 @@ def __init__(self, smarthome): if ':' in calendar and 'http' != calendar[:4]: name, _, cal = calendar.partition(':') calendar = cal.strip() - self.logger.info('Registering calendar {} with alias {}.'.format(calendar, name)) + self.logger.info('Registering calendar {} with alias {}.'.format(self._clean_uri(calendar), name)) self._ical_aliases[name.strip()] = calendar else: - self.logger.info('Registering calendar {} without alias.'.format(calendar)) + self.logger.info('Registering calendar {} without alias.'.format(self._clean_uri(calendar))) calendar = calendar.strip() self._icals[calendar] = self._read_events(calendar) @@ -151,7 +156,7 @@ def _update_items(self): def _update_calendars(self): for uri in self._icals: self._icals[uri] = self._read_events(uri) - self.logger.debug('Updated calendar {0}'.format(uri)) + self.logger.debug('Updated calendar {0}'.format(self._clean_uri(uri))) if len(self._icals): self._update_items() @@ -220,18 +225,44 @@ def _read_events(self, ics, username=None, password=None, prio=1, verify=True): return self._parse_ical(ical, ics, prio) + #parse different date formats used in google calendar. Timezone is either coded in time field cointaining ('T') or in separate TZID field. def _parse_date(self, val, dtzinfo, par=''): - if par.startswith('TZID='): - tmp, par, timezone = par.partition('=') + if 'T' in val: # ISO datetime val, sep, off = val.partition('Z') dt = datetime.datetime.strptime(val, "%Y%m%dT%H%M%S") + # 'Z' indicates 'Zulu', thus UTC time, therefore specify time zone utc: + dt = dt.replace(tzinfo=datetime.timezone.utc) + + # the following condition occurs for complete day schedules. They do not have the 'T' lettre in start/end times. else: # date y = int(val[0:4]) m = int(val[4:6]) d = int(val[6:8]) dt = datetime.datetime(y, m, d) - dt = dt.replace(tzinfo=dtzinfo) + # Using timestamp configured in smarthome as reference for all complete day timestamps: + dt = dt.replace(tzinfo=dtzinfo) + + # handling of series calendar entries: + if par.startswith('TZID='): + tmp, par, timezoneFromCalendar = par.partition('=') + #self.logger.debug('Decoding series entry with time zone: {0}'.format(timezoneFromCalendar)) + #self.logger.debug('Datetime before conversion: {0}'.format(dt)) + + calendar_tz = dateutil.tz.gettz(timezoneFromCalendar) + + dt = dt.replace(tzinfo=calendar_tz) + + #self.logger.debug('Datetime after conversion for series entries: {}'.format(dt)) + + # convert all time stamps to local timezone, configured in smarthome + dt = dt.astimezone(self.sh.tzinfo()) + #self.logger.debug('Datetime after final conversion in plugin ical: {}'.format(dt)) + + + #convert time based on time zone info which has been extracted on a higher level: + #dt = dt.astimezone(dtzinfo) + return dt def _parse_ical(self, ical, ics, prio): @@ -276,14 +307,16 @@ def _parse_ical(self, ical, ics, prio): key, sep, val = line.partition(':') key, sep, par = key.partition(';') key = key.upper() + # why does the folowing code overwrite the time zone info configured in smarthomeNG? if key == 'TZID': tzinfo = dateutil.tz.gettz(val) + self.logger.warning('Debug time zone: {0}'.format(val)) elif key in ['UID', 'SUMMARY', 'SEQUENCE', 'RRULE', 'CLASS', 'DESCRIPTION']: if event.get(key) is None or prio_count[key] == prio: prio_count[key] = prio_count.get(key) + 1 event[key] = val else: - self.logger.info('Value {} for entry {} ignored because of prio setting'.format(val, key)) + self.logger.debug('Value {} for entry {} ignored because of prio setting'.format(val, key)) elif key in ['DTSTART', 'DTEND', 'EXDATE', 'RECURRENCE-ID']: try: date = self._parse_date(val, tzinfo, par) @@ -354,3 +387,15 @@ def _parse_rrule(self, event, tzinfo): args[par.lower()] = rrule[par] return dateutil.rrule.rrule(freq, **args) + + def _clean_uri(self, uri): + # check for http[s]?://user:password@domain.tld/... style uris and strip name/pass + pattern = re.compile('http([s]?)://([^:]+:[^@]+@)') + if self.handle_login == 'show' or not pattern.match(uri): + return uri + + replacement = { + 'strip': 'http\g<1>://', + 'mask': 'http\g<1>://***:***@' + } + return pattern.sub(replacement[self.handle_login], uri) \ No newline at end of file diff --git a/ical/plugin.yaml b/ical/plugin.yaml index 2f1a93f7b..c80ac4571 100755 --- a/ical/plugin.yaml +++ b/ical/plugin.yaml @@ -20,7 +20,7 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1352089-support-thread-zum-ical-plugin - version: 1.5.2 # Plugin version + version: 1.5.4 # Plugin version sh_minversion: 1.5 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance @@ -60,6 +60,20 @@ parameters: en: "list of calendars to automatically keep up to date and provided via `sh.ical()` function. Configures an alias (optional) and the URI of calendar, which can be a local file or a remote file when starting with `https://`. Online calendars are downloaded in the folder var/ical and updated depending on the cycle attribute.\n Example: holidays:https://cal.server/holidays.ics" + handle_login: + type: str + default: 'mask' + valid_list: + - 'mask' + - 'show' + - 'strip' + description: + de: "Umgang mit Login-Daten (http://user:pass@domain.tld/path) in Kalender-URIs in Logs." + en: "Handling of login data (http://user:pass@domain.tld/path) in calendar uris in logs." + description_long: + de: "Umgang mit Login-Daten (http://user:pass@domain.tld/path) in Kalender-URIs in Logs. 'mask' ersetzt die Login-Daten durch '***', 'show' gibt sie ins Log aus und 'strip' löscht den jeweiligen Teil der URI vor Ausgabe ins Log." + en: "Handling of login data (http://user:pass@domain.tld/path) in calendar uris in logs. 'mask' replaces login data with '***', 'show' prints login data to the log, 'strip' removes login data before logging." + item_attributes: # Definition of item attributes defined by this plugin ical_calendar: @@ -91,11 +105,13 @@ plugin_functions: description_long: de: "Es wird ein dictionary mit einem datetime.date Objekt als Key und folgenden Arraywerten zurückgeliefert:\n die Event Startzeit, die Event Endzeit, die Eventkategorie (z.B. privat), die Eventzusammenfassung (z.B. die Notizen)\n + Start- und Endzeit werden durch das Plugin von der im Kalender verwendeten Zeitzone in die lokale smarthomeNG Zeitzone konvertiert.\n \n Wenn ein Kalender öfters verwendet werden soll, wird empfohlen, diesen in der etc/plugin.yaml zu konfigurieren. " en: "It returns a dictonary with a datetime.date object as key and an array including\n the event start time, the event end time, the event's class type (e.g. private calendar entry), the event's summary, i.e. content\n + The plugin converts start and end times from calendar based timezones in local smarthomeNG timezone.\n \n If you want to use a calendar more regularly it could be helpful to configure this calendar in the plugin configuration. " diff --git a/ical/user_doc.rst b/ical/user_doc.rst index f6346ba2c..943d2ad7e 100755 --- a/ical/user_doc.rst +++ b/ical/user_doc.rst @@ -76,6 +76,7 @@ Beispiel for day in holidays: logger.info("Date: {0}".format(day)) for event in holidays[day]: + #The folloging code extracts the start time in python datetime format, already converted into the local time zone configured for smarthomeNG. start = event['Start'] summary = event['Summary'] cal_class = event['Class'] diff --git a/influxdb/__init__.py b/influxdb/__init__.py index 170292ecc..7057fa7bd 100644 --- a/influxdb/__init__.py +++ b/influxdb/__init__.py @@ -26,7 +26,7 @@ class InfluxDB(SmartPlugin): - PLUGIN_VERSION = "1.0.0" + PLUGIN_VERSION = "1.0.1" ALLOW_MULTIINSTANCE = False def __init__(self, smarthome, host='localhost', udp_port=8089, keyword='influxdb', tags={}, fields={}, value_field='value'): @@ -34,7 +34,7 @@ def __init__(self, smarthome, host='localhost', udp_port=8089, keyword='influxdb self.logger.info('Init InfluxDB') self.host = host - self.udp_port = udp_port + self.udp_port = int(udp_port) self.keyword = keyword self.tags = tags self.fields = fields diff --git a/influxdb/plugin.yaml b/influxdb/plugin.yaml index d3ea5a471..5dee763d5 100755 --- a/influxdb/plugin.yaml +++ b/influxdb/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: database documentation: https://github.com/smarthomeNG/smarthome/wiki/Installation-Influx-Grafana # url of documentation (wiki) page - version: 1.0.0 # Plugin version + version: 1.0.1 # Plugin version sh_minversion: 1.1 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance diff --git a/jsonread/plugin.yaml b/jsonread/plugin.yaml index b58fd55f3..0f917a749 100755 --- a/jsonread/plugin.yaml +++ b/jsonread/plugin.yaml @@ -7,6 +7,7 @@ plugin: en: 'json parser plugin based on jq' maintainer: Torsten Dreyer tester: none (yet) + state: develop keywords: json documentation: http://smarthomeng.de/user/plugins_doc/config/not-yet.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/not-yet @@ -39,4 +40,4 @@ item_attributes: # Definition of item attributes defined by this plugin jsonread_filter: type: str - + diff --git a/kathrein/plugin.yaml b/kathrein/plugin.yaml index 7e601a94e..7bddfb9a7 100755 --- a/kathrein/plugin.yaml +++ b/kathrein/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'Plugin to control Kathrein receiver' maintainer: bmxp tester: mayrjohannes, waldmeister68 # Who tests this plugin? - state: develop # change to ready when done with development + state: ready # change to ready when done with development keywords: kathrein receiver ufd documentation: https://github.com/smarthomeNG/smarthome/Kathrein/Readme.md # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py diff --git a/logo/README.md b/logo/README.md index 618b8c2b1..f491798af 100644 --- a/logo/README.md +++ b/logo/README.md @@ -17,6 +17,9 @@ NOTE: The library libnodave runs only on 32-bit machines!! Version 1.2.1 The version is not tested with new multi-instance functionality of SmartHomeNG. +There is a new plugin in development which uses snap7. Details can be found +in support thread at https://knx-user-forum.de/forum/supportforen/smarthome-py/36372-plugin-siemens-logo-0ba7 + ## Supported Hardware Siemens LOGO version 0BA7 diff --git a/logo/__init__.py b/logo/__init__.py index 883018fc4..bf8e8f6d4 100644 --- a/logo/__init__.py +++ b/logo/__init__.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### -# Copyright 2013 KNX-User-Forum e.V. http://knx-user-forum.de/ +# Copyright 2013 KNX-User-Forum e.V. http://knx-user-forum.de/ +# Copyright 2016 - 2018 Bernd Meiners Bernd.Meiners@mail.de ######################################################################### -# This file is part of SmartHomeNG. https://github.com/smarthomeNG// +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py # # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,30 +20,40 @@ # # You should have received a copy of the GNU General Public License # along with SmartHomeNG. If not, see . +# ######################################################################### + import ctypes import os import string import time import logging import threading -from lib.model.smartplugin import SmartPlugin + +from lib.module import Modules +from lib.model.smartplugin import * +from lib.item import Items class LOGO(SmartPlugin): - ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.2.1" - def __init__(self, smarthome, io_wait=5, host='192.168.0.76', port=102, version='0BA7'): - self.logger = logging.getLogger(__name__) - self.host = str(host).encode('ascii') - self.port = int(port) - self._version = version - self._io_wait = float(io_wait) + PLUGIN_VERSION = "1.2.4" + + def __init__(self, sh): + # Call init code of parent class (SmartPlugin) + super().__init__() + + from bin.smarthome import VERSION + if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5': + self.logger = logging.getLogger(__name__) + + self.host = self.get_parameter_value('host') + self.port = self.get_parameter_value('port') + self._version = self.get_parameter_value('version') + self._io_wait = self.get_parameter_value('io_wait') self._sock = False self._lock = threading.Lock() self.connected = False - self._sh = smarthome self._connection_attempts = 0 self._connection_errorlog = 2 @@ -58,7 +71,7 @@ def __init__(self, smarthome, io_wait=5, host='192.168.0.76', port=102, version= 'VM': {'VMaddr': 0, 'bytes': 850}} self.logger.info('LOGO: init {0}:{1} '.format(self.get_instance_name(), self.host)) - # Hardware Version 0BA8 + # Hardware Version 0BA8 self._vmIO_0BA8 = 1024 # lesen der I Q M AI AQ AM ab VM-Adresse VM1024 self._vmIO_len_0BA8 = 445 # Anzahl der zu lesenden Bytes 445 self.table_VM_IO_0BA8 = { # Address-Tab der I,Q,M,AI,AQ,AM im PLC-VM-Buffer @@ -79,16 +92,16 @@ def __init__(self, smarthome, io_wait=5, host='192.168.0.76', port=102, version= self.tableVM_IO = self.table_VM_IO_0BA8 self._vmIO = self._vmIO_0BA8 self._vmIO_len = self._vmIO_len_0BA8 - # End Hardware Version 0BA8 + # End Hardware Version 0BA8 self.reads = {} self.writes = {} self.Dateipfad = '/lib' # Dateipfad zur Bibliothek self.threadLastRead = 0 # verstrichene Zeit zwischen zwei LeseBefehlen - smarthome.connections.monitor(self) # damit connect ausgeführt wird + self.get_sh().connections.monitor(self) # damit connect ausgeführt wird - # libnodave Parameter zum lesen aus LOGO + # libnodave Parameter zum lesen aus LOGO self.ph = 0 # Porthandle self.di = 0 # Dave Interface Handle self.dc = 0 diff --git a/logo/plugin.yaml b/logo/plugin.yaml index 97da13eb1..ac5838a93 100755 --- a/logo/plugin.yaml +++ b/logo/plugin.yaml @@ -3,18 +3,18 @@ plugin: # Global plugin attributes type: interface # plugin type (gateway, interface, protocol, system, web) description: - de: 'Ansteuerung einer Siemens LOGO PLC' - en: 'Control a Siemens LOGO PLC' - maintainer: '?' - tester: '?' # Who tests this plugin? + de: 'Ansteuerung einer LOGO SPS' + en: 'Control a LOGO PLC' + maintainer: 'ivande' + tester: '' # Who tests this plugin? state: ready -# keywords: iot xyz -# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page -# support: https://knx-user-forum.de/forum/supportforen/smarthome-py + keywords: LOGO 0BA7 0BA8 PLC SPS libnodave + documentation: https://www.smarthomeng.de/dev/user/plugins/logo/README.html + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/36372-plugin-siemens-logo-0ba7 - version: 1.2.3 # Plugin version + version: 1.2.4 # Plugin version sh_minversion: 1.2 # minimum shNG version to use this plugin -# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) + #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance restartable: unknown classname: LOGO # class containing the plugin @@ -28,15 +28,15 @@ parameters: valid_min: 1 valid_max: 600 description: - de: 'Zeit zwischen zwei read requests' - en: 'timeperiod between two read requests' + de: 'Zeit zwischen zwei Leseanforderungen' + en: 'time period between two read requests' host: - type: str - default: '192.168.0.76' + type: ip + default: '' description: - de: 'Adresse der Siemens LOGO' - en: 'Address of the Siemens LOGO' + de: 'IP-Adresse oder Hostname der LOGO' + en: 'Hostname or ip address of the LOGO' port: type: int @@ -44,8 +44,8 @@ parameters: valid_min: 0 valid_max: 65535 description: - de: 'Von Siemens LOGO benutzter Port' - en: 'Port used Siemens LOGO' + de: 'Kommunikationsport der LOGO' + en: 'Communication port used by LOGO' version: type: str @@ -54,8 +54,8 @@ parameters: - '0BA7' - '0BA8' description: - de: 'Siemens Hardware Version' - en: 'Hardware version of the Siemens LOGO' + de: 'Hardware Version der LOGO' + en: 'Hardware version of the LOGO' item_attributes: # Definition of item attributes defined by this plugin @@ -63,13 +63,13 @@ item_attributes: logo_read: type: str description: - de: "Lesebefehl für die Siemens LOGO PLC" - en: "Read command for the Siemens LOGO PLC" + de: "Lesebefehl für die LOGO SPS" + en: "Read command for the LOGO PLC" logo_write: type: str description: - de: "Schreibbefehl für die Siemens LOGO PLC" - en: "Write command for the Siemens LOGO PLC" + de: "Schreibbefehl für die LOGO SPS" + en: "Write command for the LOGO PLC" item_structs: NONE diff --git a/mailrcv/plugin.yaml b/mailrcv/plugin.yaml index d870f4b4e..a8f74d293 100644 --- a/mailrcv/plugin.yaml +++ b/mailrcv/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'Receive emails via imap' maintainer: msinn tester: psilo909, onkelandy, Sandman60 - state: develop + state: ready # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py @@ -66,7 +66,7 @@ parameters: description: de: 'Passwort für die Anmeldung am IMAP host' en: 'Password for login to the IMAP host' - + trashfolder: type: str default: 'Trash' diff --git a/mailsend/plugin.yaml b/mailsend/plugin.yaml index 63d147755..7df25aa4f 100644 --- a/mailsend/plugin.yaml +++ b/mailsend/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'Send emails via smtp. This plugin allows to send emails from logics by calling a function.' maintainer: msinn tester: psilo909, onkelandy, Sandman60 - state: develop + state: ready # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py diff --git a/mqtt/plugin.yaml b/mqtt/plugin.yaml index d48f8c8c4..2b553b8fa 100644 --- a/mqtt/plugin.yaml +++ b/mqtt/plugin.yaml @@ -7,7 +7,7 @@ plugin: en: 'MQTT plugin which utilizes the MQTT module of SmartHomeNG for communication' maintainer: msinn # tester: # Who tests this plugin? - state: develop # change to ready when done with development + state: ready # change to ready when done with development keywords: iot # documentation: http://smarthomeng.de/user/plugins/mqtt2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1089334-neues-mqtt-plugin @@ -19,7 +19,7 @@ plugin: restartable: unknown classname: Mqtt2 # class containing the plugin -parameters: +parameters: NONE # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) item_attributes: @@ -117,12 +117,12 @@ item_attributes: de: "Gewünschte Werte (im MQTT Payload) für boolsche Items (z.B. ['Falsch','Wahr'])" en: "Values (in MQTT payload) for boolean items (e.g. ['Wrong','Right'])" -item_structs: +item_structs: NONE # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) -plugin_functions: +plugin_functions: NONE # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) -logic_parameters: +logic_parameters: NONE # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/mqtt1/plugin.yaml b/mqtt1/plugin.yaml index 1c3e0a561..389d3f7bf 100644 --- a/mqtt1/plugin.yaml +++ b/mqtt1/plugin.yaml @@ -423,3 +423,12 @@ item_attributes: ' en: 'When set to True, the MQTT message is sent with the retain flag set.\n ' + +item_structs: NONE + # Definition of item-structure templates for this plugin + +logic_parameters: NONE + # Definition of logic parameters defined by this plugin + +plugin_functions: + # Definition of function interface of the plugin diff --git a/onewire/plugin.yaml b/onewire/plugin.yaml index 3ec3d3a38..911f4821a 100755 --- a/onewire/plugin.yaml +++ b/onewire/plugin.yaml @@ -9,6 +9,8 @@ plugin: tester: 'henfri, chester4444' state: ready keywords: 1wire onewire dallas ibutton sensor temperature humidity + #documentation: http://smarthomeng.de/user/plugins/onewire/user_doc.html + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1493319-support-thread-zum-onewire-plugin version: 1.6.0 # Plugin version sh_minversion: 1.4 # minimum shNG version to use this plugin multi_instance: False diff --git a/prowl/README.md b/prowl/README.md index 65749d34b..8938c4f40 100644 --- a/prowl/README.md +++ b/prowl/README.md @@ -1,8 +1,13 @@ # Prowl +From https://www.prowlapp.com taken: + +> Prowl is a push notification client for iOS. Push to your + iPhone, iPod touch, or iPad notifications from a Mac or Windows computer, or from a multitude of apps and services. Easily integrate the Prowl API into your applications. + ## Requirements -This plugin has no requirements or dependencies. +This plugin has no requirements or dependencies, it just calls a web api. ## Configuration @@ -17,7 +22,7 @@ notify: ``` #### Attributes - * `apikey`: this attribute is optional. You could define a global apikey for the prowl service. + * `apikey`: this attribute is optional. You could define a global **apikey** for the prowl service. * `instance`: this attribute is optional. If set it will be displayed in prowl messages. ## Functions @@ -25,11 +30,11 @@ notify: Because there is only one function you could access it directly by the object. With the above example it would look like this: ``sh.notify('Intrusion', 'Living room window broken!')`` This function takes several arguments: - 1. event: type of event. - 2. description: describes the event. - 3. priority: you could give a priority (0-2) to differentiate beetween events on your mobile device. - 4. url: This url would be linked to the notification. - 5. apikey: you could specify an individual apikey. + 1. **event**: type of event. + 2. **description**: describes the event. + 3. **priority**: you could give a priority (0-2) to differentiate between events on your mobile device. + 4. **url**: This url would be linked to the notification. + 5. **apikey**: you could specify an individual apikey. 6. application: describes the name of the application. By default it is SmartHome. ```python diff --git a/prowl/__init__.py b/prowl/__init__.py index dae7facef..fa0b7879a 100644 --- a/prowl/__init__.py +++ b/prowl/__init__.py @@ -3,7 +3,9 @@ ######################################################################### # Copyright 2012-2013 Marcus Popp marcus@popp.mx ######################################################################### -# This file is part of SmartHomeNG. https://github.com/smarthomeNG// +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py # # SmartHomeNG is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,31 +24,52 @@ import logging import urllib.parse import http.client -from lib.model.smartplugin import SmartPlugin +from lib.module import Modules +from lib.model.smartplugin import * +from lib.item import Items class Prowl(SmartPlugin): - ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = "1.3.0" + + PLUGIN_VERSION = "1.3.1" _host = 'api.prowlapp.com' _api = '/publicapi/add' - def __init__(self, smarthome, apikey=None): - self.logger = logging.getLogger(__name__) - self._apikey = apikey - self._sh = smarthome + def __init__(self, smarthome): + # Call init code of parent class (SmartPlugin) + super().__init__() + + from bin.smarthome import VERSION + if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5': + self.logger = logging.getLogger(__name__) + + self._apikey = self.get_parameter_value('apikey') def run(self): - pass + """ + Run method for the plugin + """ + self.logger.debug("Run method called") + self.alive = True def stop(self): - pass + """ + Stop method for the plugin + """ + self.logger.debug("Stop method called") + self.alive = False def notify(self, event='', description='', priority=None, url=None, apikey=None, application='SmartHomeNG'): + """Provides an exposed function to send a notification""" self.__call__(self, event, description, priority, url, apikey, application) def __call__(self, event='', description='', priority=None, url=None, apikey=None, application='SmartHomeNG'): + """does the work to send a notification to prowl api""" + if not self.alive: + self.logger.warning("Could not send prowl notification, the plugin is not alive!") + return + data = {} origin = application if self.get_instance_name() != '': diff --git a/prowl/plugin.yaml b/prowl/plugin.yaml index 6d08ad689..2ced41dc7 100755 --- a/prowl/plugin.yaml +++ b/prowl/plugin.yaml @@ -8,13 +8,13 @@ plugin: maintainer: Foxi352 state: ready tester: '?' # Who tests this plugin? -# keywords: iot xyz -# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page -# support: https://knx-user-forum.de/forum/supportforen/smarthome-py + keywords: iOS Prowl Growl SMS Push + documentation: https://www.smarthomeng.de/user/plugins/prowl/README.html + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1492644-support-thread-zum-prowl-plugin - version: 1.3.0 # Plugin version + version: 1.3.1 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin -# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) + #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance restartable: unknown classname: Prowl # class containing the plugin diff --git a/rcswitch/__init__.py b/rcswitch/__init__.py index c9f526e7b..b20aa0773 100755 --- a/rcswitch/__init__.py +++ b/rcswitch/__init__.py @@ -33,9 +33,9 @@ class RCswitch(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.2.0.4" + PLUGIN_VERSION = "1.2.0.5" - def __init__(self, smarthome, rcswitch_dir='/usr/local/bin/rcswitch-pi', rcswitch_sendDuration='0.5', rcswitch_host='', rcswitch_user='', rcswitch_password=''): + def __init__(self, smarthome, rcswitch_dir='/usr/local/bin/rcswitch-pi', rcswitch_sendDuration='0.5', rcswitch_host=None, rcswitch_user=None, rcswitch_password=None): self.logger = logging.getLogger(__name__) self.setupOK = True self.mapping = {'a':1,'A':1,'b':2,'B':2,'c':3,'C':3,'d':4,'D':4,'e':5,'E':5} @@ -58,7 +58,7 @@ def __init__(self, smarthome, rcswitch_dir='/usr/local/bin/rcswitch-pi', rcswitc if (rcswitch_host and (self.is_hostname(rcswitch_host) or self.is_ipv4(rcswitch_host))): # then check if user defined its own local host -> error if ((rcswitch_host == gethostname()) or (rcswitch_host == '127.0.0.1')): - self.logger.error('RCswitch: rcswitch_host is defined as your own machine, not the remote adress! Please check the parameter rcswitch_host, >>{}<< seems to be not correct!'.format(rcswitch_host)) + self.logger.error('RCswitch: rcswitch_host is defined as your own machine, not the remote address! Please check the parameter rcswitch_host, >>{}<< it seems to be not correct!'.format(rcswitch_host)) #check connection to remote host and accept fingerprint try: diff --git a/rcswitch/plugin.yaml b/rcswitch/plugin.yaml index 666bcb062..4fbc05d6a 100755 --- a/rcswitch/plugin.yaml +++ b/rcswitch/plugin.yaml @@ -40,7 +40,6 @@ parameters: rcswitch_host: type: ip - default: '' description: de: "IP oder HOSTNAME des rcswitch hosts, falls das System nicht auf dieser SmartHomeNG-Maschine läuft. \ Achtung: Auf dem Remote Host muss ein SSH Server installiert sein." @@ -49,14 +48,12 @@ parameters: rcswitch_user: type: str - default: '' description: de: "User auf dem Remote Host" en: "user at remote host" rcswitch_password: type: str - default: '' description: de: "Passwort auf dem Remote Host" en: "password for user at remote host" diff --git a/russound/__init__.py b/russound/__init__.py index e44e7a162..e5ba88025 100755 --- a/russound/__init__.py +++ b/russound/__init__.py @@ -178,7 +178,7 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_shortname(): # code to execute if the plugin is not stopped # and only, if the item has not been changed by this this plugin: - self.logger.info("Update item: {}, item has been changed outside this plugin".format(item.id())) + self.logger.info("Update item: {}, item has been changed outside this plugin (caller={}, source={}, dest={})".format(item.id(),caller, source, dest)) if self.has_iattr(item.conf, 'rus_path'): path = self.get_iattr_value(item.conf, 'rus_path') @@ -282,7 +282,7 @@ def found_terminator(self, resp): path = '{0}.{1}.{2}'.format(c, z, cmd) if path in list(self.params.keys()): self.params[path]['item']( - self._decode(cmd, value), 'Russound') + self._decode(cmd, value), self.get_shortname()) elif resp.startswith('System.status'): return elif resp[0] == 'S': @@ -295,7 +295,7 @@ def found_terminator(self, resp): # if s in self.sources.keys(): # for child in self.sources[s]['item'].return_children(): # if str(child).lower() == cmd.lower(): -# child(unicode(value, 'utf-8'), 'Russound') +# child(unicode(value, 'utf-8'), self.get_shortname()) return except Exception as e: self.logger.error(e) diff --git a/shelly/plugin.yaml b/shelly/plugin.yaml index bcaf60aff..8090f2673 100644 --- a/shelly/plugin.yaml +++ b/shelly/plugin.yaml @@ -6,8 +6,8 @@ plugin: de: 'Plugin zur Steuerung von Shelly Devices, welches das MQTT Module von SmartHomeNG zur Kommunikation nutzt.' en: 'Plugin to control Shelly devices which utilizes the MQTT module of SmartHomeNG for communication' maintainer: msinn -# tester: # Who tests this plugin? - state: develop # change to ready when done with development + tester: msinn # Who tests this plugin? + state: ready # change to ready when done with development keywords: iot # documentation: http://smarthomeng.de/user/plugins/mqtt2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1451853-support-thread-für-das-shelly-plugin @@ -19,7 +19,7 @@ plugin: restartable: unknown classname: Shelly # class containing the plugin -parameters: +parameters: NONE # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) item_attributes: @@ -69,12 +69,12 @@ item_attributes: de: "Nummer des zu schaltenden Relais" en: "Number of the relay to use for switching" -item_structs: +item_structs: NONE # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) -plugin_functions: +plugin_functions: NONE # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) -logic_parameters: +logic_parameters: NONE # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/snap7_logo/plugin.yaml b/snap7_logo/plugin.yaml index 7ba538a45..5cc7da2d7 100644 --- a/snap7_logo/plugin.yaml +++ b/snap7_logo/plugin.yaml @@ -7,6 +7,7 @@ plugin: en: 'Control of a Siemens LOGO PLC' maintainer: 'ivande' # tester: # Who tests this plugin? + state: ready # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py @@ -31,38 +32,38 @@ parameters: description: de: 'tsap_server - Dezimalzahl' en: 'tsap_server - dezimal' - + tsap_client: type: num default: 0x0100 description: de: 'tsap_client - Dezimalzahl' en: 'tsap_client - dezimal' - + version: type: str default: '0BA7' description: de: 'Siemens Hardware Version 0BA7 oder 0BA8' en: 'Siemens hardware version 0BA7 or 0BA8' - + cycle: type: num default: 5 description: de: 'Zeit (sec) nachdem eine neue Verbindung zur Logo aufgebaut wird, um Änderungen zu holen' en: 'time (sec) after a new link to Logo will be established to get updates' - + item_attributes: logo_read: type: str description: de: 'Eingang, Ausgang, Merker von der Siemens-Logo lesen' en: 'Input, Output, Mark to read from Siemens Logo' - + logo_write: type: str description: de: 'Eingang, Ausgang, Merker in die Siemens-Logo schreiben' en: 'Input, Output, Mark to write from Siemens Logo' - + diff --git a/sonos/plugin.yaml b/sonos/plugin.yaml index de406bad4..054c410d0 100755 --- a/sonos/plugin.yaml +++ b/sonos/plugin.yaml @@ -7,6 +7,7 @@ plugin: en: 'Sonos plugin' maintainer: pfischi tester: pfischi + state: ready keywords: Sonos sonos multimedia documentation: https://github.com/smarthomeNG/plugins/tree/master/sonos # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/25151-sonos-anbindung @@ -70,4 +71,4 @@ parameters: item_attributes: # Definition of item attributes defined by this plugin - + diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 810e06e44..9823068eb 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -490,8 +490,11 @@ def _schedule(self, item, caller=None): _value = None self._update_sun(item, caller='schedule') if self._items[item].get('interpolation') is None: - self.logger.error("Something is wrong with your UZSU item. You most likely use a wrong smartVISU widget version! Use the latest device.uzsu from SV 2.9") - if not self._items[item]['interpolation'].get('itemtype'): + self.logger.error("Something is wrong with your UZSU item. You most likely use a wrong smartVISU widget version!" + " Use the latest device.uzsu from SV 2.9. " + "If you write your uzsu dict directly please use the format given in the documentation: " + "https://www.smarthomeng.de/user/plugins/uzsu/user_doc.html and include the interpolation array correctly!") + elif not self._items[item]['interpolation'].get('itemtype'): self.logger.error("item '{}' to be set by uzsu does not exist.".format( self.get_iattr_value(item.conf, ITEM_TAG[0]))) elif not self._items[item].get('list') and self._items[item].get('active') is True: diff --git a/vacations/plugin.yaml b/vacations/plugin.yaml index 63cb2f264..07ec230a8 100644 --- a/vacations/plugin.yaml +++ b/vacations/plugin.yaml @@ -7,6 +7,7 @@ plugin: en: 'Enables the retrieval of German school vacations for the provinces BW, BY, BE, BB, HB, HH, HE, MV, NI, NW, RP, SL, SN, ST, SH, TH.' maintainer: Marc René Frieß # tester: # Who tests this plugin? + state: develop keywords: vacations documentation: 'http://smarthomeng.de/user/plugins_doc/config/vacations.html' # url of documentation (wiki) page support: 'https://knx-user-forum.de/forum/supportforen/smarthome-py/1443788-support-thread-f%C3%BCr-das-vacations-plugin' diff --git a/webservices/README.md b/webservices/README.md index 08ef1598b..8eb79456f 100644 --- a/webservices/README.md +++ b/webservices/README.md @@ -11,7 +11,6 @@ Support-Thread für das Plugin: https://knx-user-forum.de/forum/supportforen/sma ## Requirements This plugin requires CherryPy to be installed via pip. -It requires SmartHomeNG 1.5 or higher! ## Configuration diff --git a/webservices/__init__.py b/webservices/__init__.py index 75692a5de..495a1f2ad 100644 --- a/webservices/__init__.py +++ b/webservices/__init__.py @@ -34,12 +34,16 @@ class WebServices(SmartPlugin): - PLUGIN_VERSION = '1.5.0.5' + PLUGIN_VERSION = '1.6.0' ALLOWED_FOO_PATHS = ['env.location.moonrise', 'env.location.moonset', 'env.location.sunrise', 'env.location.sunset'] - def __init__(self, smarthome, mode="all"): + def __init__(self, sh, *args, **kwargs): self.logger.debug("Plugin '{}': '__init__'".format(self.get_fullname())) - self._mode = mode + + # Call init code of parent class (SmartPlugin or MqttPlugin) + super().__init__() + + self._mode = self.get_parameter_value('mode') self.items = Items.get_instance() if not self.init_webinterfaces(): diff --git a/webservices/plugin.yaml b/webservices/plugin.yaml index 76277cda7..a631d9478 100755 --- a/webservices/plugin.yaml +++ b/webservices/plugin.yaml @@ -13,8 +13,8 @@ plugin: # documentation: http://smarthomeng.de/user/plugins/webservices/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1163886-support-thread-f%C3%BCr-das-webservices-plugin - version: 1.5.0.5 # Plugin version - sh_minversion: 1.5b # minimum shNG version to use this plugin + version: 1.6.0 # Plugin version + sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance restartable: unknown @@ -24,7 +24,7 @@ parameters: # Definition of parameters to be configured in etc/plugin.yaml mode: type: str - mandatory: False + default: 'all' description: de: '(optional) Mode des Plugins: "all", wenn die Webservice-Schnittstelle alle Items der SmartHomeNG Instanz verfügbar machen soll (default). Ansonsten werden nur Items ausgeliefert, die einem Set via webservices_set zugeordnet sind.' en: '(optional) Mode of the plugin: "all", if standard interface with all items shall be enabled (default if left empty). Otherwise only items are delivered via the interface that are added to a set via webservices_set attribute.' diff --git a/xiaomi_vac/README.md b/xiaomi_vac/README.md deleted file mode 100644 index 9c6a56fdd..000000000 --- a/xiaomi_vac/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Xiaomi Vacuum Robot Plugin for SmarthomeNG -Das Plugin basiert auf der [python-miio](https://github.com/rytilahti/python-miio) Bibliothek. Das Plugin bzw python-miio benötigt den Kommunikationstoken des Roboters. - -- [Bitte der Installationsanweisung von python-miio folgen](https://python-miio.readthedocs.io/en/latest/discovery.html#installation) -- anschließend den Plugin Ordner nach smarthomeNG/plugins kopieren -- Folgendes zur etc/plugin.yaml hinzufügen - - ``` - Roboter: - class_name: Robvac - class_path: plugins.xiaomi_vac - ip: '192.XXX.XXX.XXX' - token: 'euerToken' - read_cycle: 12 - ``` - -- Um die Verbindung zu überprüfen, kann in der Kommandozeile/Shell nach der Installation mit - -``` - export MIROBO_IP=192.xxx.xxx.xxx - export MIROBO_TOKEN=euerToken -``` - -- und anschließendem - -```mirobo``` -sollte eine Ausgabe ähnlich diesem anzeigen. -``` -> State: Charging -> Battery: 100 -> Fanspeed: 60 -> Cleaning since: 0:00:00 -> Cleaned area: 0.0 m² -> DND enabled: 0 -> Map present: 1 -> in_cleaning: 0" -``` - -## Funktionen - -folgende Werte/Funktionen können vom Saugroboter ausgelesen bzw. gestartet werden: -- Start -- Stop/ zur Ladestation fahren -- Pause -- Finden -- Spotreinigung -- Lüftergesschwindigkeit ändern -- Lautstärke ansage ändern -- gerenigte Fläche -- Reinigungszeit -- Status -- Zustand -- Fehlercode - -uvm. diff --git a/xiaomi_vac/__init__.py b/xiaomi_vac/__init__.py index 70e4554d1..539646b30 100644 --- a/xiaomi_vac/__init__.py +++ b/xiaomi_vac/__init__.py @@ -27,7 +27,7 @@ # VERSION - 2 # #################################################################################### -# +# import logging import threading import struct @@ -48,14 +48,14 @@ class Robvac(SmartPlugin): ALLOW_MULTIINSTANCE = False PLUGIN_VERSION="1.0.0" - - def __init__(self, smarthome,ip='127.0.0.1', token='', read_cycle=20): - self._ip = str(ip) - self._token = str(token) - self._cycle = int(read_cycle) + + def __init__(self, smarthome): + self._ip = self.get_parameter_value("ip") + self._token = self.get_parameter_value("token") + self._cycle = self.get_parameter_value("read_cycle") self._sh = smarthome self.logger = logging.getLogger(__name__) - + self.messages = {} self.found = False self._lock = threading.Lock() @@ -65,7 +65,7 @@ def __init__(self, smarthome,ip='127.0.0.1', token='', read_cycle=20): self._data = {} if not self.init_webinterface(): self._init_complete = False - + if self._token == '': self.logger.error("Xiaomi_Robvac: No Key for Communication given, Plugin would not start!") pass @@ -78,7 +78,7 @@ def __init__(self, smarthome,ip='127.0.0.1', token='', read_cycle=20): # ---------------------------------------------------------------------------------------------- # Verbinden zum Roboter # ---------------------------------------------------------------------------------------------- - def _connect(self): + def _connect(self): if self._connected == False: for i in range(0,self.retry_count_max-self.retry_count): try: @@ -92,19 +92,19 @@ def _connect(self): self._connected = False return False #finally: - - + + # ---------------------------------------------------------------------------------------------- # Daten Lesen, über SHNG bei item_Change # ---------------------------------------------------------------------------------------------- def groupread(self, ga, dpt): pass - + # ---------------------------------------------------------------------------------------------- # Daten Lesen, zyklisch - # ---------------------------------------------------------------------------------------------- + # ---------------------------------------------------------------------------------------------- + - def _read(self): data = {} #config @@ -114,147 +114,162 @@ def _read(self): clean_history = self.vakuum.clean_history() self._data['clean_total_count'] = int(clean_history.count) self._data['clean_total_area'] = round(clean_history.total_area,2) - self._data['clean_total_duration'] = clean_history.total_duration.total_seconds() // 3600 + self._data['clean_total_duration'] = clean_history.total_duration.total_seconds() / 60 self._data['clean_ids'] = clean_history.ids - self.logger.debug("Xiaomi_Robvac: Reingungsstatistik Anzahl {0}, Fläche {1}², Dauer {2}, clean ids {3}".format( - self._data['clean_total_count'], - self._data['clean_total_area'], - self._data['clean_total_duration'], - self._data['clean_ids'])) + self.logger.debug("Xiaomi_Robvac: Reingungsstatistik Anzahl {0}, Fläche {1}², " + "Dauer {2}, clean ids {3}".format( + self._data['clean_total_count'], + self._data['clean_total_area'], + self._data['clean_total_duration'], + self._data['clean_ids'])) #letzte reinigung #funktioniert nur mit übergebener id if self._data['clean_ids'] != None: #self._data['clean_ids'] = self._data['clean_ids'].sort(reverse=True) - self._data['clean_details_last0'] = self.vakuum.clean_details(self._data['clean_ids'][0],return_list=True) - self._data['last0_area'] = round(self._data['clean_details_last0'][0].area,2) - self._data['last0_complete'] = self._data['clean_details_last0'][0].complete - self._data['last0_duration'] = round(self._data['clean_details_last0'][0].duration.total_seconds()/ 3600,2) - self._data['last0_start_date'] = self._data['clean_details_last0'][0].start.strftime("%d.%m.%Y") - self._data['last0_start_time'] = self._data['clean_details_last0'][0].start.strftime("%H:%M") - self._data['last0_end_date'] = self._data['clean_details_last0'][0].start.strftime("%d.%m.%Y") - self._data['last0_end_time'] = (self._data['clean_details_last0'][0].start+self._data['clean_details_last0'][0].duration).strftime("%H:%M") - - self._data['clean_details_last1'] = self.vakuum.clean_details(self._data['clean_ids'][1],return_list=True) - self._data['last1_area'] = round(self._data['clean_details_last1'][0].area,2) - self._data['last1_complete'] = self._data['clean_details_last1'][0].complete - self._data['last1_duration'] = round(self._data['clean_details_last1'][0].duration.total_seconds()/ 3600,2) - self._data['last1_start_date'] = self._data['clean_details_last1'][0].start.strftime("%d.%m.%Y") - self._data['last1_start_time'] = self._data['clean_details_last1'][0].start.strftime("%H:%M") - self._data['last1_end_date'] = self._data['clean_details_last1'][0].start.strftime("%d.%m.%Y") - self._data['last1_end_time'] = (self._data['clean_details_last1'][0].start+self._data['clean_details_last1'][0].duration).strftime("%H:%M") - - self._data['clean_details_last2'] = self.vakuum.clean_details(self._data['clean_ids'][2],return_list=True) - self._data['last2_area'] = round(self._data['clean_details_last2'][0].area,2) - self._data['last2_complete'] = self._data['clean_details_last2'][0].complete - self._data['last2_duration'] = round(self._data['clean_details_last2'][0].duration.total_seconds()/ 3600,2) - self._data['last2_start_date'] = self._data['clean_details_last2'][0].start.strftime("%d.%m.%Y") - self._data['last2_start_time'] = self._data['clean_details_last2'][0].start.strftime("%H:%M") - self._data['last2_end_date'] = self._data['clean_details_last2'][0].start.strftime("%d.%m.%Y") - self._data['last2_end_time'] = (self._data['clean_details_last2'][0].start+self._data['clean_details_last2'][0].duration).strftime("%H:%M") - - self._data['clean_details_last3'] = self.vakuum.clean_details(self._data['clean_ids'][3],return_list=True) - self._data['last3_area'] = round(self._data['clean_details_last3'][0].area,2) - self._data['last3_complete'] = self._data['clean_details_last3'][0].complete - self._data['last3_duration'] = round(self._data['clean_details_last3'][0].duration.total_seconds()/ 3600,2) - self._data['last3_start_date'] = self._data['clean_details_last3'][0].start.strftime("%d.%m.%Y") - self._data['last3_start_time'] = self._data['clean_details_last3'][0].start.strftime("%H:%M") - self._data['last3_end_date'] = self._data['clean_details_last3'][0].start.strftime("%d.%m.%Y") - self._data['last3_end_time'] = (self._data['clean_details_last3'][0].start+self._data['clean_details_last3'][0].duration).strftime("%H:%M") - - self.logger.debug("Xiaomi_Robvac: Historische id1 {}, id2{}, id3 {}".format(self._data['clean_details_last0'], + self._data['clean_details_last0'] = self.vakuum.clean_details(self._data['clean_ids'][0],return_list=False) + self._data['last0_area'] = round(self._data['clean_details_last0'].area,2) + self._data['last0_complete'] = self._data['clean_details_last0'].complete + self._data['last0_duration'] = round(self._data['clean_details_last0'].duration.total_seconds() / 60, 2) + self._data['last0_start_date'] = self._data['clean_details_last0'].start.strftime("%d.%m.%Y") + self._data['last0_start_time'] = self._data['clean_details_last0'].start.strftime("%H:%M") + self._data['last0_end_date'] = self._data['clean_details_last0'].start.strftime("%d.%m.%Y") + self._data['last0_end_time'] = (self._data['clean_details_last0'].start+self._data['clean_details_last0'].duration).strftime("%H:%M") + + self._data['clean_details_last1'] = self.vakuum.clean_details(self._data['clean_ids'][1],return_list=False) + self._data['last1_area'] = round(self._data['clean_details_last1'].area,2) + self._data['last1_complete'] = self._data['clean_details_last1'].complete + self._data['last1_duration'] = round(self._data['clean_details_last1'].duration.total_seconds() / 60, 2) + self._data['last1_start_date'] = self._data['clean_details_last1'].start.strftime("%d.%m.%Y") + self._data['last1_start_time'] = self._data['clean_details_last1'].start.strftime("%H:%M") + self._data['last1_end_date'] = self._data['clean_details_last1'].start.strftime("%d.%m.%Y") + self._data['last1_end_time'] = (self._data['clean_details_last1'].start+self._data['clean_details_last1'].duration).strftime("%H:%M") + + self._data['clean_details_last2'] = self.vakuum.clean_details(self._data['clean_ids'][2],return_list=False) + self._data['last2_area'] = round(self._data['clean_details_last2'].area,2) + self._data['last2_complete'] = self._data['clean_details_last2'].complete + self._data['last2_duration'] = round(self._data['clean_details_last2'].duration.total_seconds() / 60, 2) + self._data['last2_start_date'] = self._data['clean_details_last2'].start.strftime("%d.%m.%Y") + self._data['last2_start_time'] = self._data['clean_details_last2'].start.strftime("%H:%M") + self._data['last2_end_date'] = self._data['clean_details_last2'].start.strftime("%d.%m.%Y") + self._data['last2_end_time'] = (self._data['clean_details_last2'].start+self._data['clean_details_last2'].duration).strftime("%H:%M") + + self._data['clean_details_last3'] = self.vakuum.clean_details(self._data['clean_ids'][3],return_list=False) + self._data['last3_area'] = round(self._data['clean_details_last3'].area,2) + self._data['last3_complete'] = self._data['clean_details_last3'].complete + self._data['last3_duration'] = round(self._data['clean_details_last3'].duration.total_seconds() / 60, 2) + self._data['last3_start_date'] = self._data['clean_details_last3'].start.strftime("%d.%m.%Y") + self._data['last3_start_time'] = self._data['clean_details_last3'].start.strftime("%H:%M") + self._data['last3_end_date'] = self._data['clean_details_last3'].start.strftime("%d.%m.%Y") + self._data['last3_end_time'] = (self._data['clean_details_last3'].start+self._data['clean_details_last3'].duration).strftime("%H:%M") + + self.logger.debug("Xiaomi_Robvac: Historische id1 {}, id2{}, id3 {}".format( + self._data['clean_details_last0'], self._data['clean_details_last1'], self._data['clean_details_last2'])) - - self.logger.debug("Xiaomi_Robvac: Clean Run complete id1 {}, id2{}, id3 {}".format(self._data['last1_complete'], - self._data['last2_complete'], - self._data['last3_complete'],)) + + self.logger.debug("Xiaomi_Robvac: Clean Run complete id1 {}, id2{}, id3 {}".format( + self._data['last1_complete'], + self._data['last2_complete'], + self._data['last3_complete'],)) carpet_mode = self.vakuum.carpet_mode() self._data['carpetmode_high'] = carpet_mode.current_high - self._data['carpetmode_integral'] = carpet_mode.current_integral + self._data['carpetmode_integral'] = carpet_mode.current_integral self._data['carpetmode_low'] = carpet_mode.current_low self._data['carpetmode_enabled'] = carpet_mode.enabled self._data['carpetmode_stall_time'] = carpet_mode.stall_time - self.logger.debug("Xiaomi_Robvac: Carpet Mode high: {}, integral: {}, low: {}, enabled: {}, , stall_time: {}".format(self._data['carpetmode_high'], - self._data['carpetmode_integral'], - self._data['carpetmode_low'], - self._data['carpetmode_enabled'], - self._data['carpetmode_stall_time'])) - + self.logger.debug("Xiaomi_Robvac: Carpet Mode high: {}, integral: {}, low: {}, " + "enabled: {}, stall_time: {}".format( + self._data['carpetmode_high'], + self._data['carpetmode_integral'], + self._data['carpetmode_low'], + self._data['carpetmode_enabled'], + self._data['carpetmode_stall_time'])) + #status - self._data['serial'] = self.vakuum.serial_number() - self._data['vol'] = self.vakuum.sound_volume() + self._data['serial'] = self.vakuum.serial_number() + self._data['volume'] = self.vakuum.sound_volume() self._data['dnd_status'] = self.vakuum.dnd_status().enabled - self._data['dnd_start'] = self.vakuum.dnd_status().start - self._data['dnd_end'] = self.vakuum.dnd_status().end - self.logger.debug("Xiaomi_Robvac: Serial{}, vol {}, dnd status {}, dnd start {},dnd end {},".format(self._data['serial'], - self._data['vol'], - self._data['dnd_status'], - self._data['dnd_start'], - self._data['dnd_end'])) - - self._data['device_group'] = self.vakuum.get_device_group() - self._data['segment_status'] = self.vakuum.get_segment_status() - self._data['fanspeed'] = self.vakuum.status().fanspeed - self._data['batt'] = self.vakuum.status().battery - self._data['area'] = round(self.vakuum.status().clean_area,2) - self._data['cleantime'] = self.vakuum.status().total_seconds() // 3600 - self._data['aktiv'] = self.vakuum.status().is_on #reinigt? - self._data['zone_cleaning'] = self.vakuum.status().in_zone_cleaning #reinigt? - self._data['is_error'] = self.vakuum.status().got_error - self.logger.debug("Xiaomi_Robvac: segment_status, fanspeed {},batt {}, area {}, cleantime {}, aktiv {} zonen_reinigung {} , device group{}".format(self._data['segment_status'], self._data['fanspeed'], - self._data['batt'], - self._data['area'], - self._data['cleantime'], - self._data['aktiv'], - self._data['zone_cleaning'], + self._data['dnd_start'] = self.vakuum.dnd_status().start + self._data['dnd_end'] = self.vakuum.dnd_status().end + self.logger.debug("Xiaomi_Robvac: Serial{}, vol {}, dnd status {}, dnd start {},dnd end {},".format( + self._data['serial'], + self._data['volume'], + self._data['dnd_status'], + self._data['dnd_start'], + self._data['dnd_end'])) + + self._data['device_group'] = self.vakuum.get_device_group() + self._data['segment_status'] = self.vakuum.get_segment_status() + self._data['fanspeed'] = self.vakuum.status().fanspeed + self._data['batt'] = self.vakuum.status().battery + self._data['battery'] = self.vakuum.status().battery + self._data['area'] = round(self.vakuum.status().clean_area,2) + self._data['clean_time'] = self.vakuum.status().clean_time.total_seconds() / 60 + self._data['active'] = self.vakuum.status().is_on #reinigt? + self._data['zone_cleaning'] = self.vakuum.status().in_zone_cleaning #reinigt? + self._data['is_error'] = self.vakuum.status().got_error + self.logger.debug("Xiaomi_Robvac: segment_status {}, fanspeed {}, battery {}, area {}, " + "clean_time {}, active {}, zonen_reinigung {}, device group {}".format( + self._data['segment_status'], + self._data['fanspeed'], + self._data['battery'], + self._data['area'], + self._data['clean_time'], + self._data['active'], + self._data['zone_cleaning'], self._data['device_group'])) self._data['error'] = self.vakuum.status().error_code - self._data['pause'] = self.vakuum.status().is_paused #reinigt? - self._data['status'] = self.vakuum.status().state #status charging - self._data['timer'] = self.vakuum.timer()#[self.vakuum.timer()[0]['id'], self.vakuum.timer()[0]['action'], self.vakuum.timer()[0]['enabled'], self.vakuum.timer()[0]['ts']] + self._data['pause'] = self.vakuum.status().is_paused #reinigt? + self._data['state'] = self.vakuum.status().state #status charging + self._data['timer'] = self.vakuum.timer() + #[self.vakuum.timer()[0]['id'], self.vakuum.timer()[0]['action'], self.vakuum.timer()[0]['enabled'], self.vakuum.timer()[0]['ts']] self._data['timezone'] = self.vakuum.timezone() - - #bekannet States: Charging, Pause, Charging Disconnected - if self._data['status'] == 'Charging': + + #bekannet States: Charging, Pause, Charging Disconnected + if self._data['state'] == 'Charging': self._data['charging'] = True else: self._data['charging'] = False #->2018-12-26 11:10:37 DEBUG plugins.xiaomi_vac Xiaomi_Robvac: Lese batt 100 area0.0 time 0:00:15 status False stateCharging - self.logger.debug("Xiaomi_Robvac: error {}, pause {}, status{} , timer {}, timezone{}".format( self._data['error'], - self._data['pause'], - self._data['status'], - self._data['timer'], - self._data['timezone'])) + self.logger.debug("Xiaomi_Robvac: error {}, pause {}, status {} , timer {}, timezone {}".format( + self._data['error'], + self._data['pause'], + self._data['state'], + self._data['timer'], + self._data['timezone'])) #buerste #consumable_status() self._data['sensor_dirty'] = self.vakuum.consumable_status().sensor_dirty.total_seconds() // 3600 - self._data['sensor_dirty_left'] = self.vakuum.consumable_status().sensor_dirty_left.total_seconds()// 3600 + self._data['sensor_dirty_left'] = self.vakuum.consumable_status().sensor_dirty_left.total_seconds() // 3600 self._data['side_brush'] = self.vakuum.consumable_status().side_brush.total_seconds() // 3600 self._data['side_brush_left'] = self.vakuum.consumable_status().side_brush_left.total_seconds() // 3600 self._data['main_brush'] = self.vakuum.consumable_status().main_brush.total_seconds() // 3600 self._data['main_brush_left'] = self.vakuum.consumable_status().main_brush_left.total_seconds() // 3600 self._data['filter'] = self.vakuum.consumable_status().filter.total_seconds() // 3600 self._data['filter_left'] = self.vakuum.consumable_status().filter_left.total_seconds() // 3600 - self.logger.debug("Xiaomi_Robvac: buerste seite {0}/{1} Buerste Haupt {2}/{3} filter{4}/{5}".format(self._data['side_brush'], - self._data['side_brush_left'], - self._data['main_brush'], - self._data['main_brush_left'], - self._data['filter'], - self._data['filter_left'])) + self.logger.debug("Xiaomi_Robvac: Buerste Seite {0}/{1}, Buerste Haupt {2}/{3}, Filter{4}/{5}, Sensor{6}/{7}".format( + self._data['side_brush'], + self._data['side_brush_left'], + self._data['main_brush'], + self._data['main_brush_left'], + self._data['filter'], + self._data['filter_left'], + self._data['sensor_dirty'], + self._data['sensor_dirty_left'])) self.logger.debug("Xiaomi_Robvac:{}".format(self._data)) except Exception as e: self.logger.error("Xiaomi_Robvac: Error {}".format(e)) self._connected = False - + for x in self._data: if x in self.messages: self.logger.debug("Xiaomi_Robvac: Update item {1} mit key {0} = Wert {2}".format(x, self.messages[x], self._data[x])) item = self.messages[x] item(self._data[x], 'Xiaomi Robovac') - + # ---------------------------------------------------------------------------------------------- # Befehl senden, wird aufgerufen wenn sich item mit robvac ändert! @@ -265,14 +280,14 @@ def update_item(self, item, caller=None, source=None, dest=None): # message = item.conf['robvac'] if self.has_iattr(item.conf, 'robvac'): #bei boolischem item Item zurücksetzen, damit enforce_updates nicht nötig! - + message = self.get_iattr_value(item.conf, 'robvac') - self.logger.debug("Xiaomi_Robvac: Tu dies und das ! Keyword {0} , weil item {1} geändert wurde ".format(message, item)) - + self.logger.debug("Xiaomi_Robvac: Keyword {0}, item {1} wurde geändert".format(message, item)) + if message == 'fanspeed': self.vakuum.set_fan_speed(item()) - self.logger.debug("Xiaomi_Robvac: {0} geaendert wurde ".format(self.vakuum.fan_speed())) - elif message == 'vol': + self.logger.debug("Xiaomi_Robvac: {0} wurde geaendert.".format(self.vakuum.fan_speed())) + elif message == 'volume': if item() > 100: vol = 100 else: @@ -304,15 +319,15 @@ def update_item(self, item, caller=None, source=None, dest=None): self.vakuum.find() elif message == "reset_filtertimer": if item() == True: - item(False, 'Robvac') + item(False, 'Robvac') self.vakuum.reset_consumable() elif message == "disable_dnd": if item() == True: item(False, 'Robvac') self.vakuum.disable_dnd() - + elif message == "set_dnd": - + if item() == True: item(False, 'Robvac') #start_hr, start_min, end_hr, end_min @@ -320,19 +335,29 @@ def update_item(self, item, caller=None, source=None, dest=None): elif message == "clean_zone": #Clean zones. :param List zones: List of zones to clean: [[x1,y1,x2,y2, iterations],[x1,y1,x2,y2, iterations]] self.vakuum.zoned_clean(item()[0], item()[1],item()[2], item()[3], item()[4]) - elif message == "go_to": + elif message == "go_to": self.vakuum.goto(item()[0], item()[1]) elif message == "create_nogo_zones": self.vakuum.create_nogo_zone(item()[0], item()[1]) - pass - + elif message == "reset": + if item().lower() == "sensor_dirty" or item().lower() == "sensor_reinigen": + self.vakuum.consumable_reset(miio.vacuum.Consumable.SensorDirty) + elif item().lower() == "main_brush" or item().lower() == "buerste_haupt": + self.vakuum.consumable_reset(miio.vacuum.Consumable.MainBrush) + elif item().lower() == "side_brush" or item().lower() == "buerste_seite": + self.vakuum.consumable_reset(miio.vacuum.Consumable.SideBrush) + elif item().lower() == "filter": + self.vakuum.consumable_reset(miio.vacuum.Consumable.Filter) + else: + self.logger.warning("Consumable {} does not exit. Please use only sensor_dirty/sensor_reinigen, main_brush/buerste_haupt, side_brush/buerste_seite, filter.".format(item())) + def run(self): self.alive = True self.logger.debug("Xiaomi_Robvac: Found items{}".format(self.messages)) - + def stop(self): self.alive = False - + def parse_item(self, item): if self.has_iattr(item.conf, 'robvac'): message = self.get_iattr_value(item.conf, 'robvac') @@ -340,17 +365,16 @@ def parse_item(self, item): if not message in self.messages: self.messages[message] = item - + return self.update_item def update_item_read(self, item, caller=None, source=None, dest=None): if self.has_iattr(item.conf, 'robvac'): for message in item.get_iattr_value(item.conf, 'robvac'): - #for message in item.conf['robvac']: # send status update self.logger.debug("Xiaomi_Robvac: update_item_read {0}".format(message)) # ------------------------------------------ # Webinterface Methoden -# ------------------------------------------ +# ------------------------------------------ def get_connection_info(self): info = {} @@ -359,7 +383,7 @@ def get_connection_info(self): info['cycle'] = self._cycle info['connected'] = self._connected return info - + def init_webinterface(self): """" Initialize the web interface for this plugin @@ -386,7 +410,7 @@ def init_webinterface(self): 'tools.staticdir.dir': 'static' } } - + self.logger.debug("Plugin '{0}': {1}, {2}, {3}, {4}, {5}".format(self.get_shortname(), webif_dir, self.get_shortname(),config, self.get_classname(), self.get_instance_name())) # Register the web interface as a cherrypy app self.mod_http.register_webif(WebInterface(webif_dir, self), @@ -423,7 +447,7 @@ def __init__(self, webif_dir, plugin): self.tplenv = self.init_template_environment() self.logger.debug("Plugin : Init Webif") self.items = Items.get_instance() - + @cherrypy.expose def index(self, reload=None): """ @@ -437,11 +461,10 @@ def index(self, reload=None): plgitems.append(item) self.logger.debug("Plugin : Render index Webif") tmpl = self.tplenv.get_template('index.html') - return tmpl.render(plugin_shortname=self.plugin.get_shortname(), + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), p=self.plugin, connection = self.plugin.get_connection_info(), webif_dir = self.webif_dir , items=sorted(plgitems, key=lambda k: str.lower(k['_path']))) - diff --git a/xiaomi_vac/locale.yaml b/xiaomi_vac/locale.yaml new file mode 100755 index 000000000..8ae70a21d --- /dev/null +++ b/xiaomi_vac/locale.yaml @@ -0,0 +1,10 @@ +plugin_translations: + # Translations for the plugin specially for the web interface + 'vorgegebene Zykluszeit': {'de': '=', 'en': 'Defined cycle time'} + 'Verbunden': {'de': '=', 'en': 'Connected'} + 'Wert': {'de': '=', 'en': 'Value'} + 'Typ': {'de': '=', 'en': 'Type'} + 'Visu Zugriff': {'de': '=', 'en': 'Visu Access'} + 'Host': {'de': '=', 'en': '='} + 'Token': {'de': '=', 'en': '='} + 'Item': {'de': '=', 'en': '='} diff --git a/xiaomi_vac/plugin.yaml b/xiaomi_vac/plugin.yaml index 5820455ed..480a73a9f 100644 --- a/xiaomi_vac/plugin.yaml +++ b/xiaomi_vac/plugin.yaml @@ -1,23 +1,30 @@ # Metadata for the Xiaomi Saugroboter plugin: # Global plugin attributes - type: gateway # plugin type (gateway, interface, protocol, system, web) - description: # Alternative: description in multiple languages - de: 'Zugriff auf Xiaomi Saugroboter' - en: 'Control the Xioami Vacuum Robot' + type: interface # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Zugriff und Steuern eines Xiaomi Saugroboters' + en: 'Control a Xioami Vacuum Robot' maintainer: Bonze - state: develop - tester: Bonze, henfri + state: ready + tester: Bonze, henfri, onkelandy keywords: communication, iot + requirements: + de: 'python-miio python Modul' + en: 'python-miio python module' + requirements_long: + de: 'Das Plugin benötigt das python-miio Modul. Die richtige Version wird automatisch beim Start von smarthomeNG installiert.\n + Die Bibliothek benötigt den Token des Roboters zur Kommunikation. Hierzu ist bitte die `Anleitung auf github `_ zu befolgen. + ' + en: 'This plugin needs the python-miio module. The correct version is installed automatically at the smarthomeNG start.\n + The module needs the token of the robot for communication. Please follow the `instructions on github `_. + ' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1453597-support-thread-f%C3%BCr-xiaomi-saugroboter-plugin -# documentation: version: 1.0.0 # Plugin version sh_minversion: 1.4 # minimum shNG version to use this plugin multi_instance: False # plugin supports multi instance classname: Robvac # class containing the plugin restartable: unknown - - plugin_functions: NONE # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) @@ -29,275 +36,393 @@ logic_parameters: NONE # Definition of parameters to be configured in etc/plugin.yaml parameters: - ip: - de: 'IP Adresse des Roboters ' - en: 'IP Adress of the Vakuum' - read_cycle: - de: 'Standart Zyklus 12' - en: 'default cycle time 12' - token: - de: 'Token für die Kommunikation' - en: 'Token for Communication' - + ip: + type: ipv4 + mandatory: True + description: + de: 'IP Adresse des Saug-Roboters ' + en: 'IP Adress of the Vakuum Cleaner' + read_cycle: + type: int + default: 12 + description: + de: 'Standart Zyklus zum Lesen der Paramter vom Gerät' + en: 'Default cycle time for reading' + token: + type: str + madnatory: True + description: + de: 'Token für die Kommunikation' + en: 'Token for Communication' + +item_attributes: + # Definition of item attributes defined by this plugin + robvac: + type: str + description: + de: 'Befehl oder Abfrage, die an den Roboter gesendet werden soll.' + en: 'Command or query to be sent to the robot.' + description_long: + de: '**Befehl oder Abfrage, die an den Roboter gesendet werden soll.**\n + Die Befehle richten sich nach der python-miio Bibliothek. Es wird empfohlen, + die im Plugin vorhandenen Befehle über ``struct: xiaomi_vac.saugroboter`` einzubinden. + ' + en: '**Command or query to be sent to the robot.**\n + The commands are based on the python-miio module. It is strongly recommended to implement + the commands supported by the plugin via ``struct: xiaomi_vac.saugroboter``. + ' + item_structs: #saugroboter -saugroboter: - live: - name: 'Aktuelle Werte des Saugroboters' - batterie_status: - name: 'Batterie Status' - type: num - robvac: 'batt' - reinigt: - name: 'aktuell am reinigen?' - type: bool - robvac: 'state' - zustand: - name: 'Betriebszustand' + saugroboter: + name: 'Vorlage-Struktur für die Ansteuerung eines Xiaomi Saugroboters' + serial: type: str - robvac: 'status' - aktiv: - type: bool - robvac: 'aktiv' - flaeche: - name: 'gereinigte Fläche' - type: num - robvac: 'area' - gereinigt: - type: num - robvac: 'gereinigt' - reinigungszeit: - type: num - robvac: 'cleantime' - carpetmode_high: - type: num - robvac: 'carpetmode_high' - carpetmode_integral: - type: num - robvac: 'carpetmode_integral' - carpetmode_low: - type: num - robvac: 'carpetmode_low' - carpetmode_enabled: - type: num - robvac: 'carpetmode_enabled' - carpetmode_stall_time: - type: num - robvac: 'carpetmode_stall_time' - errorcode: - type: num - robvac: 'error' - error: - type: bool - robvac: 'is_error' - dnd_status: - type: bool - robvac: 'dnd_status' - dnd_start: - type: foo - robvac: 'dnd_start' - dnd_end: - type: foo - robvac: 'dnd_end' - zonen_reinigung: - type: bool - robvac: 'zone_cleaning' - pausiert: - type: bool - robvac: 'pause' - segment_status: - type: bool - robvac: 'segment_status' - befehl: - name: 'Befehle zum Saugroboter' - start: - type: bool - robvac: 'set_start' - stop: - type: bool - robvac: 'set_stop' - pause: - type: bool - robvac: 'set_pause' - basis: - type: bool - robvac: 'set_home' - spot_cleaner: - type: bool - robvac: 'set_spot' - find: - type: bool - robvac: 'set_find' - reset_filtertimer: - type: bool - robvac: 'reset_filtertimer' - disable_dnd: - type: bool - robvac: 'disbale_dnd' - set_dnd: - type: list - robvac: 'set_dnd' - goto: - type: list - robvac: 'go_to' - statistik: - name: 'Reinigungsstatistiken' - anzahl_reinigungen: - type: num - robvac: 'clean_total_count' - gereinigte_flaeche: - type: num - robvac: 'clean_total_area' - gesamtlaufzeit: - type: num - robvac: 'clean_total_duration' - reinigungsids: - type: foo - robvac: 'clean_ids' - reinigungsdetails0: - type: foo - robvac: 'clean_details_last0' - flaeche: - name: 'gereinigte Fläche' + robvac: 'serial' + visu_acl: ro + struct: + - xiaomi_vac.live + - xiaomi_vac.befehl + - xiaomi_vac.statistik + - xiaomi_vac.einstellungen + - xiaomi_vac.stundenzaehler + + live: + live: + name: 'Aktuelle Werte des Saugroboters' + batterie_status: + name: 'Batterie Status' type: num - robvac: 'last0_area' - complete: + robvac: 'battery' + visu_acl: ro + reinigt: + name: 'aktuell am reinigen?' type: bool - robvac: 'last0_complete' - reinigungszeit: - type: num - robvac: 'last0_duration' - start_date: + robvac: 'active' + visu_acl: ro + zustand: + name: 'Betriebszustand' type: str - robvac: 'last0_start_date' - start_time: - type: str - robvac: 'last0_start_time' - end_date: - type: str - robvac: 'last0_end_date' - end_time: - type: str - robvac: 'last0_end_time' - reinigungsdetails1: - type: foo - robvac: 'clean_details_last1' + robvac: 'state' + visu_acl: ro flaeche: name: 'gereinigte Fläche' type: num - robvac: 'last1_area' - complete: - type: bool - robvac: 'last1_complete' + robvac: 'area' + visu_acl: ro reinigungszeit: type: num - robvac: 'last1_duration' - start_date: - type: str - robvac: 'last1_start_date' - start_time: - type: str - robvac: 'last1_start_time' - end_date: - type: str - robvac: 'last1_end_date' - end_time: - type: str - robvac: 'last1_end_time' - reinigungsdetails2: - type: foo - robvac: 'clean_details_last2' - flaeche: - name: 'gereinigte Fläche' + robvac: 'clean_time' + visu_acl: ro + carpetmode_high: + type: num + robvac: 'carpetmode_high' + visu_acl: ro + carpetmode_integral: + type: num + robvac: 'carpetmode_integral' + visu_acl: ro + carpetmode_low: + type: num + robvac: 'carpetmode_low' + visu_acl: ro + carpetmode_enabled: type: num - robvac: 'last2_area' - complete: + robvac: 'carpetmode_enabled' + visu_acl: ro + carpetmode_stall_time: + type: num + robvac: 'carpetmode_stall_time' + visu_acl: ro + errorcode: + type: num + robvac: 'error' + visu_acl: ro + error: type: bool - robvac: 'last2_complete' - reinigungszeit: + robvac: 'is_error' + visu_acl: ro + dnd_status: + type: bool + robvac: 'dnd_status' + visu_acl: ro + dnd_start: + type: foo + robvac: 'dnd_start' + visu_acl: ro + dnd_end: + type: foo + robvac: 'dnd_end' + visu_acl: ro + zonen_reinigung: + type: bool + robvac: 'zone_cleaning' + visu_acl: rw + pausiert: + type: bool + robvac: 'pause' + visu_acl: ro + segment_status: + type: list + robvac: 'segment_status' + visu_acl: ro + befehl: + befehl: + name: 'Befehle zum Saugroboter' + start: + type: bool + robvac: 'set_start' + visu_acl: rw + stop: + type: bool + robvac: 'set_stop' + visu_acl: rw + pause: + type: bool + robvac: 'set_pause' + visu_acl: rw + basis: + type: bool + robvac: 'set_home' + visu_acl: rw + spot_cleaner: + type: bool + robvac: 'set_spot' + visu_acl: rw + find: + type: bool + robvac: 'set_find' + visu_acl: rw + reset_filtertimer: + type: bool + robvac: 'reset_filtertimer' + visu_acl: rw + disable_dnd: + type: bool + robvac: 'disbale_dnd' + visu_acl: rw + set_dnd: + type: list + robvac: 'set_dnd' + visu_acl: rw + goto: + type: list + robvac: 'go_to' + visu_acl: rw + statistik: + statistik: + name: 'Reinigungsstatistiken' + anzahl_reinigungen: type: num - robvac: 'last2_duration' - start_date: - type: str - robvac: 'last2_start_date' - start_time: - type: str - robvac: 'last2_start_time' - end_date: - type: str - robvac: 'last2_end_date' - end_time: - type: str - robvac: 'last2_end_time' - reinigungsdetails3: - type: foo - robvac: 'clean_details_last3' - flaeche: - name: 'gereinigte Fläche' + robvac: 'clean_total_count' + visu_acl: ro + gereinigte_flaeche: + type: num + robvac: 'clean_total_area' + visu_acl: ro + gesamtlaufzeit: + type: num + robvac: 'clean_total_duration' + visu_acl: ro + reinigungsids: + type: foo + robvac: 'clean_ids' + visu_acl: ro + reinigungsdetails0: + type: foo + robvac: 'clean_details_last0' + visu_acl: ro + flaeche: + name: 'gereinigte Fläche' + type: num + robvac: 'last0_area' + visu_acl: ro + complete: + type: bool + robvac: 'last0_complete' + visu_acl: ro + reinigungszeit: + type: num + robvac: 'last0_duration' + visu_acl: ro + start_date: + type: str + robvac: 'last0_start_date' + visu_acl: ro + start_time: + type: str + robvac: 'last0_start_time' + visu_acl: ro + end_date: + type: str + robvac: 'last0_end_date' + visu_acl: ro + end_time: + type: str + robvac: 'last0_end_time' + visu_acl: ro + reinigungsdetails1: + type: foo + robvac: 'clean_details_last1' + visu_acl: ro + flaeche: + name: 'gereinigte Fläche' + type: num + robvac: 'last1_area' + visu_acl: ro + complete: + type: bool + robvac: 'last1_complete' + visu_acl: ro + reinigungszeit: + type: num + robvac: 'last1_duration' + visu_acl: ro + start_date: + type: str + robvac: 'last1_start_date' + visu_acl: ro + start_time: + type: str + robvac: 'last1_start_time' + visu_acl: ro + end_date: + type: str + robvac: 'last1_end_date' + visu_acl: ro + end_time: + type: str + robvac: 'last1_end_time' + visu_acl: ro + reinigungsdetails2: + type: foo + robvac: 'clean_details_last2' + visu_acl: ro + flaeche: + name: 'gereinigte Fläche' + type: num + robvac: 'last2_area' + visu_acl: ro + complete: + type: bool + robvac: 'last2_complete' + visu_acl: ro + reinigungszeit: + type: num + robvac: 'last2_duration' + visu_acl: ro + start_date: + type: str + robvac: 'last2_start_date' + visu_acl: ro + start_time: + type: str + robvac: 'last2_start_time' + visu_acl: ro + end_date: + type: str + robvac: 'last2_end_date' + visu_acl: ro + end_time: + type: str + robvac: 'last2_end_time' + visu_acl: ro + reinigungsdetails3: + type: foo + robvac: 'clean_details_last3' + visu_acl: ro + flaeche: + name: 'gereinigte Fläche' + type: num + robvac: 'last3_area' + visu_acl: ro + complete: + type: bool + robvac: 'last3_complete' + visu_acl: ro + reinigungszeit: + type: num + robvac: 'last3_duration' + visu_acl: ro + start_date: + type: str + robvac: 'last3_start_date' + visu_acl: ro + start_time: + type: str + robvac: 'last3_start_time' + visu_acl: ro + end_date: + type: str + robvac: 'last3_end_date' + visu_acl: ro + end_time: + type: str + robvac: 'last3_end_time' + visu_acl: ro + einstellungen: + einstellungen: + name: 'Einstellbare Parameter' + luefter_speed: type: num - robvac: 'last3_area' - complete: + robvac: 'fanspeed' + visu_acl: rw + teppichmodus: type: bool - robvac: 'last3_complete' - reinigungszeit: + robvac: 'carpet_mode' + visu_acl: rw + volume: type: num - robvac: 'last3_duration' - start_date: - type: str - robvac: 'last3_start_date' - start_time: - type: str - robvac: 'last3_start_time' - end_date: + robvac: 'volume' + visu_acl: rw + dnd: + type: bool + robvac: 'dnd_onoff' + visu_acl: rw + timezone: type: str - robvac: 'last3_end_date' - end_time: + robvac: 'timezone' + visu_acl: rw + timer: + type: foo + robvac: 'timer' + visu_acl: rw + reset: + remark: Use these values to reset consumables - main_brush, side_brush, filter, sensor_dirty type: str - robvac: 'last3_end_time' - einstellungen: - name: 'Einstellbare Parameter' - luefter_speed: - type: num - robvac: 'fanspeed' - teppichmodus: - type: bool - robvac: 'carpet_mode' - vol: - type: num - robvac: 'vol' - dnd: - type: bool - robvac: 'dnd_onoff' - serial: - type: str - robvac: 'serial' - timezone: - type: str - robvac: 'timezone' - timer: - type: foo - robvac: 'timer' + robvac: 'reset' + visu_acl: rw + enforce_updates: True stundenzaehler: - buerste_seite: - type: num - robvac: 'side_brush' - buerste_seite_verbleibend: - type: num - robvac: 'side_brush_left' - buerste_haupt: - type: num - robvac: 'main_brush' - buerste_haupt_verbleibend: - type: num - robvac: 'main_brush_left' - filter: - type: num - robvac: 'filter' - filter_verbleibend: - type: num - robvac: 'filter_left' - sensor_reinigen: - type: num - robvac: 'sensor_dirty' - sensor_reinigen_verbleibend: - type: num - robvac: 'sensor_dirty_left' + stundenzaehler: + buerste_seite: + type: num + robvac: 'side_brush' + visu_acl: ro + buerste_seite_verbleibend: + type: num + robvac: 'side_brush_left' + visu_acl: ro + buerste_haupt: + type: num + robvac: 'main_brush' + visu_acl: ro + buerste_haupt_verbleibend: + type: num + robvac: 'main_brush_left' + visu_acl: ro + filter: + type: num + robvac: 'filter' + visu_acl: ro + filter_verbleibend: + type: num + robvac: 'filter_left' + visu_acl: ro + sensor_reinigen: + type: num + robvac: 'sensor_dirty' + visu_acl: ro + sensor_reinigen_verbleibend: + type: num + robvac: 'sensor_dirty_left' + visu_acl: ro diff --git a/xiaomi_vac/requirements.txt b/xiaomi_vac/requirements.txt index 6bc35ef52..bf915d485 100644 --- a/xiaomi_vac/requirements.txt +++ b/xiaomi_vac/requirements.txt @@ -1,3 +1,2 @@ -setuptools python-miio==0.4.6;python_version<'3.6' -python-miio;python_version>='3.6' +python-miio>=0.4.7;python_version>='3.6' diff --git a/xiaomi_vac/user_doc.rst b/xiaomi_vac/user_doc.rst new file mode 100755 index 000000000..d0f984c3f --- /dev/null +++ b/xiaomi_vac/user_doc.rst @@ -0,0 +1,68 @@ +.. index:: Plugins; xiamo_vac +.. index:: xiamo_vac + +xiamo_vac +######### + +Konfiguration +============= + +.. important:: + + Detaillierte Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/xiaomi_vac` zu finden. + + +Das Plugin basiert auf der `python-miio `_ Bibliothek. Zuerst muss gemäß `Anleitung auf der github Seite `_ des python-miio Moduls das Token eures Roboters eruiert werden. +Im Anschluss macht es Sinn, die Kommunikation mit dem Roboter in der Kommandozeile eures Rechners zu testen: + +.. code-block:: console + + export MIROBO_IP=192.xxx.xxx.xxx + export MIROBO_TOKEN=euerToken + mirobo + +Die Ausgabe sollte in etwa so aussehen: + +.. code-block:: console + + > State: Charging + > Battery: 100 + > Fanspeed: 60 + > Cleaning since: 0:00:00 + > Cleaned area: 0.0 m² + > DND enabled: 0 + > Map present: 1 + > in_cleaning: 0 + +Funktionen +========== + +Folgende Werte/Funktionen können vom Saugroboter ausgelesen bzw. gestartet werden: + +- Start + +- Stop/ zur Ladestation fahren + +- Pause + +- Finden + +- Spotreinigung + +- Lüftergesschwindigkeit ändern + +- Lautstärke ansage ändern + +- gerenigte Fläche + +- Reinigungszeit + +- Status + +und viele mehr. Es wird empfohlen, die Items durch das ``struct`` Template **saugroboter** des Plugins automatisch zu implementieren. + + +Webinterface +============ + +Das Webinterface bietet einen schnelle und einfachen Überblick über die Statusinformationen des Roboters. diff --git a/xiaomi_vac/widget/139_vr200-vis.png b/xiaomi_vac/widget/139_vr200-vis.png deleted file mode 100644 index 533405c11..000000000 Binary files a/xiaomi_vac/widget/139_vr200-vis.png and /dev/null differ diff --git a/yamahayxc/README.md b/yamahayxc/README.md index acb1f9f5a..6aaac3946 100644 --- a/yamahayxc/README.md +++ b/yamahayxc/README.md @@ -17,6 +17,8 @@ The 'update' item has no own function. Writing to this item triggers a pull of a Feel free to comment and to file issues. At the moment only the main zone and the netusb player are supported. Support for multiple zones should be possible on request. For other players supply test equipment :) +Starting with sh.py release v1.7 item structs are available for basic media players and for devices with additional alarm clock functions. + ## Requirements @@ -60,6 +62,17 @@ yamahayxc: ### items.yaml +```yaml +media: + + wx010: + struct: yamahayxc.basic + + yamahayxc_host: 192.168.2.211 +``` + +or without structs (results in identical item tree): + ```yaml media: @@ -150,6 +163,29 @@ media: yamahayxc_cmd: state enforce_updates: 'True' + + +# the following items are only valid for devices with alarm clock functions +# these are included in addition to the others from the 'alarm' struct: + + # enable / disable alarm function + alarm_on: + type: bool + yamahayxc_cmd: alarm_on + enforce_updates: 'True' + + # enable / disable alarm beep (solo or in addition to music) + alarm_beep: + type: bool + yamahayxc_cmd: alarm_beep + enforce_updates: 'True' + + # get/set alarm time. Formatted as 4 digit 24 hour string + alarm_time: + type: str + yamahayxc_cmd: alarm_time + enforce_updates: 'True' + ``` ### Example CLI usage @@ -163,4 +199,7 @@ media: > up media.wx010.playback=play > up media.wx010.power=False > up media.wx010.passthru='v1/Main/setPower?power=off' +> up media.wx010.alarm_time='1430' ``` + +Note: I cheated. The wx-010 doesn't have alarm functions... \ No newline at end of file diff --git a/yamahayxc/__init__.py b/yamahayxc/__init__.py index a0a46fce5..db637283a 100644 --- a/yamahayxc/__init__.py +++ b/yamahayxc/__init__.py @@ -27,8 +27,7 @@ # - parse zone().input_list().id -> read possible input values # - parse zone().func_list -> read allowed cmds # - parse zone().range_step -> read range/step for vol / eq -# -# - add alarm clock controls + import logging import requests @@ -54,9 +53,13 @@ class YamahaYXC(SmartPlugin): """ This is the main plugin class YamahaYXC to control YXC-compatible devices. """ - PLUGIN_VERSION = "1.0.5" + PLUGIN_VERSION = "1.0.6" ALLOW_MULTIINSTANCE = False + # + # public functions + # + def __init__(self, smarthome): """ Default init function @@ -68,11 +71,12 @@ def __init__(self, smarthome): # valid commands for use in item configuration 'yamahayxc_cmd = ...' self._yamaha_cmds = ['state', 'power', 'input', 'playback', 'preset', 'volume', 'mute', 'track', 'artist', 'sleep', - 'total_time', 'play_time', 'pos', 'albumart', 'passthru'] + 'total_time', 'play_time', 'pos', 'albumart', + 'alarm_on', 'alarm_time', 'alarm_beep', 'passthru'] # commands to ignore when checking for return values # these commands don't get / can't process (simple) return values - self._yamaha_ignore_cmds_upd = ['state', 'preset'] + self._yamaha_ignore_cmds_upd = ['state', 'preset', 'alarm_on', 'alarm_time', 'alarm_beep'] # store items in 2D-array: # _yamaha_dev [host] [cmd] = item @@ -80,6 +84,7 @@ def __init__(self, smarthome): self._yamaha_dev = {} # store host addresses of devices self._yamaha_hosts = {} + self.srv_port = 41100 self.srv_buffer = 1024 self.sock = None @@ -90,7 +95,8 @@ def run(self): Default run function Initializes class and sets up UDP listener. - Main loop receives notifications and dispatches item/status updates. + Main loop receives notifications and dispatches item updates + based on notifications and triggers status updates. """ self._sh.trigger('YamahaYXC', self._initialize) self.logger.info("YamahaYXC starting listener") @@ -105,20 +111,21 @@ def run(self): "Error receiving data - host/port not readable") return if host not in list(self._yamaha_dev.keys()): - self.logger.warn( - "Yamaha received notify from unknown host {}".format(host)) - else: - # careful - connected device sends updates every second for - # about 10 minutes without interaction self.logger.debug( - "Yamaha unicast received {} bytes from {}: {}".format( - len(data), host, data)) + "Received notify from unknown host {}".format(host)) + else: + # connected device sends updates every second for + # about 10 minutes without further interaction + # self.logger.debug( + # "Yamaha unicast received {} bytes from {}: {}".format( + # len(data), host, data)) data = json.loads(data.decode('utf-8')) + # need to find lowest-level cmds in nested dicts # nesting is done by current zone and player # for now, assemble all relevant keys (main, netusb) # in non-nested dict. this is quite a kludge as of now... - data_flat = data + data_flat = {} try: data_flat.update(data['main']) except: @@ -129,20 +136,23 @@ def run(self): pass # try all known command words... + # this is relevant e.g. for "play_time" updates or song changes for cmd in self._yamaha_cmds: # found it in data package? if cmd in data_flat: try: # try to get command value and set item - notify_val = self._process_value(data_flat[cmd], cmd, host) + notify_val = self._convert_value_yxc_to_plugin(data_flat[cmd], cmd, host) item = self._yamaha_dev[host][cmd] item(notify_val, "YamahaYXC") except: pass + # device told us new info is available? if 'status_updated' in data_flat or 'play_info_updated' in data_flat: # pull (full) status update from device self._update_state(host) + # log possible play errors if 'play_error' in data_flat: if data_flat['play_error'] > 0: @@ -159,7 +169,84 @@ def stop(self): Stops listener and shuts down plugin """ self.alive = False - self.sock.shutdown(socket.SHUT_RDWR) + try: + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + def parse_item(self, item): + """ + parse all items at startup + + This function is called by the SmartPlugin manager in sh.py for every + item. If item config "yamahayxc_cmd" is present, item ist stored together + with associated host and zone. + Returns update function for the item (update_item()) + """ + if 'yamahayxc_cmd' in item.conf: + yamaha_host = self._lookup_host(item) + self._add_host_info(yamaha_host) + yamaha_zone = self._lookup_zone(item) + self._yamaha_zone = yamaha_zone + yamaha_cmd = item.conf['yamahayxc_cmd'].lower() + if yamaha_cmd not in self._yamaha_cmds: + self.logger.warn("{} not in valid commands: {}".format( + yamaha_cmd, self._yamaha_cmds)) + return None + else: + try: + self._yamaha_dev[yamaha_host][yamaha_cmd] = item + except KeyError: + self._yamaha_dev[yamaha_host] = {} + self._yamaha_dev[yamaha_host][yamaha_cmd] = item + + return self.update_item + + def update_item(self, item, caller=None, source=None, dest=None): + """ + recall function if item is modified in sh.py + + Only for "write" commands: calls function to build cmd string + for given item and runs network query to execute cmd + In any case _update_state is called afterwards to update sh.py items + """ + if caller != "YamahaYXC" and self.alive: + yamaha_cmd = item.conf['yamahayxc_cmd'] + yamaha_host = self._lookup_host(item) + yamaha_payload = None + + if yamaha_cmd == 'power': + yamaha_payload = self._build_cmd_power(item()) + elif yamaha_cmd == 'volume': + yamaha_payload = self._build_cmd_volume(item()) + elif yamaha_cmd == 'mute': + yamaha_payload = self._build_cmd_mute(item()) + elif yamaha_cmd == 'input': + yamaha_payload = self._build_cmd_input(item()) + elif yamaha_cmd == 'playback': + yamaha_payload = self._build_cmd_playback(item()) + elif yamaha_cmd == 'preset': + yamaha_payload = self._build_cmd_preset(item()) + elif yamaha_cmd == "sleep": + yamaha_payload = self._build_cmd_sleep(item()) + elif yamaha_cmd == "alarm_on": + yamaha_payload = self._build_cmd_alarm_on(item()) + elif yamaha_cmd == "alarm_time": + yamaha_payload = self._build_cmd_alarm_time(item()) + elif yamaha_cmd == "alarm_beep": + yamaha_payload = self._build_cmd_alarm_beep(item()) + elif yamaha_cmd == "passthru": + yamaha_payload = item() + # no more check for "update" or "state" -> after updating item + # _update_state is called anyway, would only duplicate update + if yamaha_payload: + self._submit_payload(yamaha_host, yamaha_payload) + self._update_state(yamaha_host) + return None + + # + # initialization functions + # def _initialize(self): """ @@ -171,141 +258,142 @@ def _initialize(self): for yamaha_host in list(self._yamaha_hosts): self.logger.info( "Initializing items for host: {}".format(yamaha_host)) - state = self._update_state(yamaha_host, False) - - def _power(self, value, cmd='PUT'): - """ - return cmd string for "set power" + self._update_state(yamaha_host, False) - value is boolean: - True means "power on" - False means "standby" - """ - if value is True: - cmdarg = 'on' - elif value is False: - cmdarg = 'standby' - cmd = "v1/{}/setPower?power={}".format(self._yamaha_zone, cmdarg) - return cmd - - def _input(self, value, cmd='PUT'): + def _lookup_host(self, item): """ - return cmd string for "set input" + get host IP stored for submitted item - value is string and device-dependent, e.g. - - tuner - - cd - - bluetooth - - net_radio (internet radio stream) - - server (UPNP client) + this does not work with nested items in item config, so don't nest items! """ - cmd = "v1/{}/setInput?input={}".format(self._yamaha_zone, value) - return cmd + parent = item.return_parent() + yamaha_host = socket.gethostbyname(parent.conf['yamahayxc_host']) + return yamaha_host - def _volume(self, value, cmd='PUT'): + def _lookup_zone(self, item): """ - return cmd string for "set volume" + get zone config for item - volume is numeric from 0..60 + again, don't nest items. + This is a stub function in preparation for later extension of the plugin + to multiple zones. Maybe flexible config of multiple zones for items + might become necessary -> rethink this approach then """ - cmd = "v1/{}/setVolume?volume={}".format(self._yamaha_zone, value) - return cmd + parent = item.return_parent() + yamaha_zone = parent.conf['yamahayxc_zone'] + return yamaha_zone - def _mute(self, value, cmd='PUT'): - """ - return cmd string for "set mute" + def _add_host_info(self, host): """ - if value is True: - cmdarg = 'true' - elif value is False: - cmdarg = 'false' - cmd = "v1/{}/setMute?enable={}".format(self._yamaha_zone, cmdarg) - return cmd + store local interface IP for connection to a given host - def _playback(self, value, cmd="PUT"): + just exists in case the server is multihomed. in most cases not necessary """ - return cmd string for "set playback" + try: + local_ip = self._yamaha_hosts[host] + return + except: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect((host, 80)) + local_ip = s.getsockname()[0] + s.close() + self._yamaha_hosts[host] = local_ip - value is string and can be (for netusb) - - play - - stop - - pause - - play_pause (toggle) - - previous - - next - - fast_reverse_start - - fast_reverse_stop - - fast_forward_start - - fast_forward_stop - """ - cmd = "v1/netusb/setPlayback?playback={}".format(value) - return cmd + # + # process not individually accessible items + # - def _get_state(self): - """ - return cmd string for "get status" -> get general status for zone + def _update_state(self, yamaha_host, update_items=True): """ - cmd = "v1/{}/getStatus".format(self._yamaha_zone) - return cmd + retrieve state from device via cmd get_state and get_play_state - def _get_play_state(self): + As many items cannot be queried via yamahayxc by themselves, this + function simply sends two state update commands and joins both + state replies into one dict (this will probably not work with more + than one input source and one zone!) + Then for all relevant commands suitable return values are extracted + and processed (items are updated) + Return None prematurely if no network response was received + Silently ignores invalid (None) values for commands """ - return cmd string for "get playstatus" -> get playing status + state = self._submit_payload(yamaha_host, self._build_cmd_get_state()) + if state is None: + return + state2 = self._submit_payload(yamaha_host, self._build_cmd_get_play_state()) + state.update(state2) - at the moment only netusb zone is supported, no other device to test - """ - cmd = "v1/netusb/getPlayInfo" - return cmd + # retrieving only single items from device is not possible + # so just get everything and update sh.py items + if update_items: + for yamaha_cmd, item in self._yamaha_dev[yamaha_host].items(): + if yamaha_cmd not in self._yamaha_ignore_cmds_upd: + value = self._get_value_from_response(state, yamaha_cmd, yamaha_host) + if value is not None: + item(value, "YamahaYXC") - def _preset(self, value): - """ - return cmd string for "set preset" + # try updating the alarm clock state + self._update_alarm_state(yamaha_host, update_items) - value is integer preset index (1..) + return state - at the moment only netusb zone is supported (see above) + def _update_alarm_state(self, yamaha_host, update_items=True): """ - cmd = "v1/netusb/recallPreset?zone=main&num={}".format(value) - return cmd + parses alarm status from query response 'state' - def _sleep(self, value, cmd='PUT'): + as alarm configuration is too complex to mirror in item configuration, + this is excluded from the loop in _update_state() + As alarm handling might be expanded later, this is a separate function. + At the moment it will only be called from _update_state() """ - return cmd string for "set sleep" + state = self._submit_payload(yamaha_host, self._build_cmd_get_alarm_state()) + if state: + try: + alarm = state["alarm"] + except KeyError: + return + + alarm_on = alarm["alarm_on"] + alarm_time = alarm["oneday"]["time"] + alarm_beep = alarm["oneday"]["beep"] - volume is numeric 0 / 30 / 60 / 90 / 120 (minutes) - """ - cmd = "v1/{}/setSleep?sleep={}".format(self._yamaha_zone, value) - return cmd + if update_items: + for yamaha_cmd, item in self._yamaha_dev[yamaha_host].items(): + if yamaha_cmd == "alarm_on": + item(alarm_on, "YamahaYXC") + if yamaha_cmd == "alarm_time": + item(alarm_time, "YamahaYXC") + if yamaha_cmd == "alarm_beep": + item(alarm_beep, "YamahaYXC") - def _get_value(self, notify_cmd, yamaha_host): - """ - gets value for notify_cmd from yamaha_host + return - builds cmd string and runs network query - calls _return_value to process value and assigns to item + # + # handle and format data values + # - returns without further action if no network response is received + def _get_value_from_response(self, state, cmd, host): """ - yamaha_payload = None - if notify_cmd in ['power', 'volume', 'mute', 'input', 'sleep']: - yamaha_payload = self._get_state() - elif notify_cmd in ['track', 'artist', 'albumart', - 'play_time', 'total_time', 'playback']: - yamaha_payload = self._get_play_state() + return value for selected command from response data - if yamaha_payload: - res = self._submit_payload(yamaha_host, yamaha_payload) - if res is None: - return - value = self._return_value(res, notify_cmd, yamaha_host) - if notify_cmd == "play_time" and value < 0: - return - else: - item = self._yamaha_dev[yamaha_host][notify_cmd] - item(value, "YamahaYXC") + tries to extract value for requested cmd + returns None if state is None (network error), returns value otherwise + returns error string if value is invalid or missing (this shoudn't happen + and documents configuration or code error on my side - might lead to funny + error logging) + """ + if state is None: + return None + if cmd == "albumart": + cmd = "albumart_url" + try: + value = state[cmd] + except: + return "Invalid data received (required item not found) - contact plugin author" + return self._convert_value_yxc_to_plugin(value, cmd, host) - def _process_value(self, value, cmd, host): + def _convert_value_yxc_to_plugin(self, value, cmd, host): """ + convert network values to python format and return processed value depending on command formats and returns value from raw input value @@ -317,11 +405,7 @@ def _process_value(self, value, cmd, host): elif cmd == 'volume': return int(value) elif cmd == 'mute': - if value == 'false': - return False - elif value == 'true': - return True - return value + return value == 'true' elif cmd == 'power': if value == 'standby': return False @@ -340,193 +424,230 @@ def _process_value(self, value, cmd, host): if self.last_total == 0: return (-1) else: - return int(100*value/self.last_total) + return int(100 * value / self.last_total) elif cmd == 'total_time': self.last_total = int(value) return int(value) elif cmd == 'albumart_url': value = 'http://{}{}'.format(host, value) return value + elif cmd == 'alarm_on': + return value == 'true' + elif cmd == 'alarm_time': + return value + elif cmd == 'alarm_beep': + if value == 'true': + return True + else: + return False - def _return_value(self, state, cmd, host): - """ - return selected value from response data - - transforms "state" response to json and extracts requested value - returns None if state is None (network error), returns value otherwise - returns error string if value is invalid or missing (this shoudn't happen - and documents configuration or code error on my side) - """ - if state is None: - return None - if cmd == "albumart": - cmd = "albumart_url" - try: - jdata = json.loads(state) - except Exception: - return "Invalid data received (not JSON)" - try: - value = jdata[cmd] - except: - return "Invalid data received (required item not found)" - return self._process_value(value, cmd, host) + # + # send network commands and receive responses + # def _submit_payload(self, host, payload): """ - send cmd string to device and return response data + send cmd string to device and return response data as dict returns None on network error (no connection, device not plugged in?) always subscribes to unicast notification service - log message "No payload received" probably indicates coding error or + log message "No payload received" probably indicates coding error or improper use of cmd 'passthru' + + payload can be + - a string, will then be sent via HTTP GET + - a list, will be sent via HTTP POST, needs + payload[0] as URL, payload[1] as POST data + Careful: POST data needs to be in proper JSON format + MusicCast devices are quite picky about double quotes; + single-quoted data is rewarded with errors! + + return data is None or a dict with json response data """ if payload: - url = "http://{}/YamahaExtendedControl/{}".format(host, payload) - headers = { - 'X-AppName': 'MusicCast/{}'.format(self.PLUGIN_VERSION), - 'X-AppPort': '{}'.format(self.srv_port)} + + if type(payload) is str: + url = "http://{}/YamahaExtendedControl/{}".format(host, payload) + headers = { + 'X-AppName': 'MusicCast/{}'.format(self.PLUGIN_VERSION), + 'X-AppPort': '{}'.format(self.srv_port)} + try: + res = requests.get(url, headers=headers) + response = res.text + del res + except: + self.logger.info("Device not answering: {}.".format(host)) + response = None + elif type(payload) is list: + if len(payload) < 2: + self.logger.debug("Payload in list format, but insufficient arguments") + response = None + else: + url = "http://{}/YamahaExtendedControl/{}".format(host, payload[0]) + headers = { + 'X-AppName': 'MusicCast/{}'.format(self.PLUGIN_VERSION), + 'X-AppPort': '{}'.format(self.srv_port)} + try: + res = requests.post(url, data=payload[1], headers=headers) + response = res.text + del res + except: + self.logger.info("Device not answering: {}.".format(host)) + response = None try: - res = requests.get(url, headers=headers) - response = res.text - del res - except: - self.logger.warn("Device not answering: {}.".format(host)) - response = None - return response + jdata = json.loads(response) + except Exception: + self.logger.debug("Invalid data received (not JSON). Data discarded.") + jdata = None + + return jdata else: self.logger.warn("No payload received. Used 'passthru' without argument?") return None - def _lookup_host(self, item): + # + # functions to create network commands + # + + def _build_cmd_power(self, value, cmd='PUT'): """ - get host IP stored for submitted item + return cmd string for "set power" - this does not work with nested items in item config, so don't nest items! + value is boolean: + True means "power on" + False means "standby" """ - parent = item.return_parent() - yamaha_host = socket.gethostbyname(parent.conf['yamahayxc_host']) - return yamaha_host + if value is True: + cmdarg = 'on' + elif value is False: + cmdarg = 'standby' + cmd = "v1/{}/setPower?power={}".format(self._yamaha_zone, cmdarg) + return cmd - def _lookup_zone(self, item): + def _build_cmd_input(self, value, cmd='PUT'): """ - get zone config for item + return cmd string for "set input" - again, don't nest items. - This is a stub function in preparation for later extension of the plugin - to multiple zones. Maybe flexible config of multiple zones for items - might become necessary -> rethink this approach then + value is string and device-dependent, e.g. + - tuner + - cd + - bluetooth + - net_radio (internet radio stream) + - server (UPNP client) """ - parent = item.return_parent() - yamaha_zone = parent.conf['yamahayxc_zone'] - return yamaha_zone + cmd = "v1/{}/setInput?input={}".format(self._yamaha_zone, value) + return cmd - def _add_host_info(self, host): + def _build_cmd_volume(self, value, cmd='PUT'): """ - store local interface IP for connection to a given host + return cmd string for "set volume" - just exists in case the server is multihomed. in most cases not necessary + volume is numeric from 0..60 """ - try: - local_ip = self._yamaha_hosts[host] - return - except: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect((host, 80)) - local_ip = s.getsockname()[0] - s.close() - self._yamaha_hosts[host] = local_ip + cmd = "v1/{}/setVolume?volume={}".format(self._yamaha_zone, value) + return cmd - def parse_item(self, item): + def _build_cmd_mute(self, value, cmd='PUT'): """ - parse all items at startup + return cmd string for "set mute" + """ + if value is True: + cmdarg = 'true' + elif value is False: + cmdarg = 'false' + cmd = "v1/{}/setMute?enable={}".format(self._yamaha_zone, cmdarg) + return cmd - This function is called by the SmartPlugin manager in sh.py for every - item. If item config "yamahayxc_cmd" is present, item ist stored together - with associated host and zone. - Returns update function for the item (update_item()) + def _build_cmd_playback(self, value, cmd="PUT"): """ - if 'yamahayxc_cmd' in item.conf: - yamaha_host = self._lookup_host(item) - self._add_host_info(yamaha_host) - yamaha_zone = self._lookup_zone(item) - self._yamaha_zone = yamaha_zone - yamaha_cmd = item.conf['yamahayxc_cmd'].lower() - if yamaha_cmd not in self._yamaha_cmds: - self.logger.warning("{} not in valid commands: {}".format( - yamaha_cmd, self._yamaha_cmds)) - return None - else: - try: - self._yamaha_dev[yamaha_host][yamaha_cmd] = item - except KeyError: - self._yamaha_dev[yamaha_host] = {} - self._yamaha_dev[yamaha_host][yamaha_cmd] = item - return self.update_item + return cmd string for "set playback" - def update_item(self, item, caller=None, source=None, dest=None): + value is string and can be (for netusb) + - play + - stop + - pause + - play_pause (toggle) + - previous + - next + - fast_reverse_start + - fast_reverse_stop + - fast_forward_start + - fast_forward_stop """ - recall function if item is modified in sh.py + cmd = "v1/netusb/setPlayback?playback={}".format(value) + return cmd - Only for "write" commands: calls function to build cmd string - for given item and runs network query to execute cmd - In any case _update_state is called afterwards to update sh.py items + def _build_cmd_get_state(self): """ - if caller != "YamahaYXC": - yamaha_cmd = item.conf['yamahayxc_cmd'] - yamaha_host = self._lookup_host(item) - yamaha_payload = None + return cmd string for "get status" -> get general status for zone + """ + cmd = "v1/{}/getStatus".format(self._yamaha_zone) + return cmd - if yamaha_cmd == 'power': - yamaha_payload = self._power(item()) - elif yamaha_cmd == 'volume': - yamaha_payload = self._volume(item()) - elif yamaha_cmd == 'mute': - yamaha_payload = self._mute(item()) - elif yamaha_cmd == 'input': - yamaha_payload = self._input(item()) - elif yamaha_cmd == 'playback': - yamaha_payload = self._playback(item()) - elif yamaha_cmd == 'preset': - yamaha_payload = self._preset(item()) - elif yamaha_cmd == "sleep": - yamaha_payload = self._sleep(item()) - elif yamaha_cmd == "passthru": - yamaha_payload = item() - # no more check for "update" or "state" -> after updating item - # _update_state is called in any way, would only duplicate update - if yamaha_payload: - self._submit_payload(yamaha_host, yamaha_payload) - self._update_state(yamaha_host) - return None + def _build_cmd_get_play_state(self): + """ + return cmd string for "get playstatus" -> get playing status - def _update_state(self, yamaha_host, update_items=True): + at the moment only netusb zone is supported, no other device to test """ - retrieve state from device via cmd _get_state and _get_play_state + cmd = "v1/netusb/getPlayInfo" + return cmd - As many items cannot be queried via yamahayxc by themselves, this - function simply sends two state update commands and flattens both - state replies into one flat dict (this is a kludge and does only work - with netusb and a single zone!!!!!) - Then for all relevant commands suitable return values are extracted - and processed (items are updated) - Return None prematurely if no network response was received - Silently ignores invalid (None) values for commands + def _build_cmd_get_alarm_state(self): """ - state1 = self._submit_payload(yamaha_host, self._get_state()) - if state1 is None: - return - state2 = self._submit_payload(yamaha_host, self._get_play_state()) - ostate = json.loads(state1) - ostate.update(json.loads(state2)) - state = json.dumps(ostate) - # retrieving only single items from device is not possible - # so just get everything and update sh.py items - if update_items: - for yamaha_cmd, item in self._yamaha_dev[yamaha_host].items(): - #self.logger.debug( - # "Updating cmd {} for item {}".format(yamaha_cmd, item)) - if yamaha_cmd not in self._yamaha_ignore_cmds_upd: - value = self._return_value(state, yamaha_cmd, yamaha_host) - if value is not None: - item(value, "YamahaYXC") - return state + return cmd string for "get alarm status" -> get alarm clock info + + cmd will return empty result if alarm not supported + """ + cmd = "v1/clock/getSettings" + return cmd + + def _build_cmd_preset(self, value): + """ + return cmd string for "set preset" + + value is integer preset index (1..) + + at the moment only netusb zone is supported (see above) + """ + cmd = "v1/netusb/recallPreset?zone=main&num={}".format(value) + return cmd + + def _build_cmd_sleep(self, value, cmd='PUT'): + """ + return cmd string for "set sleep" + + volume is numeric 0 / 30 / 60 / 90 / 120 (minutes) + """ + cmd = "v1/{}/setSleep?sleep={}".format(self._yamaha_zone, value) + return cmd + + def _build_cmd_alarm_on(self, value, cmd='PUT'): + """ + return cmd string for "switch alarm on/off" + + value is bool + """ + cmd = "v1/clock/setAlarmSettings" + data = json.dumps({"alarm_on": "true" if value else "false"}) + return ([cmd, data]) + + def _build_cmd_alarm_time(self, value, cmd='PUT'): + """ + return cmd string for "set alarm_time" + + value is string in 4 digit 24 hour time, e.g. "1430" + """ + cmd = "v1/clock/setAlarmSettings" + data = json.dumps({"detail": {"day": "oneday", "time": value}}) + return ([cmd, data]) + + def _build_cmd_alarm_beep(self, value, cmd='PUT'): + """ + return cmd string for "set alarm beep" + + value is bool + """ + cmd = "v1/clock/setAlarmSettings" + data = json.dumps({"detail": {"day": "oneday", "beep": "true" if value else "false"}}) + return ([cmd, data]) diff --git a/yamahayxc/plugin.yaml b/yamahayxc/plugin.yaml index f41a5ca3a..4b9932e59 100644 --- a/yamahayxc/plugin.yaml +++ b/yamahayxc/plugin.yaml @@ -6,7 +6,7 @@ plugin: de: 'Plugin, um Yamaha MusicCast-Geräte zu kontrollieren' en: 'plugin to control Yamaha MusicCast devices' - description_lone: + description_long: de: 'Dieses Plugin ermöglicht, Yamaha MusicCast-Geräte in SmartHomeNG einzubinden.\n \n Dabei ist es möglich, verschiedene Geräte aus SmartHomeNG aus anzusprechen und zu steuern, als auch über SmartHomeNG auszulesen.\n @@ -18,6 +18,7 @@ plugin: - Wiedergabesteuerung (Play/Pause/Stop/nächster,vorheriger Titel/Vor-,Zurückspulen),\n - Lautstärkesteuerung\n, - Anwahl von Favoriten und Presets (geräteabhängig)\n + - Stellen und De-/Aktivieren des Weckers (geräteabhängig)\n \n Die Möglichkeiten, Statusinformationen abzurufen, umfassen (derzeit) u.a.\n \n @@ -42,6 +43,7 @@ plugin: - playback control (play/pause/stop/skip forward,backward/fast forward,reverse),\n - volume control,\n - selection of favorites and presets (device dependent)\n + - setting and de-/activating the alarm clock (device dependent)\n \n Status information implemented (so far) include:\n \n @@ -57,20 +59,128 @@ plugin: ' maintainer: Morg42 # tester: Morg42 + state: develop keywords: iot yxc yamaha media musiccast # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1174064-plugin-yamaha-musiccast-geräte-neuere-generation - version: 1.0.5 # Plugin version + version: 1.0.6 # Plugin version sh_minversion: 1.3 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance restartable: False classname: YamahaYXC # class containing the plugin -parameters: - # Definition of parameters to be configured in etc/plugin.yaml - +parameters: NONE + # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) + item_attributes: - # Definition of item attributes defined by this plugin - + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + +item_structs: + # Definition of item structs to be used for use with this plugin + + basic: + # basic media player setup + + yamahayxc_zone: main + + wakeup: + type: bool + enforce_updates: 'True' + + power: + type: bool + yamahayxc_cmd: power + enforce_updates: 'True' + + volume: + type: num + yamahayxc_cmd: volume + enforce_updates: 'True' + + mute: + type: bool + yamahayxc_cmd: mute + enforce_updates: 'True' + + input: + type: str + yamahayxc_cmd: input + enforce_updates: 'True' + + track: + type: str + yamahayxc_cmd: track + + albumart: + type: str + yamahayxc_cmd: albumart + + albumarturl: + type: str + visu_acl: rw + + update: + type: bool + yamahayxc_cmd: state + enforce_updates: 'True' + + preset: + type: num + yamahayxc_cmd: preset + enforce_updates: 'True' + + sleep: + type: num + yamahayxc_cmd: sleep + enforce_updates: 'True' + + artist: + type: str + yamahayxc_cmd: artist + + curtime: + type: num + yamahayxc_cmd: play_time + + totaltime: + type: num + yamahayxc_cmd: total_time + + playback: + type: str + yamahayxc_cmd: playback + enforce_updates: 'True' + + passthru: + type: str + yamahayxc_cmd: passthru + enforce_updates: 'True' + + + alarm: + # media player setup with alarm clock functions + struct: yamahayxc.basic + + alarm_on: + type: bool + yamahayxc_cmd: alarm_on + enforce_updates: 'True' + + alarm_time: + type: str + yamahayxc_cmd: alarm_time + enforce_updates: 'True' + + alarm_beep: + type: bool + yamahayxc_cmd: alarm_beep + enforce_updates: 'True' + +plugin_functions: NONE + # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) + +logic_parameters: NONE + # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) +