Repository: elgatosf/streamdeck-philipshue Branch: master Commit: 1af7c43f186c Files: 52 Total size: 218.1 KB Directory structure: gitextract_alvhxsyj/ ├── .editorconfig ├── .gitignore ├── .hintrc ├── LICENSE ├── README.md ├── Release/ │ └── com.elgato.philips-hue.streamDeckPlugin └── Sources/ └── com.elgato.philips-hue.sdPlugin/ ├── de.json ├── en.json ├── es.json ├── fr.json ├── ja.json ├── ko.json ├── manifest.json ├── pi/ │ ├── css/ │ │ ├── colorPI.css │ │ ├── cyclePI.css │ │ ├── pi.css │ │ └── sdpi.css │ ├── index.html │ └── js/ │ ├── brightnessPI.js │ ├── brightnessRelPI.js │ ├── colorPI.js │ ├── cyclePI.js │ ├── main.js │ ├── pi.js │ ├── powerPI.js │ ├── scenePI.js │ ├── temperaturePI.js │ └── tooltips.js ├── plugin/ │ ├── index.html │ └── js/ │ ├── action.js │ ├── brightnessAction.js │ ├── brightnessRelAction.js │ ├── colorAction.js │ ├── cycleAction.js │ ├── main.js │ ├── philips/ │ │ ├── cache.js │ │ └── meethue.js │ ├── powerAction.js │ ├── propertyAction.js │ ├── sceneAction.js │ ├── temperatureAction.js │ ├── timers.js │ └── utils.js ├── setup/ │ ├── css/ │ │ └── main.css │ ├── index.html │ └── js/ │ ├── discoveryView.js │ ├── introView.js │ ├── main.js │ ├── manualView.js │ ├── pairingView.js │ └── saveView.js └── zh_CN.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 2 ================================================ FILE: .gitignore ================================================ __xx/* ================================================ FILE: .hintrc ================================================ { "extends": [ "development" ], "hints": { "axe/forms": "off", "meta-viewport": "off", "axe/language": "off", "no-inline-styles": "off" } } ================================================ FILE: LICENSE ================================================ The MIT License Copyright 2018 Corsair Memory, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Philips Hue Plugin for Elgato Stream Deck # UPDATE INFORMATION ## There's a new version of the Philips Hue plugin available on the [Elgato Marketplace](https://marketplace.elgato.com/product/philips-hue-27f49792-2de3-455f-8892-fd382716f548). You'll find that version [here](https://marketplace.elgato.com/product/philips-hue-27f49792-2de3-455f-8892-fd382716f548) New discussions/support will move to [Discord](https://discord.gg/elgato) The new version of the plugin should work with all recent Philips Hue Bridges (sold like 6-7 years ago). **If you have the original plugin installed, please, first install the update to the old plugin** [Philips-Hue original plugin 1.6.8](https://github.com/user-attachments/files/15746596/com.elgato.philips-hue.streamDeckPlugin.zip), or remove it - otherwise your sidebar gets mixed up... You can install both versions (old and new) in parallel without problems, but it has no benefits. The new version is far more capable than the legacy version. CleanShot_2024-06-08_08 35 59 ---- (The notes below are kept for reference) ## Philips Hue Plugin for Elgato Stream Deck (legacy version) This sample plugin allows controlling `Philips Hue` lights in your network. It's a demonstration of the [Stream Deck SDK](https://developer.elgato.com/documentation/stream-deck/). Since version 1.6.0, it also supports the Stream Deck +. The brightness-action contains an example of how to change the display of the dial-control's touch-panel. ## Version 1.6.7 is also available in the Stream Deck Store! ## Features - Code written in JavaScript - Cross-platform (macOS, Windows) - Localized - Basic support for Stream Deck + ![](screenshot.png) # Installation In the [Release](./Release) folder, you can find the file `com.elgato.philips-hue.streamDeckPlugin`. If you double-click this file on your machine, Stream Deck will install the plugin. # Source code The [Sources](./Sources) folder contains the source code of the plugin. # Changes ## 1.6.4 - fixed/improved support for temperature actions - PI now lets you only select lights for a temperature action if they support color temperature ## 1.6.3 - updated CSS to the latest versions of our SDK-libs - added an option to the PI to allow larger steps if you rotate dials (1,2,3,4,5,10). ## 1.6.0 - fixed broken localizations - changed versioning to semver - added basic support for Stream Deck + # How it works (since 1.6.0) ![](touchpanel.png) You can now drag a brightness-/ or temperature-action to a SD+ dial-control. It supports these actions: - Turn the dial to change the brightness/temperature - Press the dial to: - - set the brightness/temperature to the configured value - if the light is on - - turn the light on - if the light is off - Long-Press the dial to toggle the light on/off - Tap the touch-panel to toggle the light on/off DialStacks are not properly supported yet. ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/de.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Steuere deine Philips Hue-Lampen.", "com.elgato.philips-hue.power": { "Name": "Ein/Aus", "Tooltip": "Schalte das Licht ein oder aus.", "States": [ { "Name": "Ein" }, { "Name": "Aus" } ] }, "com.elgato.philips-hue.color": { "Name": "Farbe", "Tooltip": "Stelle die Farbe des Lichts ein." }, "com.elgato.philips-hue.cycle": { "Name": "Farben durchschalten", "Tooltip": "Wechsle zwischen mehreren Lichtfarben." }, "com.elgato.philips-hue.brightness": { "Name": "Helligkeit", "Tooltip": "Stelle die Helligkeit des Lichts ein.", "Encoder": { "TriggerDescription": { "Rotate": "Helligkeit anpassen", "Touch": "Licht ein/aus" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "Relative Helligkeit", "Tooltip": "Stelle die Helligkeit des Lichts relativ zur aktuellen Helligkeit ein." }, "com.elgato.philips-hue.scene": { "Name": "Szene", "Tooltip": "Stelle eine Szene ein." }, "Localization": { "PI": { "Bridge": "Bridge", "NoBridges": "Keine Bridges", "AddBridge": "Neue hinzufügen", "Lights": "Lampen", "Group": "Gruppe", "LightsTitle": "Lampen", "GroupsTitle": "Gruppen", "NoLights": "Keine Lampen", "NoGroups": "Keine Gruppen", "Color": "Farbe", "Colors": "Farben", "Temperature": "Temperatur", "Brightness": "Helligkeit", "Steps": "Schritte", "Scene": "Szene", "NoScenes": "Keine Szenen" }, "Setup": { "Intro": { "Title": "Bridge hinzufügen", "Description": "Mit einer Philips Hue-Bridge koppeln, um deine Lichter zu steuern.", "Start": "Bridges suchen", "Manual": "Bridge manuell hinzufügen", "Close": "Nicht jetzt" }, "Discovery": { "Title": "Bridges werden gesucht …", "TitleNone": "Keine Bridges gefunden", "TitleOne": "Eine Bridge gefunden", "TitleMultiple": "{{ number }} Bridges gefunden", "DescriptionFound": "Jetzt koppeln?", "DescriptionNone": "Vergewissere dich, dass die Bridge eingeschaltet und mit dem Netzwerk verbunden ist.", "Pair": "Koppeln", "Close": "Schließen", "Retry": "Erneut versuchen" }, "Manual": { "Title": "Bridge manuell hinzufügen", "Description": "Füge deine Philips Hue-Bridge deinem Netzwerk manuell über die IP-Adresse hinzu, falls Auto-Discovery nicht unterstützt wird.", "IPAddress": "IP-Adresse der Bridge", "Check": "Eingabe überprüfen", "Close": "Schließen", "Error": { "Empty": "Bitte trage eine IP-Adresse in das Eingabefeld ein.", "Invalid": "Die eingetragene IP-Adresse hat ein ungültiges Format. Bitte überprüfe deine Eingabe.", "Unreachable": "Die Bridge ist unter der IP-Adresse nicht erreichbar. Bitte überprüfe die IP-Adresse." } }, "Pairing": { "Title": "Wird gekoppelt …", "Description": "Drücke auf der Bridge jetzt die Taste zum Verbinden.", "Close": "Schließen", "Retry": "Erneut versuchen" }, "Save": { "Title": "Bridge hinzugefügt", "Description": "Die Bridge wurde erfolgreich hinzugefügt.", "Save": "Fertig" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/en.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Control your Philips Hue lights.", "com.elgato.philips-hue.power": { "Name": "On / Off", "Tooltip": "Turn lights on or off.", "States": [{ "Name": "On" }, { "Name": "Off" }] }, "com.elgato.philips-hue.color": { "Name": "Color", "Tooltip": "Set the color of your light." }, "com.elgato.philips-hue.cycle": { "Name": "Color Cycle", "Tooltip": "Cycle between colors of your light." }, "com.elgato.philips-hue.brightness": { "Name": "Brightness", "Tooltip": "Set the brightness of your light.", "Encoder": { "TriggerDescription": { "Rotate": "Adjust Brightness", "Touch": "Toggle Lights on/off" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "Brightness Relative", "Tooltip": "Adjust the brightness of the light relative to the current brightness." }, "com.elgato.philips-hue.scene": { "Name": "Scene", "Tooltip": "Set a scene." }, "Localization": { "PI": { "Bridge": "Bridge", "NoBridges": "No Bridges", "AddBridge": "Add New", "Lights": "Lights", "Group": "Group", "LightsTitle": "Lights", "GroupsTitle": "Groups", "NoLights": "No Lights", "NoGroups": "No Groups", "Color": "Color", "Colors": "Colors", "Temperature": "Temperature", "Brightness": "Brightness", "Steps": "Steps", "Scene": "Scene", "NoScenes": "No Scenes" }, "Setup": { "Intro": { "Title" : "Add Bridge", "Description": "Pair with a Philips Hue bridge to control your lights.", "Start": "Discover Bridges", "Manual": "Add Bridge manually", "Close": "Not Now" }, "Discovery": { "Title" : "Discovering Bridges …", "TitleNone" : "No Bridges Found", "TitleOne" : "One Bridge Found", "TitleMultiple" : "{{ number }} Bridges Found", "DescriptionFound": "Start pairing now?", "DescriptionNone": "Make sure your bridge is switched on and connected to the network.", "Pair": "Pair", "Close": "Close", "Retry": "Try Again" }, "Manual": { "Title": "Add bridge manually", "Description": "Add your Philips Hue-Bridge manually (via IP-Address) to your network, in cases where Auto-Discovery is not supported.", "IPAddress": "IP address of the bridge", "Check": "Validate input", "Close": "Close", "Error": { "Empty": "Please add the ip address of your bridge.", "Invalid": "The entered IP address has an invalid format. Please check your input.", "Unreachable": "Bridge is not reachable. Please check the ip address." } }, "Pairing": { "Title" : "Pairing...", "Description": "Please press the link button on the bridge now.", "Close": "Close", "Retry": "Try Again" }, "Save": { "Title" : "Bridge Added", "Description": "The bridge has been paired sucessfully.", "Save": "Done" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/es.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Controla tus luces Philips Hue.", "com.elgato.philips-hue.power": { "Name": "On / Off", "Tooltip": "Encender o apagar las luces.", "States": [ { "Name": "On" }, { "Name": "Off" } ] }, "com.elgato.philips-hue.color": { "Name": "Color", "Tooltip": "Ajusta el color de tu luz." }, "com.elgato.philips-hue.cycle": { "Name": "Transición de color", "Tooltip": "Hacer transición entre los colores de tu luz." }, "com.elgato.philips-hue.brightness": { "Name": "Brillo", "Tooltip": "Ajusta el brillo de tu luz.", "Encoder": { "TriggerDescription": { "Rotate": "Ajustar brillo", "Touch": "Encender/apagar luces" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "Brillo relativo", "Tooltip": "Ajusta el brillo de la luz con respecto al brillo actual." }, "com.elgato.philips-hue.scene": { "Name": "Escena", "Tooltip": "Activa una escena." }, "Localization": { "PI": { "Bridge": "Bridge", "NoBridges": "No hay Bridges", "AddBridge": "Añadir", "Lights": "Luces", "Group": "Grupo", "LightsTitle": "Luces", "GroupsTitle": "Grupos", "NoLights": "No hay luces", "NoGroups": "No hay grupos", "Color": "Color", "Colors": "Colores", "Temperature": "Temperatura", "Brightness": "Brillo", "Steps": "Pasos", "Scene": "Escena", "NoScenes": "No hay escenas" }, "Setup": { "Intro": { "Title": "Añadir Bridge", "Description": "Enlaza con un Bridge Philips Hue para controlar tus luces.", "Start": "Detectar Bridge", "Manual": "Añadir Bridge a mano", "Close": "Ahora no" }, "Discovery": { "Title": "Detectando Bridges …", "TitleNone": "No se han detectado Bridges", "TitleOne": "Se ha detectado un Bridge", "TitleMultiple": "{{ number }} Bridges detectados", "DescriptionFound": "¿Empezar a enlazar ahora?", "DescriptionNone": "Comprueba que el Bridge está encendido y conectado a la red.", "Pair": "Enlazar", "Close": "Cerrar", "Retry": "Volver a intentar" }, "Manual": { "Title": "Añadir Bridge a mano", "Description": "Añade tu Philips Hue Bridge manualmente (mediante la dirección IP) a tu red, en los casos en los que la detección automática no se puede usar.", "IPAddress": "Dirección IP del Bridge", "Check": "Validar entrada", "Close": "Cerrar", "Error": { "Empty": "Añade la dirección IP de tu Bridge.", "Invalid": "El formato de la dirección IP no es válido. Comprueba lo que has introducido.", "Unreachable": "Nose puede conectar con el Bridge. Comprueba la dirección IP." } }, "Pairing": { "Title": "Enlazando...", "Description": "Pulsa ahora el botón de enlace en el Bridge.", "Close": "Cerrar", "Retry": "Volver a intentar" }, "Save": { "Title": "Bridge añadido", "Description": "El Bridge se ha enlazado correctamente.", "Save": "OK" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/fr.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Contrôlez vos systèmes d’éclairage Philips Hue.", "com.elgato.philips-hue.power": { "Name": "Activer/Désactiver", "Tooltip": "Allumez ou éteignez les lumières.", "States": [ { "Name": "Activer" }, { "Name": "Désactiver" } ] }, "com.elgato.philips-hue.color": { "Name": "Couleur", "Tooltip": "Réglez la couleur de la lumière." }, "com.elgato.philips-hue.cycle": { "Name": "Cycle de couleurs", "Tooltip": "Activez les différentes couleurs de votre lumière de façon cyclique." }, "com.elgato.philips-hue.brightness": { "Name": "Luminosité", "Tooltip": "Réglez la luminosité de la lumière.", "Encoder": { "TriggerDescription": { "Rotate": "Ajuster la luminosité", "Touch": "Allumer ou éteindre les lumières" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "Luminosité relative", "Tooltip": "Permet de régler la luminosité de l’éclairage par rapport à la luminosité actuelle." }, "com.elgato.philips-hue.scene": { "Name": "Scène", "Tooltip": "Définissez une scène." }, "Localization": { "PI": { "Bridge": "Pont", "NoBridges": "Aucun pont", "AddBridge": "Ajouter", "Lights": "Lumières", "Group": "Groupe", "LightsTitle": "Lumières", "GroupsTitle": "Groupes", "NoLights": "Aucune lumière", "NoGroups": "Aucun groupe", "Color": "Couleur", "Colors": "Couleurs", "Temperature": "Température", "Brightness": "Luminosité", "Steps": "Pas", "Scene": "Scène", "NoScenes": "Aucune scène" }, "Setup": { "Intro": { "Title": "Ajouter un pont", "Description": "Jumelez l’application à un pont Philips Hue pour contrôler vos lumières.", "Start": "Découvrir des ponts", "Manual": "Ajouter un pont manuellement", "Close": "Pas maintenant" }, "Discovery": { "Title": "Découverte des ponts…", "TitleNone": "Aucun pont trouvé", "TitleOne": "1 pont trouvé", "TitleMultiple": "{{ number }} ponts trouvés", "DescriptionFound": "Lancer le jumelage ?", "DescriptionNone": "Vérifiez que votre pont est allumé et connecté au réseau.", "Pair": "Jumeler", "Close": "Fermer", "Retry": "Réessayer" }, "Manual": { "Title": "Ajouter un pont manuellement", "Description": "Ajoutez votre pont Philips Hue manuellement (à partir de son adresse IP) à votre réseau, au cas où la découverte automatique serait impossible.", "IPAddress": "Adresse IP du pont", "Check": "Valider la saisie", "Close": "Fermer", "Error": { "Empty": "Veuillez ajouter l’adresse IP de votre pont.", "Invalid": "Le format de l’adresse IP saisie n’est pas valide. Veuillez vérifier votre saisie.", "Unreachable": "Le pont est injoignable. Veuillez vérifier l’adresse IP." } }, "Pairing": { "Title": "Jumelage…", "Description": "Veuillez appuyer sur le bouton de jumelage du pont.", "Close": "Fermer", "Retry": "Réessayer" }, "Save": { "Title": "Pont ajouté", "Description": "Le pont a bien été jumelé à l’application.", "Save": "Terminé" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/ja.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Philips Hueの照明をコントロールします。", "com.elgato.philips-hue.power": { "Name": "オン/オフ", "Tooltip": "照明をオン、またはオフにします。", "States": [ { "Name": "オン" }, { "Name": "オフ" } ] }, "com.elgato.philips-hue.color": { "Name": "カラー", "Tooltip": "照明のカラーを設定します。" }, "com.elgato.philips-hue.cycle": { "Name": "カラーサイクル", "Tooltip": "照明のカラーを循環して表示します。" }, "com.elgato.philips-hue.brightness": { "Name": "明るさ", "Tooltip": "照明の明るさを調整します。", "Encoder": { "TriggerDescription": { "Rotate": "明るさを調整", "Touch": "照明のオン/オフを切り替え" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "明るさ (相対的)", "Tooltip": "現在の明るさを基準にして照明の明るさを調整します。" }, "com.elgato.philips-hue.scene": { "Name": "シーン", "Tooltip": "シーンを設定します。" }, "Localization": { "PI": { "Bridge": "ブリッジ", "NoBridges": "ブリッジがありません", "AddBridge": "新規を追加", "Lights": "照明", "Group": "グループ", "LightsTitle": "照明", "GroupsTitle": "グループ", "NoLights": "照明がありません", "NoGroups": "グループがありません", "Color": "カラー", "Colors": "カラー", "Temperature": "温度", "Brightness": "明るさ", "Steps": "ステップ", "Scene": "シーン", "NoScenes": "シーンがありません" }, "Setup": { "Intro": { "Title": "ブリッジを追加", "Description": "照明をコントロールするには、Philips Hueのブリッジを使ってペアリングしてください。", "Start": "ブリッジを検出", "Manual": "ブリッジを手動で追加", "Close": "今はしない" }, "Discovery": { "Title": "ブリッジを検出中…", "TitleNone": "ブリッジが見つかりません", "TitleOne": "1件のブリッジを検出しました", "TitleMultiple": "{{ number }}件のブリッジを検出しました", "DescriptionFound": "今すぐペアリングしますか?", "DescriptionNone": "ブリッジがオンになっていて、ネットワークに接続されていることを確認してください。", "Pair": "ペアリング", "Close": "閉じる", "Retry": "やり直す" }, "Manual": { "Title": "ブリッジを手動で追加", "Description": "自動検出がサポートされていない場合、Philips Hueブリッジをお使いのネットワークに (IPアドレスを使って) 手動で追加してください。", "IPAddress": "ブリッジのIPアドレス", "Check": "入力を検証", "Close": "閉じる", "Error": { "Empty": "ブリッジのIPアドレスを追加してください。", "Invalid": "入力されたIPアドレスは無効なフォーマットです。入力を確認してください。", "Unreachable": "ブリッジに接続できません。IPアドレスを確認してください。" } }, "Pairing": { "Title": "ペアリング中…", "Description": "今すぐブリッジの接続ボタンを押してください。", "Close": "閉じる", "Retry": "やり直す" }, "Save": { "Title": "ブリッジが追加されました", "Description": "ブリッジのペアリングを完了しました。", "Save": "完了" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/ko.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "Philips Hue 조명을 조종하세요.", "com.elgato.philips-hue.power": { "Name": "켬 / 끔", "Tooltip": "조명을 끄거나 켭니다.", "States": [ { "Name": "켬" }, { "Name": "끔" } ] }, "com.elgato.philips-hue.color": { "Name": "색상", "Tooltip": "조명 색상을 설정하세요." }, "com.elgato.philips-hue.cycle": { "Name": "색상 사이클", "Tooltip": "조명 색상 사이를 순환합니다." }, "com.elgato.philips-hue.brightness": { "Name": "밝기", "Tooltip": "조명의 밝기를 조절합니다.", "Encoder": { "TriggerDescription": { "Rotate": "밝기 조절", "Touch": "조명 켬/끔" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "상대적 밝기", "Tooltip": "현재 밝기에 비례하여 밝기를 조절합니다." }, "com.elgato.philips-hue.scene": { "Name": "장면", "Tooltip": "장면을 설정합니다." }, "Localization": { "PI": { "Bridge": "브릿지", "NoBridges": "브릿지 없음", "AddBridge": "신규 추가", "Lights": "조명", "Group": "그룹", "LightsTitle": "조명", "GroupsTitle": "그룹", "NoLights": "조명 없음", "NoGroups": "그룹 없음", "Color": "색상", "Colors": "색상", "Temperature": "온도", "Brightness": "밝기", "Steps": "단계", "Scene": "장면", "NoScenes": "장면 없음" }, "Setup": { "Intro": { "Title": "브릿지 추가", "Description": "Philips Hue 브릿지와 연결하여 조명을 조절하세요.", "Start": "브릿지 탐색", "Manual": "수동으로 브릿지 추가", "Close": "나중에" }, "Discovery": { "Title": "브릿지 탐색 중...", "TitleNone": "브릿지를 찾을 수 없음", "TitleOne": "하나의 브릿지 발견", "TitleMultiple": "{{ number }}개의 브릿지 발견", "DescriptionFound": "지금 연결하시겠습니까?", "DescriptionNone": "브릿지가 켜져 있고 네트워크에 연결되어 있는지 확인하세요.", "Pair": "연결", "Close": "닫기", "Retry": "다시 시도하기" }, "Manual": { "Title": "수동으로 브릿지 추가", "Description": "자동 탐색이 지원되지 않는 경우, (IP 주소를 통해) Philips Hue 브릿지를 수동으로 네트워크에 추가하십시오.", "IPAddress": "브릿지의 IP 주소", "Check": "입력 유효성 검사", "Close": "닫기", "Error": { "Empty": "브릿지의 IP 주소를 추가하십시오.", "Invalid": "입력한 IP 주소가 유효한 형식이 아닙니다. 입력을 확인하십시오.", "Unreachable": "브릿지에 도달할 수 없습니다. IP 주소를 확인하십시오." } }, "Pairing": { "Title": "연결 중...", "Description": "지금 브릿지의 링크 버튼을 누르세요.", "Close": "닫기", "Retry": "다시 시도하기" }, "Save": { "Title": "브릿지 추가됨", "Description": "브릿지가 성공적으로 연결되었습니다.", "Save": "완료" } } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/manifest.json ================================================ { "Actions": [ { "Icon": "plugin/images/actions/power", "Name": "On / Off", "States": [ { "Image": "plugin/images/icons/on", "Name": "On" }, { "Image": "plugin/images/icons/off", "Name": "Off" } ], "Tooltip": "Turn lights on or off.", "UUID": "com.elgato.philips-hue.power" }, { "Icon": "plugin/images/actions/color", "Name": "Color", "States": [ { "Image": "plugin/images/icons/color" } ], "Tooltip": "Set the color of your light.", "UUID": "com.elgato.philips-hue.color" }, { "Icon": "plugin/images/actions/cycle", "Name": "Color Cycle", "States": [ { "Image": "plugin/images/icons/cycle" } ], "Tooltip": "Cycle between colors of your light.", "UUID": "com.elgato.philips-hue.cycle" }, { "Icon": "plugin/images/actions/brightness", "Name": "Brightness", "States": [ { "Image": "plugin/images/icons/brightness" } ], "Controllers": [ "Keypad", "Encoder" ], "Encoder": { "layout": "$B1", "TriggerDescription": { "Rotate": "Adjust Brightness", "Touch": "Toggle Light on/off" } }, "Tooltip": "Set the brightness of your light.", "UUID": "com.elgato.philips-hue.brightness" }, { "Icon": "plugin/images/actions/temperature", "Name": "Temperature", "States": [ { "Image": "plugin/images/icons/temperature" } ], "Controllers": [ "Keypad", "Encoder" ], "Encoder": { "layout": "$B1", "TriggerDescription": { "Rotate": "Adjust Temperature", "Touch": "Toggle Light on/off" } }, "Tooltip": "Set the temperature of your light.", "UUID": "com.elgato.philips-hue.temperature" }, { "Icon": "plugin/images/actions/brightness", "Name": "Brightness Relative", "States": [ { "Image": "plugin/images/icons/brightness" } ], "Tooltip": "Set the brightness of your light.", "UUID": "com.elgato.philips-hue.brightness-rel" }, { "Icon": "plugin/images/actions/scene", "Name": "Scene", "States": [ { "Image": "plugin/images/icons/scene" } ], "Tooltip": "Set a scene.", "UUID": "com.elgato.philips-hue.scene" } ], "SDKVersion": 2, "Author": "Elgato", "CodePath": "plugin/index.html", "Description": "Control your Philips Hue lights.", "Name": "Philips Hue", "Icon": "plugin/images/icons/plugin", "Category": "Philips Hue", "CategoryIcon": "plugin/images/actions/category", "PropertyInspectorPath": "pi/index.html", "URL": "https://www.elgato.com/gaming/stream-deck", "Version": "1.6.5", "DefaultWindowSize": [ 500, 650 ], "OS": [ { "Platform": "mac", "MinimumVersion": "10.11" }, { "Platform": "windows", "MinimumVersion": "10" } ], "Software": { "MinimumVersion": "5.0" } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/css/colorPI.css ================================================ /** @file colorPI.css @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ .temperature::-webkit-slider-runnable-track { background-image: linear-gradient(to right, #faa04e, #86c6e8) !important; } .temperature::-webkit-slider-thumb { background-color: #86c6e8 !important; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/css/cyclePI.css ================================================ /** @file cyclePI.css @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ #cycle-buttons .sdpi-item-value { font-size: 0; } #cycle-buttons button { width: 36px; font-weight: 900; display: inline-block; margin: 0 6px 0 2px; padding: 0; } #color-input-container div.sdpi-item-value { font-size: 0; } #color-input-container input[type='color'] { margin: 0 4px 0 0; padding: 0; width: 40px; } #color-input-container span:nth-child(5n) input { margin-right: 0; } #color-input-container span:nth-child(5n):after { content: ' '; display: block; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/css/pi.css ================================================ /** @file pi.css @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ #pi { display: none; } #placeholder { margin-top: 3px; } select option:disabled { font-size: 1pt; background-color: #505050; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/css/sdpi.css ================================================ :root { --sdpi-bgcolor: #2D2D2D; --sdpi-background: #3D3D3D; --sdpi-color: #d8d8d8; --sdpi-bordercolor: #3a3a3a; --sdpi-buttonbordercolor: #969696; --sdpi-borderradius: 0px; --sdpi-width: 224px; --sdpi-fontweight: 600; --sdpi-letterspacing: -0.25pt; } html { --sdpi-bgcolor: #2D2D2D; --sdpi-background: #3D3D3D; --sdpi-color: #d8d8d8; --sdpi-bordercolor: #3a3a3a; --sdpi-buttonbordercolor: #969696; --sdpi-borderradius: 0px; --sdpi-width: 224px; --sdpi-fontweight: 600; --sdpi-letterspacing: -0.25pt; height: 100%; width: 100%; overflow: hidden; touch-action: none; } html, body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 9pt; background-color: var(--sdpi-bgcolor); color: #9a9a9a; } body { height: 100%; padding: 0; overflow-x: hidden; overflow-y: auto; margin: 0; -webkit-overflow-scrolling: touch; -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased; } mark { background-color: var(--sdpi-bgcolor); color: var(--sdpi-color); } hr, hr2 { -webkit-margin-before: 1em; -webkit-margin-after: 1em; border-style: none; background: var(--sdpi-background); height: 1px; } hr2, .sdpi-heading { display: flex; flex-basis: 100%; align-items: center; color: inherit; font-size: 9pt; margin: 8px 0px; } .sdpi-heading::before, .sdpi-heading::after { content: ""; flex-grow: 1; background: var(--sdpi-background); height: 1px; font-size: 0px; line-height: 0px; margin: 0px 16px; } hr2 { height: 2px; } hr, hr2 { margin-left: 16px; margin-right: 16px; } .sdpi-item-value, option, input, select, button { font-size: 10pt; font-weight: var(--sdpi-fontweight); letter-spacing: var(--sdpi-letterspacing); } .sdpi-item-value > :last-of-type, .sdpi-item-value:last-child { margin-bottom: 4px; } .win .sdpi-item-value, .win option, .win input, .win select, .win button { font-size: 11px; font-style: normal; letter-spacing: inherit; font-weight: 100; } .win button { font-size: 12px; } ::-webkit-progress-value, meter::-webkit-meter-optimum-value { border-radius: 2px; /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */ } ::-webkit-progress-bar, meter::-webkit-meter-bar { border-radius: 3px; background: var(--sdpi-background); } ::-webkit-progress-bar:active, meter::-webkit-meter-bar:active { border-radius: 3px; background: #222222; } ::-webkit-progress-value:active, meter::-webkit-meter-optimum-value:active { background: #99f; } progress, progress.sdpi-item-value { min-height: 5px !important; height: 5px; background-color: #303030; } progress { margin-top: 8px !important; margin-bottom: 8px !important; } .full progress, progress.full { margin-top: 3px !important; } ::-webkit-progress-inner-element { background-color: transparent; } .sdpi-item[type="progress"] { margin-top: 4px !important; margin-bottom: 12px; min-height: 15px; } .sdpi-item-child.full:last-child { margin-bottom: 4px; } .tabs { /** * Setting display to flex makes this container lay * out its children using flexbox, the exact same * as in the above "Stepper input" example. */ display: flex; border-bottom: 1px solid #D7DBDD; } .tab { cursor: pointer; padding: 5px 30px; color: #16a2d7; font-size: 9pt; border-bottom: 2px solid transparent; } .tab.is-tab-selected { border-bottom-color: #4ebbe4; } select { -webkit-appearance: none; -moz-appearance: none; -o-appearance: none; appearance: none; background: url(../assets/caret.svg) no-repeat 97% center; } label.sdpi-file-label, input[type="button"], input[type="submit"], input[type="reset"], input[type="file"], input[type=file]::-webkit-file-upload-button, button, select { color: var(--sdpi-color); border: 1pt solid #303030; font-size: 8pt; background-color: var(--sdpi-background); border-radius: var(--sdpi-borderradius); } label.sdpi-file-label, input[type="button"], input[type="submit"], input[type="reset"], input[type="file"], input[type=file]::-webkit-file-upload-button, button { border: 1pt solid var(--sdpi-buttonbordercolor); border-radius: var(--sdpi-borderradius); border-color: var(--sdpi-buttonbordercolor); min-height: 23px !important; height: 23px !important; margin-right: 8px; } input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } input[type="file"] { border-radius: var(--sdpi-borderradius); max-width: 220px; } option { height: 1.5em; padding: 4px; } /* SDPI */ .sdpi-wrapper { overflow-x: hidden; height: 100%; } .sdpi-item { display: flex; flex-direction: row; min-height: 32px; align-items: center; margin-top: 2px; max-width: 344px; -webkit-user-drag: none; } .sdpi-item:first-child { margin-top: -1px; } .sdpi-item:first-of-type { margin-top: 2px; } .sdpi-item[type="radio"]:first-of-type, .sdpi-item[type="checkbox"]:first-of-type { margin-top: -4px; } .sdpi-item:last-child { margin-bottom: 0px; } .sdpi-item > *:not(.sdpi-item-label):not(meter):not(details):not(canvas) { min-height: 26px; } .sdpi-item > *:not(.sdpi-item-label.empty):not(meter) { min-height: 26px; } .sdpi-item > input { padding: 0px 4px; } .sdpi-item-group { padding: 0 !important; } meter.sdpi-item-value { margin-left: 6px; } .sdpi-item[type="group"] { display: block; margin-top: 12px; margin-bottom: 12px; /* border: 1px solid white; */ flex-direction: unset; text-align: left; } .sdpi-item[type="group"] > .sdpi-item-label, .sdpi-item[type="group"].sdpi-item-label { width: 96%; text-align: left; font-weight: 700; margin-bottom: 4px; padding-left: 4px; } dl, ul, ol { -webkit-margin-before: 0px; -webkit-margin-after: 4px; -webkit-padding-start: 1em; max-height: 90px; overflow-y: scroll; cursor: pointer; user-select: none; } table.sdpi-item-value, dl.sdpi-item-value, ul.sdpi-item-value, ol.sdpi-item-value { -webkit-margin-before: 4px; -webkit-margin-after: 8px; -webkit-padding-start: 1em; width: var(--sdpi-width); text-align: center; } table > caption { margin: 2px; } .list, .sdpi-item[type="list"] { align-items: baseline; } .sdpi-item-label { text-align: right; flex: none; width: 94px; padding-right: 5px; font-weight: 600; -webkit-user-select: none; line-height: 24px; margin-left: -1px; } .win .sdpi-item-label, .sdpi-item-label > small { font-weight: normal; } .sdpi-item-label:after { content: ": "; } .sdpi-item-label.empty:after { content: ""; } .sdpi-test, .sdpi-item-value { flex: 1 0 0; /* flex-grow: 1; flex-shrink: 0; */ margin-right: 14px; margin-left: 4px; justify-content: space-evenly; } canvas.sdpi-item-value { max-width: 144px; max-height: 144px; width: 144px; height: 144px; margin: 0 auto; cursor: pointer; } input.sdpi-item-value { margin-left: 5px; } .sdpi-item-value button, button.sdpi-item-value { margin-left: 6px; margin-right: 14px; } .sdpi-item-value.range { margin-left: 0px; } table, dl.sdpi-item-value, ul.sdpi-item-value, ol.sdpi-item-value, .sdpi-item-value > dl, .sdpi-item-value > ul, .sdpi-item-value > ol { list-style-type: none; list-style-position: outside; margin-left: -4px; margin-right: -4px; padding: 4px; border: 1px solid var(--sdpi-bordercolor); } dl.sdpi-item-value, ul.sdpi-item-value, ol.sdpi-item-value, .sdpi-item-value > ol { list-style-type: none; list-style-position: inside; margin-left: 5px; margin-right: 12px; padding: 4px !important; display: flex; flex-direction: column; } .two-items li { display: flex; } .two-items li > *:first-child { flex: 0 0 50%; text-align: left; } .two-items.thirtyseventy li > *:first-child { flex: 0 0 30%; } ol.sdpi-item-value, .sdpi-item-value > ol[listtype="none"] { list-style-type: none; } ol.sdpi-item-value[type="decimal"], .sdpi-item-value > ol[type="decimal"] { list-style-type: decimal; } ol.sdpi-item-value[type="decimal-leading-zero"], .sdpi-item-value > ol[type="decimal-leading-zero"] { list-style-type: decimal-leading-zero; } ol.sdpi-item-value[type="lower-alpha"], .sdpi-item-value > ol[type="lower-alpha"] { list-style-type: lower-alpha; } ol.sdpi-item-value[type="upper-alpha"], .sdpi-item-value > ol[type="upper-alpha"] { list-style-type: upper-alpha; } ol.sdpi-item-value[type="upper-roman"], .sdpi-item-value > ol[type="upper-roman"] { list-style-type: upper-roman; } ol.sdpi-item-value[type="lower-roman"], .sdpi-item-value > ol[type="lower-roman"] { list-style-type: upper-roman; } tr:nth-child(even), .sdpi-item-value > ul > li:nth-child(even), .sdpi-item-value > ol > li:nth-child(even), li:nth-child(even) { background-color: rgba(0, 0, 0, .2) } td:hover, .sdpi-item-value > ul > li:hover:nth-child(even), .sdpi-item-value > ol > li:hover:nth-child(even), li:hover:nth-child(even), li:hover { background-color: rgba(255, 255, 255, .1); } td.selected, td.selected:hover, li.selected:hover, li.selected { color: white; background-color: #77f; } tr { border: 1px solid var(--sdpi-bordercolor); } td { border-right: 1px solid var(--sdpi-bordercolor); -webkit-user-select: none; } tr:last-child, td:last-child { border: none; } .sdpi-item-value.select, .sdpi-item-value > select { margin-right: 13px; margin-left: 4px; padding: 0px 4px; } .sdpi-item-child, .sdpi-item-group > .sdpi-item > input[type="color"] { margin-top: 0.4em; margin-right: 4px; margin-left: 4px; } .full, .full *, .sdpi-item-value.full, .sdpi-item-child > full > *, .sdpi-item-child.full, .sdpi-item-child.full > *, .full > .sdpi-item-child, .full > .sdpi-item-child > * { display: flex; flex: 1 1 0; margin-bottom: 4px; margin-left: 0px; width: 100%; justify-content: space-evenly; } .sdpi-item-group > .sdpi-item > input[type="color"] { margin-top: 0px; } ::-webkit-calendar-picker-indicator:focus, input[type=file]::-webkit-file-upload-button:focus, button:focus, textarea:focus, input:focus, select:focus, option:focus, details:focus, summary:focus, .custom-select select { outline: none; } summary { cursor: default; -webkit-user-select: none; } .pointer, summary .pointer { cursor: pointer; } details * { font-size: 12px; font-weight: normal; } details.message { padding: 4px 18px 4px 12px; } details.message summary { font-size: 10pt; font-weight: 600; min-height: 18px; } details.message:first-child { margin-top: 4px; margin-left: 0; padding-left: 102px !important; } details.message > summary:first-of-type { line-height: 20px; } details.message h1 { text-align: left; } details:not(.pointer) > summary { list-style: none; } details > summary::-webkit-details-marker .message > summary::-webkit-details-marker { display: none; } .info20, .question, .caution, .info { background-repeat: no-repeat; background-position: 72px center; } .info20 { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A"); } .info { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A"); } .info2 { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A"); } .sdpi-more-info { background-image: linear-gradient(to right, #00000000 0%, #00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A"); } .caution { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A"); } .question { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A"); } .sdpi-more-info { position: fixed; left: 0px; right: 0px; bottom: 0px; min-height: 16px; padding-right: 16px; text-align: right; -webkit-touch-callout: none; cursor: pointer; user-select: none; background-position: right center; background-repeat: no-repeat; border-radius: var(--sdpi-borderradius); text-decoration: none; color: var(--sdpi-color); } .sdpi-more-info-button { display: flex; align-self: right; margin-left: auto; position: fixed; right: 17px; bottom: 0px; user-select: none; } .sdpi-bottom-bar { display: flex; align-self: right; margin-left: auto; position: fixed; right: 17px; bottom: 0px; user-select: none; } .sdpi-bottom-bar.right { right: 0px; } .sdpi-bottom-bar button { min-height: 20px !important; height: 20px !important; } details a { background-position: right !important; min-height: 24px; display: inline-block; line-height: 24px; padding-right: 28px; } input:not([type="range"]), textarea { -webkit-appearance: none; background: var(--sdpi-background); color: var(--sdpi-color); font-weight: normal; font-size: 9pt; border: none; margin-top: 2px; margin-bottom: 2px; min-width: 219px; } textarea + label { display: flex; justify-content: flex-end } input[type="radio"], input[type="checkbox"] { display: none; } input[type="radio"] + label, input[type="checkbox"] + label { font-size: 9pt; color: var(--sdpi-color); font-weight: normal; margin-right: 8px; -webkit-user-select: none; } input[type="radio"] + label:after, input[type="checkbox"] + label:after { content: " " !important; } .sdpi-item[type="radio"] > .sdpi-item-value, .sdpi-item[type="checkbox"] > .sdpi-item-value { padding-top: 2px; } .sdpi-item[type="checkbox"] > .sdpi-item-value > * { margin-top: 4px; } .sdpi-item[type="checkbox"] .sdpi-item-child, .sdpi-item[type="radio"] .sdpi-item-child { display: inline-block; } .sdpi-item[type="range"] .sdpi-item-value, .sdpi-item[type="meter"] .sdpi-item-child, .sdpi-item[type="progress"] .sdpi-item-child { display: flex; } .sdpi-item[type="range"] .sdpi-item-value { min-height: 26px; } .sdpi-item[type="range"] .sdpi-item-value span, .sdpi-item[type="meter"] .sdpi-item-child span, .sdpi-item[type="progress"] .sdpi-item-child span { margin-top: -2px; min-width: 8px; text-align: right; user-select: none; cursor: pointer; -webkit-user-select: none; user-select: none; } .sdpi-item[type="range"] .sdpi-item-value span { margin-top: 7px; text-align: right; } span + input[type="range"] { display: flex; max-width: 168px; } .sdpi-item[type="range"] .sdpi-item-value span:first-child, .sdpi-item[type="meter"] .sdpi-item-child span:first-child, .sdpi-item[type="progress"] .sdpi-item-child span:first-child { margin-right: 4px; } .sdpi-item[type="range"] .sdpi-item-value span:last-child, .sdpi-item[type="meter"] .sdpi-item-child span:last-child, .sdpi-item[type="progress"] .sdpi-item-child span:last-child { margin-left: 4px; } .reverse { transform: rotate(180deg); } .sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child { margin-left: -10px; } .sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child { margin-left: -14px; } .sdpi-item[type="radio"] > .sdpi-item-value > * { margin-top: 2px; } details { padding: 8px 18px 8px 12px; min-width: 86px; } details > h4 { border-bottom: 1px solid var(--sdpi-bordercolor); } legend { display: none; } .sdpi-item-value > textarea { padding: 0px; width: 219px; margin-left: 1px; margin-top: 3px; padding: 4px; } input[type="radio"] + label span, input[type="checkbox"] + label span { display: inline-block; width: 16px; height: 16px; margin: 2px 4px 2px 0; border-radius: 3px; vertical-align: middle; background: var(--sdpi-background); cursor: pointer; border: 1px solid rgb(0, 0, 0, .2); } input[type="radio"] + label span { border-radius: 100%; } input[type="radio"]:checked + label span, input[type="checkbox"]:checked + label span { background-color: #77f; background-image: url(../assets/check.svg); background-repeat: no-repeat; background-position: center center; border: 1px solid rgb(0, 0, 0, .4); } input[type="radio"]:active:checked + label span, input[type="radio"]:active + label span, input[type="checkbox"]:active:checked + label span, input[type="checkbox"]:active + label span { background-color: #303030; } input[type="radio"]:checked + label span { background-image: url(../assets/rcheck.svg); } input[type="range"] { width: var(--sdpi-width); height: 30px; overflow: hidden; cursor: pointer; background: transparent !important; } .sdpi-item > input[type="range"] { margin-left: 2px; max-width: var(--sdpi-width); width: var(--sdpi-width); padding: 0px; margin-top: 2px; } /* input[type="range"], input[type="range"]::-webkit-slider-runnable-track, input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; } */ input[type="range"]::-webkit-slider-runnable-track { height: 5px; background: #979797; border-radius: 3px; padding: 0px !important; border: 1px solid var(--sdpi-background); } input[type="range"]::-webkit-slider-thumb { position: relative; -webkit-appearance: none; background-color: var(--sdpi-color); width: 12px; height: 12px; border-radius: 20px; margin-top: -5px; border: none; } input[type="range" i] { margin: 0; } input[type="range"]::-webkit-slider-thumb::before { position: absolute; content: ""; height: 5px; /* equal to height of runnable track or 1 less */ width: 500px; /* make this bigger than the widest range input element */ left: -502px; /* this should be -2px - width */ top: 8px; /* don't change this */ background: #77f; } input[type="color"] { min-width: 32px; min-height: 32px; width: 32px; height: 32px; padding: 0; background-color: var(--sdpi-bgcolor); flex: none; } ::-webkit-color-swatch { min-width: 24px; } textarea { height: 3em; word-break: break-word; line-height: 1.5em; } .textarea { padding: 0px !important; } textarea { width: 219px; /*98%;*/ height: 96%; min-height: 6em; resize: none; border-radius: var(--sdpi-borderradius); } /* CAROUSEL */ .sdpi-item[type="carousel"] {} .sdpi-item.card-carousel-wrapper, .sdpi-item > .card-carousel-wrapper { padding: 0; } .card-carousel-wrapper { display: flex; align-items: center; justify-content: center; margin: 12px auto; color: #666a73; } .card-carousel { display: flex; justify-content: center; width: 278px; } .card-carousel--overflow-container { overflow: hidden; } .card-carousel--nav__left, .card-carousel--nav__right { /* display: inline-block; */ width: 12px; height: 12px; border-top: 2px solid #42b883; border-right: 2px solid #42b883; cursor: pointer; margin: 0 4px; transition: transform 150ms linear; } .card-carousel--nav__left[disabled], .card-carousel--nav__right[disabled] { opacity: 0.2; border-color: black; } .card-carousel--nav__left { transform: rotate(-135deg); } .card-carousel--nav__left:active { transform: rotate(-135deg) scale(0.85); } .card-carousel--nav__right { transform: rotate(45deg); } .card-carousel--nav__right:active { transform: rotate(45deg) scale(0.85); } .card-carousel-cards { display: flex; transition: transform 150ms ease-out; transform: translatex(0px); } .card-carousel-cards .card-carousel--card { margin: 0 5px; cursor: pointer; /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */ background-color: #fff; border-radius: 4px; z-index: 3; } .xxcard-carousel-cards .card-carousel--card:first-child { margin-left: 0; } .xxcard-carousel-cards .card-carousel--card:last-child { margin-right: 0; } .card-carousel-cards .card-carousel--card img { vertical-align: bottom; border-top-left-radius: 4px; border-top-right-radius: 4px; transition: opacity 150ms linear; width: 60px; } .card-carousel-cards .card-carousel--card img:hover { opacity: 0.5; } .card-carousel-cards .card-carousel--card--footer { border-top: 0; max-width: 80px; overflow: hidden; display: flex; height: 100%; flex-direction: column; } .card-carousel-cards .card-carousel--card--footer p { padding: 3px 0; margin: 0; margin-bottom: 2px; font-size: 15px; font-weight: 500; color: #2c3e50; } .card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) { font-size: 12px; font-weight: 300; padding: 6px; color: #666a73; } h1 { font-size: 1.3em; font-weight: 500; text-align: center; margin-bottom: 12px; } ::-webkit-datetime-edit { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; background: url(../assets/elg_calendar_inv.svg) no-repeat left center; padding-right: 1em; padding-left: 25px; background-position: 4px 0px; } ::-webkit-datetime-edit-fields-wrapper {} ::-webkit-datetime-edit-text { padding: 0 0.3em; } ::-webkit-datetime-edit-month-field {} ::-webkit-datetime-edit-day-field {} ::-webkit-datetime-edit-year-field {} ::-webkit-inner-spin-button { /* display: none; */ } ::-webkit-calendar-picker-indicator { background: transparent; font-size: 17px; } ::-webkit-calendar-picker-indicator:focus { background-color: rgba(0, 0, 0, 0.2); } input[type="date"] { -webkit-align-items: center; display: -webkit-inline-flex; font-family: monospace; overflow: hidden; padding: 0; -webkit-padding-start: 1px; } input::-webkit-datetime-edit { -webkit-flex: 1; -webkit-user-modify: read-only !important; display: inline-block; min-width: 0; overflow: hidden; } /* input::-webkit-datetime-edit-fields-wrapper { -webkit-user-modify: read-only !important; display: inline-block; padding: 1px 0; white-space: pre; } */ /* input[type="date"] { background-color: red; outline: none; } input[type="date"]::-webkit-clear-button { font-size: 18px; height: 30px; position: relative; } input[type="date"]::-webkit-inner-spin-button { height: 28px; } input[type="date"]::-webkit-calendar-picker-indicator { font-size: 15px; } */ input[type="file"] { opacity: 0; display: none; } .sdpi-item > input[type="file"] { opacity: 1; display: flex; } input[type="file"] + span { display: flex; flex: 0 1 auto; background-color: #0000ff50; } label.sdpi-file-label { cursor: pointer; user-select: none; display: inline-block; min-height: 21px !important; height: 21px !important; line-height: 20px; padding: 0px 4px; margin: auto; margin-right: 0px; float: right; } .sdpi-file-label > label:active, .sdpi-file-label.file:active, label.sdpi-file-label:active, label.sdpi-file-info:active, input[type="file"]::-webkit-file-upload-button:active, button:active { background-color: var(--sdpi-color); color: #303030; } input:required:invalid, input:focus:invalid { background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center; } input:required:valid { background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center; } .tooltip, :tooltip, :title { color: yellow; } .sdpi-item-group.file { width: 232px; display: flex; align-items: center; } .sdpi-file-info { overflow-wrap: break-word; word-wrap: break-word; hyphens: auto; min-width: 132px; max-width: 144px; max-height: 32px; margin-top: 0px; margin-left: 5px; display: inline-block; overflow: hidden; padding: 6px 4px; background-color: var(--sdpi-background); } ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); margin: 4px; border-radius: 8px; } ::-webkit-scrollbar-thumb { background-color: #999999; outline: 1px solid slategrey; border-radius: 8px; } a { color: #7397d2; } .testcontainer { display: flex; background-color: #0000ff20; max-width: 400px; height: 200px; align-content: space-evenly; } input[type=range] { -webkit-appearance: none; /* background-color: green; */ height: 6px; margin-top: 12px; z-index: 0; overflow: visible; } /* input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; background-color: var(--sdpi-color); width: 12px; height: 12px; border-radius: 20px; margin-top: -6px; border: none; } */ :-webkit-slider-thumb { -webkit-appearance: none; background-color: var(--sdpi-color); width: 16px; height: 16px; border-radius: 20px; margin-top: -6px; border: 1px solid #999999; } .sdpi-item[type="range"] .sdpi-item-group { display: flex; flex-direction: column; } .xxsdpi-item[type="range"] .sdpi-item-group input { max-width: 204px; } .sdpi-item[type="range"] .sdpi-item-group span { margin-left: 0px !important; } .sdpi-item[type="range"] .sdpi-item-group > .sdpi-item-child { display: flex; flex-direction: row; } .rangeLabel { position: absolute; font-weight: normal; margin-top: 24px; } :disabled { color: #993333; } select, select option { color: var(--sdpi-color); } select.disabled, select option:disabled { color: #fd9494; font-style: italic; } .runningAppsContainer { display: none; } .one-line { min-height: 1.5em; } .two-lines { min-height: 3em; } .three-lines { min-height: 4.5em; } .four-lines { min-height: 6em; } .min80 > .sdpi-item-child { min-width: 80px; } .min100 > .sdpi-item-child { min-width: 100px; } .min120 > .sdpi-item-child { min-width: 120px; } .min140 > .sdpi-item-child { min-width: 140px; } .min160 > .sdpi-item-child { min-width: 160px; } .min200 > .sdpi-item-child { min-width: 200px; } .max40 { flex-basis: 40%; flex-grow: 0; } .max30 { flex-basis: 30%; flex-grow: 0; } .max20 { flex-basis: 20%; flex-grow: 0; } .up20 { margin-top: -20px; } .alignCenter { align-items: center; } .alignTop { align-items: flex-start; } .alignBaseline { align-items: baseline; } .noMargins, .noMargins *, .noInnerMargins * { margin: 0; padding: 0; } .hidden { display: none !important; } .icon-help, .icon-help-line, .icon-help-fill, .icon-help-inv, .icon-brighter, .icon-darker, .icon-warmer, .icon-cooler { min-width: 20px; width: 20px; background-repeat: no-repeat; opacity: 1; } .icon-help:active, .icon-help-line:active, .icon-help-fill:active, .icon-help-inv:active, .icon-brighter:active, .icon-darker:active, .icon-warmer:active, .icon-cooler:active { opacity: 0.5; } .icon-brighter, .icon-darker, .icon-warmer, .icon-cooler { margin-top: 5px !important; } .icon-help, .icon-help-line, .icon-help-fill, .icon-help-inv { cursor: pointer; margin: 0px; margin-left: 4px; } .icon-brighter { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='4'/%3E%3Cpath d='M14.8532861,7.77530426 C14.7173255,7.4682615 14.5540843,7.17599221 14.3666368,6.90157083 L16.6782032,5.5669873 L17.1782032,6.4330127 L14.8532861,7.77530426 Z M10.5,4.5414007 C10.2777625,4.51407201 10.051423,4.5 9.82179677,4.5 C9.71377555,4.5 9.60648167,4.50311409 9.5,4.50925739 L9.5,2 L10.5,2 L10.5,4.5414007 Z M5.38028092,6.75545367 C5.18389364,7.02383457 5.01124349,7.31068015 4.86542112,7.61289977 L2.82179677,6.4330127 L3.32179677,5.5669873 L5.38028092,6.75545367 Z M4.86542112,12.3871002 C5.01124349,12.6893198 5.18389364,12.9761654 5.38028092,13.2445463 L3.32179677,14.4330127 L2.82179677,13.5669873 L4.86542112,12.3871002 Z M9.5,15.4907426 C9.60648167,15.4968859 9.71377555,15.5 9.82179677,15.5 C10.051423,15.5 10.2777625,15.485928 10.5,15.4585993 L10.5,18 L9.5,18 L9.5,15.4907426 Z M14.3666368,13.0984292 C14.5540843,12.8240078 14.7173255,12.5317385 14.8532861,12.2246957 L17.1782032,13.5669873 L16.6782032,14.4330127 L14.3666368,13.0984292 Z'/%3E%3C/g%3E%3C/svg%3E"); } .icon-darker { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 14C7.790861 14 6 12.209139 6 10 6 7.790861 7.790861 6 10 6 12.209139 6 14 7.790861 14 10 14 12.209139 12.209139 14 10 14zM10 13C11.6568542 13 13 11.6568542 13 10 13 8.34314575 11.6568542 7 10 7 8.34314575 7 7 8.34314575 7 10 7 11.6568542 8.34314575 13 10 13zM14.8532861 7.77530426C14.7173255 7.4682615 14.5540843 7.17599221 14.3666368 6.90157083L16.6782032 5.5669873 17.1782032 6.4330127 14.8532861 7.77530426zM10.5 4.5414007C10.2777625 4.51407201 10.051423 4.5 9.82179677 4.5 9.71377555 4.5 9.60648167 4.50311409 9.5 4.50925739L9.5 2 10.5 2 10.5 4.5414007zM5.38028092 6.75545367C5.18389364 7.02383457 5.01124349 7.31068015 4.86542112 7.61289977L2.82179677 6.4330127 3.32179677 5.5669873 5.38028092 6.75545367zM4.86542112 12.3871002C5.01124349 12.6893198 5.18389364 12.9761654 5.38028092 13.2445463L3.32179677 14.4330127 2.82179677 13.5669873 4.86542112 12.3871002zM9.5 15.4907426C9.60648167 15.4968859 9.71377555 15.5 9.82179677 15.5 10.051423 15.5 10.2777625 15.485928 10.5 15.4585993L10.5 18 9.5 18 9.5 15.4907426zM14.3666368 13.0984292C14.5540843 12.8240078 14.7173255 12.5317385 14.8532861 12.2246957L17.1782032 13.5669873 16.6782032 14.4330127 14.3666368 13.0984292z'/%3E%3C/g%3E%3C/svg%3E"); } .icon-warmer { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M12.3247275 11.4890349C12.0406216 11.0007637 11.6761954 10.5649925 11.2495475 10.1998198 11.0890394 9.83238991 11 9.42659309 11 9 11 7.34314575 12.3431458 6 14 6 15.6568542 6 17 7.34314575 17 9 17 10.6568542 15.6568542 12 14 12 13.3795687 12 12.8031265 11.8116603 12.3247275 11.4890349zM17.6232392 11.6692284C17.8205899 11.4017892 17.9890383 11.1117186 18.123974 10.8036272L20.3121778 12.0669873 19.8121778 12.9330127 17.6232392 11.6692284zM18.123974 7.19637279C17.9890383 6.88828142 17.8205899 6.5982108 17.6232392 6.33077158L19.8121778 5.0669873 20.3121778 5.9330127 18.123974 7.19637279zM14.5 4.52746439C14.3358331 4.50931666 14.1690045 4.5 14 4.5 13.8309955 4.5 13.6641669 4.50931666 13.5 4.52746439L13.5 2 14.5 2 14.5 4.52746439zM13.5 13.4725356C13.6641669 13.4906833 13.8309955 13.5 14 13.5 14.1690045 13.5 14.3358331 13.4906833 14.5 13.4725356L14.5 16 13.5 16 13.5 13.4725356zM14 11C15.1045695 11 16 10.1045695 16 9 16 7.8954305 15.1045695 7 14 7 12.8954305 7 12 7.8954305 12 9 12 10.1045695 12.8954305 11 14 11zM9.5 11C10.6651924 11.4118364 11.5 12.5 11.5 14 11.5 16 10 17.5 8 17.5 6 17.5 4.5 16 4.5 14 4.5 12.6937812 5 11.5 6.5 11L6.5 7 9.5 7 9.5 11z'/%3E%3Cpath d='M12,14 C12,16.209139 10.209139,18 8,18 C5.790861,18 4,16.209139 4,14 C4,12.5194353 4.80439726,11.2267476 6,10.5351288 L6,4 C6,2.8954305 6.8954305,2 8,2 C9.1045695,2 10,2.8954305 10,4 L10,10.5351288 C11.1956027,11.2267476 12,12.5194353 12,14 Z M11,14 C11,12.6937812 10.1651924,11.5825421 9,11.1707057 L9,4 C9,3.44771525 8.55228475,3 8,3 C7.44771525,3 7,3.44771525 7,4 L7,11.1707057 C5.83480763,11.5825421 5,12.6937812 5,14 C5,15.6568542 6.34314575,17 8,17 C9.65685425,17 11,15.6568542 11,14 Z'/%3E%3C/g%3E%3C/svg%3E"); } .icon-cooler { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10.4004569 11.6239517C10.0554735 10.9863849 9.57597206 10.4322632 9 9.99963381L9 9.7450467 9.53471338 9.7450467 10.8155381 8.46422201C10.7766941 8.39376637 10.7419749 8.32071759 10.7117062 8.2454012L9 8.2454012 9 6.96057868 10.6417702 6.96057868C10.6677696 6.86753378 10.7003289 6.77722682 10.7389179 6.69018783L9.44918707 5.40045694 9 5.40045694 9 4.34532219 9.32816127 4.34532219 9.34532219 2.91912025 10.4004569 2.91912025 10.4004569 4.53471338 11.6098599 5.74411634C11.7208059 5.68343597 11.8381332 5.63296451 11.9605787 5.59396526L11.9605787 3.8884898 10.8181818 2.74609294 11.5642748 2 12.5727518 3.00847706 13.5812289 2 14.3273218 2.74609294 13.2454012 3.82801356 13.2454012 5.61756719C13.3449693 5.65339299 13.4408747 5.69689391 13.5324038 5.74735625L14.7450467 4.53471338 14.7450467 2.91912025 15.8001815 2.91912025 15.8001815 4.34532219 17.2263834 4.34532219 17.2263834 5.40045694 15.6963166 5.40045694 14.4002441 6.69652946C14.437611 6.78161093 14.4692249 6.86979146 14.4945934 6.96057868L16.2570138 6.96057868 17.3994107 5.81818182 18.1455036 6.56427476 17.1370266 7.57275182 18.1455036 8.58122888 17.3994107 9.32732182 16.3174901 8.2454012 14.4246574 8.2454012C14.3952328 8.31861737 14.3616024 8.38969062 14.3240655 8.45832192L15.6107903 9.7450467 17.2263834 9.7450467 17.2263834 10.8001815 15.8001815 10.8001815 15.8001815 12.2263834 14.7450467 12.2263834 14.7450467 10.6963166 13.377994 9.32926387C13.3345872 9.34850842 13.2903677 9.36625331 13.2454012 9.38243281L13.2454012 11.3174901 14.3273218 12.3994107 13.5812289 13.1455036 12.5848864 12.1491612 11.5642748 13.1455036 10.8181818 12.3994107 11.9605787 11.2570138 11.9605787 9.40603474C11.8936938 9.38473169 11.828336 9.36000556 11.7647113 9.33206224L10.4004569 10.6963166 10.4004569 11.6239517zM12.75 8.5C13.3022847 8.5 13.75 8.05228475 13.75 7.5 13.75 6.94771525 13.3022847 6.5 12.75 6.5 12.1977153 6.5 11.75 6.94771525 11.75 7.5 11.75 8.05228475 12.1977153 8.5 12.75 8.5zM9.5 14C8.5 16.3333333 7.33333333 17.5 6 17.5 4.66666667 17.5 3.5 16.3333333 2.5 14L9.5 14z'/%3E%3Cpath d='M10,14 C10,16.209139 8.209139,18 6,18 C3.790861,18 2,16.209139 2,14 C2,12.5194353 2.80439726,11.2267476 4,10.5351288 L4,4 C4,2.8954305 4.8954305,2 6,2 C7.1045695,2 8,2.8954305 8,4 L8,10.5351288 C9.19560274,11.2267476 10,12.5194353 10,14 Z M9,14 C9,12.6937812 8.16519237,11.5825421 7,11.1707057 L7,4 C7,3.44771525 6.55228475,3 6,3 C5.44771525,3 5,3.44771525 5,4 L5,11.1707057 C3.83480763,11.5825421 3,12.6937812 3,14 C3,15.6568542 4.34314575,17 6,17 C7.65685425,17 9,15.6568542 9,14 Z'/%3E%3C/g%3E%3C/svg%3E"); } .icon-help { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' d='M11.292 12.516l.022 1.782H9.07v-1.804c0-1.98 1.276-2.574 2.662-3.278h-.022c.814-.44 1.65-.88 1.694-2.2.044-1.386-1.122-2.728-3.234-2.728-1.518 0-2.662.902-3.366 2.354L5 5.608C5.946 3.584 7.662 2 10.17 2c3.564 0 5.632 2.442 5.588 5.06-.066 2.618-1.716 3.41-3.102 4.158-.704.374-1.364.682-1.364 1.298zm-1.122 2.442c.858 0 1.452.594 1.452 1.452 0 .682-.594 1.408-1.452 1.408-.77 0-1.386-.726-1.386-1.408 0-.858.616-1.452 1.386-1.452z'/%3E%3C/svg%3E"); } .icon-help-line { background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-1a9 9 0 1 0 0-18 9 9 0 0 0 0 18z'/%3E%3Cpath d='M10.848 12.307l.02 1.578H8.784v-1.597c0-1.753 1.186-2.278 2.474-2.901h-.02c.756-.39 1.533-.78 1.574-1.948.041-1.226-1.043-2.414-3.006-2.414-1.41 0-2.474.798-3.128 2.083L5 6.193C5.88 4.402 7.474 3 9.805 3 13.118 3 15.04 5.161 15 7.478c-.061 2.318-1.595 3.019-2.883 3.68-.654.332-1.268.604-1.268 1.15zM9.805 14.47c.798 0 1.35.525 1.35 1.285 0 .603-.552 1.246-1.35 1.246-.715 0-1.288-.643-1.288-1.246 0-.76.573-1.285 1.288-1.285z' fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E"); } .icon-help-fill { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23999'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M8.368 7.189H5C5 3.5 7.668 2 10.292 2 13.966 2 16 4.076 16 7.012c0 3.754-3.849 3.136-3.849 5.211v1.656H8.455v-1.832c0-2.164 1.4-2.893 2.778-3.6.437-.242 1.006-.574 1.006-1.236 0-2.208-3.871-2.142-3.871-.022zM10.25 18a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5z'/%3E%3C/g%3E%3C/svg%3E"); } .icon-help-inv { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zM8.368 7.189c0-2.12 3.87-2.186 3.87.022 0 .662-.568.994-1.005 1.236-1.378.707-2.778 1.436-2.778 3.6v1.832h3.696v-1.656c0-2.075 3.849-1.457 3.849-5.21C16 4.075 13.966 2 10.292 2 7.668 2 5 3.501 5 7.189h3.368zM10.25 18a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z'/%3E%3C/svg%3E"); } .kelvin::after { content: "K"; } .mired::after { content: " Mired"; } .percent::after { content: "%"; } .sdpi-item-value + .icon-cooler, .sdpi-item-value + .icon-warmer { margin-left: 0px !important; margin-top: 15px !important; } /** CONTROL-CENTER STYLES */ input[type="range"].colorbrightness::-webkit-slider-runnable-track, input[type="range"].colortemperature::-webkit-slider-runnable-track { height: 8px; background: #979797; border-radius: 4px; background-image: linear-gradient(to right, #94d0ec, #ffb165); } input[type="range"].colorbrightness::-webkit-slider-runnable-track { background-color: #efefef; background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0)); } input[type="range"].colorbrightness::-webkit-slider-thumb, input[type="range"].colortemperature::-webkit-slider-thumb { width: 16px; height: 16px; border-radius: 20px; margin-top: -5px; background-color: #86c6e8; box-shadow: 0px 0px 1px #000000; border: 1px solid #d8d8d8; } .sdpi-info-label { display: inline-block; user-select: none; position: absolute; height: 15px; width: auto; text-align: center; border-radius: 4px; min-width: 44px; max-width: 80px; background: white; font-size: 11px; color: black; z-index: 1000; box-shadow: 0px 0px 12px rgba(0, 0, 0, .8); padding: 2px; } .sdpi-info-label.hidden { opacity: 0; transition: opacity 0.25s linear; } .sdpi-info-label.shown { position: absolute; opacity: 1; transition: opacity 0.25s ease-out; } /* adding some styles here that override sdpi things so we can use this as notes for sdpi updates*/ select { min-width: 0px; /* this is a clunky fix for using background image as select arrow with long text options */ -webkit-appearance: media-slider; text-overflow: ellipsis; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/index.html ================================================ com.elgato.philips-hue.pi
================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/brightnessPI.js ================================================ /** @file brightnessPI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function BrightnessPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion, isEncoder) { // Init BrightnessPI let instance = this; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Before overwriting parent method, save a copy of it let piLocalize = this.localize; // Localize the UI this.localize = () => { // Call PIs localize method piLocalize.call(instance); // Localize the brightness label document.getElementById('brightness-label').innerHTML = instance.localization['Brightness']; if(isEncoder) { document.getElementById('scaleticks-label').innerHTML = instance.localization['Scale Ticks'] || 'Scale Ticks'; } }; const values = [1,2,3,4,5,10,20]; const selectedIndex = values.indexOf(Number(settings.scaleTicks)); // Add brightness slider document.getElementById('placeholder').innerHTML = `
${this.getEncoderOptions(settings.scaleTicks, isEncoder)} `; console.log("value:", selectedIndex, settings.scaleTicks, typeof settings.scaleTicks, document.getElementById('placeholder').innerHTML); // Initialize the tooltips initToolTips(); // Add event listener document.getElementById('brightness-input').addEventListener('change', brightnessChanged); if(isEncoder) { document.getElementById('scaleticks-input').addEventListener('change', scaleticksChanged); } // Brightness changed function brightnessChanged(inEvent) { // Save the new brightness settings settings.brightness = inEvent.target.value; instance.saveSettings(); // Inform the plugin that a new brightness is set instance.sendToPlugin({ piEvent: 'valueChanged', }); } function scaleticksChanged(inEvent) { settings.scaleTicks = inEvent.target.value; instance.saveSettings(); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/brightnessRelPI.js ================================================ /** @file brightnessRelPI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function BrightnessRelPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Init BrightnessPI let instance = this; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Before overwriting parent method, save a copy of it let piLocalize = this.localize; // Localize the UI this.localize = function() { // Call PIs localize method piLocalize.call(instance); // Localize the brightness label document.getElementById('brightness-rel-label').innerHTML = instance.localization['Steps']; }; // Add steps slider document.getElementById('placeholder').innerHTML = `
`; // Initialize the tooltips initToolTips(); // Add event listener document.getElementById('brightness-rel-input').addEventListener('change', brightnessRelChanged); // Brightness changed function brightnessRelChanged(inEvent) { // Save the new brightness settings settings.brightnessRel = inEvent.target.value; instance.saveSettings(); // Inform the plugin that a new brightness is set instance.sendToPlugin({ 'piEvent': 'valueChanged' }); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/colorPI.js ================================================ /** @file colorPI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function ColorPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Init ColorPI let instance = this; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Add event listener document.getElementById('light-select').addEventListener('change', lightChanged); // Color changed function colorChanged(inEvent) { // Get the selected color let color = inEvent.target.value; // If the color is hex if (color.charAt(0) === '#') { // Convert the color to HSV let hsv = Bridge.hex2hsv(color); // Check if the color is valid if (hsv.v !== 1) { // Remove brightness component hsv.v = 1; // Set the color to the corrected color color = Bridge.hsv2hex(hsv); } } // Save the new color settings.color = color; instance.saveSettings(); // Inform the plugin that a new color is set instance.sendToPlugin({ piEvent: 'valueChanged', }); } // Light changed function lightChanged() { // Get the light value manually // Because it is not set if this function was triggered via a CustomEvent let lightID = document.getElementById('light-select').value; // Don't show any color picker if no light or group is set if (lightID === 'no-lights' || lightID === 'no-groups') { return; } // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache)) { return; } // Find the configured bridge let bridgeCache = cache[settings.bridge]; // Check if the selected light or group is in the cache if (!(lightID in bridgeCache.lights || lightID in bridgeCache.groups)) { return; } // Get light or group cache let lightCache; if (lightID.indexOf('l') !== -1) { lightCache = bridgeCache.lights[lightID]; } else { lightCache = bridgeCache.groups[lightID]; } // Add full color picker or only temperature slider let colorPicker; if (lightCache.xy !== null) { colorPicker = `
${instance.localization['Color']}
`; } else { colorPicker = `
${instance.localization['Temperature']}
`; } // Add color picker document.getElementById('placeholder').innerHTML = colorPicker; // Initialize the tooltips initToolTips(); // Add event listener document.getElementById('color-input').addEventListener('change', colorChanged); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/cyclePI.js ================================================ /** @file cyclePI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function CyclePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Init CyclePI let instance = this; // Maximum amount of Colors let maxColors = 10; // Current amount of Colors let curColors = settings?.colors?.length || 0; // Default color for new pickers let defaultColor = "#ffffff"; // Default temperature for new pickers let defaultTemperature = 2000; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Add event listener document.getElementById('light-select').addEventListener('change', lightChanged); // Color changed function colorChanged(inEvent) { // Get the selected index and color let index = inEvent.target.dataset.id; let color = inEvent.target.value; // If the color is hex if (color.charAt(0) === '#') { // Convert the color to HSV let hsv = Bridge.hex2hsv(color); // Check if the color is valid if (hsv.v !== 1) { // Remove brightness component hsv.v = 1; // Set the color to the corrected color color = Bridge.hsv2hex(hsv); } } // Save the new color settings.colors[index] = color; instance.saveSettings(); // Inform the plugin that a new color is set instance.sendToPlugin({ piEvent: 'valueChanged', }); } // Light changed function lightChanged() { // Get the light value manually // Because it is not set if this function was triggered via a CustomEvent let lightID = document.getElementById('light-select').value; // Don't show any color picker if no light or group is set if (lightID === 'no-lights' || lightID === 'no-groups') { return; } // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache)) { return; } // Find the configured bridge let bridgeCache = cache[settings.bridge]; // Check if the selected light or group is in the cache if (!(lightID in bridgeCache.lights || lightID in bridgeCache.groups)) { return; } // Get light or group cache let lightCache; if (lightID.indexOf('l') !== -1) { lightCache = bridgeCache.lights[lightID]; } else { lightCache = bridgeCache.groups[lightID]; } // Get html of color picker or temperature slider let getColorPicker = i => { let colorIndex = i - 1; if (lightCache.xy != null) { if (i === 0) { return `
${instance.localization['Colors']}
`; } else { return ` `; } } else if (i > 0) { return `
${instance.localization['Temperature']} ${i}
`; } return ''; }; let placeholder = document.getElementById('placeholder'); // Add a new color picker to document let addColorPicker = i => { let picker = document.createElement('div'); const cphtml = getColorPicker(i).trim(); picker.innerHTML = cphtml; if (lightCache.xy != null) { document.querySelector('#color-input-container .sdpi-item-value').append(picker.firstChild); } else { placeholder.insertBefore(picker.firstChild, document.getElementById('cycle-buttons')); } document.getElementById('color-input-' + (i - 1)).addEventListener('change', colorChanged); }; // Add first color pickers container and buttons placeholder.innerHTML = getColorPicker(0) + `
`; // Initial create color pickers from settings for (let n = 1; n <= settings.colors.length; n++) { addColorPicker(n); } // Get buttons for later usage let addButton = document.getElementById('add-color'); let removeButton = document.getElementById('remove-color'); let checkButtonStates = () => { // Hide add button when reached max color pickers addButton.style.display = curColors >= maxColors ? 'none' : 'inline-block'; // Hide remove button when only two color pickers left removeButton.style.display = curColors <= 2 ? 'none' : 'inline-block'; }; // Event listener for add color addButton.addEventListener('click', () => { addColorPicker((++curColors)); // Add new picker value to settings let colorIndex = curColors - 1; if (!settings.colors[colorIndex]) { if (lightCache.xy != null) { settings.colors[colorIndex] = defaultColor; } else { settings.colors[colorIndex] = defaultTemperature; } instance.saveSettings(); } checkButtonStates(); }); // Event listener for remove last color removeButton.addEventListener('click', () => { document.getElementById('color-input-container-' + (--curColors)).remove(); // Remove color from settings settings.colors = settings.colors.splice(0, settings.colors.length - 1); instance.saveSettings(); checkButtonStates(); }); // Initial button states checkButtonStates(); // Initialize the tooltips initToolTips(); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/main.js ================================================ /** @file main.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Global web socket var websocket = null; // Global plugin settings var globalSettings = {}; // Global settings var settings = {}; // Global cache var cache = {}; // Setup the websocket and handle communication function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) { // Parse parameter from string to object let actionInfo = JSON.parse(inActionInfo); let info = JSON.parse(inInfo); let isEncoder = actionInfo?.payload?.controller == 'Encoder'; let streamDeckVersion = info['application']['version']; let pluginVersion = info['plugin']['version']; // Save global settings settings = actionInfo['payload']['settings']; // Retrieve language let language = info['application']['language']; // Retrieve action identifier let action = actionInfo['action']; // Open the web socket to Stream Deck // Use 127.0.0.1 because Windows needs 300ms to resolve localhost websocket = new WebSocket(`ws://127.0.0.1:${inPort}`); // WebSocket is connected, send message websocket.onopen = () => { // Register property inspector to Stream Deck registerPluginOrPI(inRegisterEvent, inUUID); // Request the global settings of the plugin requestGlobalSettings(inUUID); }; // Create actions let pi; if (action === 'com.elgato.philips-hue.power') { pi = new PowerPI(inUUID, language, streamDeckVersion, pluginVersion); } else if (action === 'com.elgato.philips-hue.color') { pi = new ColorPI(inUUID, language, streamDeckVersion, pluginVersion); } else if (action === 'com.elgato.philips-hue.cycle') { pi = new CyclePI(inUUID, language, streamDeckVersion, pluginVersion); } else if (action === 'com.elgato.philips-hue.brightness') { pi = new BrightnessPI(inUUID, language, streamDeckVersion, pluginVersion, isEncoder); } else if (action === 'com.elgato.philips-hue.temperature') { pi = new TemperaturePI(inUUID, language, streamDeckVersion, pluginVersion, isEncoder); } else if (action === 'com.elgato.philips-hue.scene') { pi = new ScenePI(inUUID, language, streamDeckVersion, pluginVersion); } else if (action === 'com.elgato.philips-hue.brightness-rel') { pi = new BrightnessRelPI(inUUID, language, streamDeckVersion, pluginVersion); } websocket.onmessage = msg => { // Received message from Stream Deck let jsonObj = JSON.parse(msg.data); let event = jsonObj['event']; let jsonPayload = jsonObj['payload']; if (event === 'didReceiveGlobalSettings') { // Set global plugin settings globalSettings = jsonPayload['settings']; } else if (event === 'didReceiveSettings') { // Save global settings after default was set settings = jsonPayload['settings']; } else if (event === 'sendToPropertyInspector') { // Save global cache cache = jsonPayload; // Load bridges and lights pi.loadBridges(); } }; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/pi.js ================================================ /** @file pi.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function PI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Init PI let instance = this; const values = [1,2,3,4,5,10,20]; this.getEncoderOptions = (settingsValue, forEncoder) => { const selectedIndex = values.indexOf(Number(settingsValue)); return forEncoder === true ? `
`: '' }; // Public localizations for the UI this.localization = {}; // Add event listener document.getElementById('bridge-select').addEventListener('change', bridgeChanged); document.getElementById('light-select').addEventListener('change', lightsChanged); document.addEventListener('saveBridge', setupCallback); // Load the localizations getLocalization(inLanguage, (inStatus, inLocalization) => { if (inStatus) { // Save public localization instance.localization = inLocalization['PI']; // Localize the PI instance.localize(); } else { log(inLocalization); } }); // Localize the UI this.localize = () => { // Check if localizations were loaded if (instance.localization == null) { return; } // Localize the bridge select document.getElementById('bridge-label').innerHTML = instance.localization['Bridge']; document.getElementById('no-bridges').innerHTML = instance.localization['NoBridges']; document.getElementById('add-bridge').innerHTML = instance.localization['AddBridge']; // Localize the light and group select document.getElementById('lights-label').innerHTML = instance.localization['Lights']; document.getElementById('lights').label = instance.localization['LightsTitle']; document.getElementById('no-lights').innerHTML = instance.localization['NoLights']; document.getElementById('no-groups').innerHTML = instance.localization['NoGroups']; // Groups label is removed for scenes PI if (document.getElementById('groups') != null) { document.getElementById('groups').label = instance.localization['GroupsTitle']; } }; // Show all paired bridges this.loadBridges = () => { // Remove previously shown bridges let bridges = document.getElementsByClassName('bridges'); while (bridges.length > 0) { bridges[0].parentNode.removeChild(bridges[0]); } // Check if any bridge is paired if (Object.keys(cache).length > 0) { // Hide the 'No Bridges' option document.getElementById('no-bridges').style.display = 'none'; // Sort the bridges alphabetically let bridgeIDsSorted = Object.keys(cache).sort((a, b) => { return cache[a].name.localeCompare(cache[b].name); }); // Add the bridges bridgeIDsSorted.forEach(inBridgeID => { // Add the group let option = ` `; document.getElementById('no-bridges').insertAdjacentHTML('beforebegin', option); }); // Check if the bridge is already configured if (settings.bridge !== undefined) { // Select the currently configured bridge document.getElementById('bridge-select').value = settings.bridge; } // Load the lights loadLights(); } else { // Show the 'No Bridges' option document.getElementById('no-bridges').style.display = 'block'; } // Show PI document.getElementById('pi').style.display = 'block'; } // Show all lights function loadLights() { // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache)) { return; } // Find the configured bridge let bridgeCache = cache[settings.bridge]; // Remove previously shown lights let lights = document.getElementsByClassName('lights'); while (lights.length > 0) { lights[0].parentNode.removeChild(lights[0]); } let requireTemperature = instance instanceof ColorPI || instance instanceof TemperaturePI; // Check if the bridge has at least one light if (Object.keys(bridgeCache.lights).length > 0) { // Hide the 'No Light' option document.getElementById('no-lights').style.display = 'none'; // Sort the lights alphabetically let lightIDsSorted = Object.keys(bridgeCache.lights).sort((a, b) => { return bridgeCache.lights[a].name.localeCompare(bridgeCache.lights[b].name); }); // Add the lights lightIDsSorted.forEach(inLightID => { let light = bridgeCache.lights[inLightID]; // Check if this is a color action and the lights supports colors if (!(requireTemperature && light.temperature == null && light.xy == null)) { // Add the light let option = ` `; document.getElementById('no-lights').insertAdjacentHTML('beforebegin', option); } }); } else { // Show the 'No Light' option document.getElementById('no-lights').style.display = 'block'; } // Remove previously shown groups let groups = document.getElementsByClassName('groups'); while (groups.length > 0) { groups[0].parentNode.removeChild(groups[0]); } // Check if the bridge has at least one group if (Object.keys(bridgeCache.groups).length > 0) { // Hide the 'No Group' option document.getElementById('no-groups').style.display = 'none'; // Sort the groups alphabetically let groupIDsSorted = Object.keys(bridgeCache.groups).sort((a, b) => { return bridgeCache.groups[a].name.localeCompare(bridgeCache.groups[b].name); }); // Add the groups groupIDsSorted.forEach(inGroupID => { let group = bridgeCache.groups[inGroupID]; // Check if this is a color action and the lights supports colors if (!(instance instanceof ColorPI && group.temperature == null && group.xy == null)) { // Add the group let option = ` `; document.getElementById('no-groups').insertAdjacentHTML('beforebegin', option); } }); } else { // Show the 'No Group' option document.getElementById('no-groups').style.display = 'block'; } // Check if a light is already setup if (settings.light !== undefined) { // Check if the configured light or group is part of the bridge cache if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) { return; } // Select the currently configured light or group document.getElementById('light-select').value = settings.light; // Dispatch light change event manually // So that the colorPI can set the correct color picker at initialization document.getElementById('light-select').dispatchEvent(new CustomEvent('change', {'detail': {'manual': true}} )); } // If this is a scene PI if (instance instanceof ScenePI) { //Load the scenes instance.loadScenes(); } } // Function called on successful bridge pairing function setupCallback(inEvent) { // Set bridge to the newly added bridge settings.bridge = inEvent.detail.id; instance.saveSettings(); // Check if global settings need to be initialized if (globalSettings.bridges === undefined) { globalSettings.bridges = {}; } // Add new bridge to the global settings globalSettings.bridges[inEvent.detail.id] = { ip: inEvent.detail.ip, id: inEvent.detail.id, username: inEvent.detail.username, }; saveGlobalSettings(inContext); } // Bridge select changed function bridgeChanged(inEvent) { if (inEvent.target.value === 'add') { // Open setup window window.open(`../setup/index.html?language=${inLanguage}&streamDeckVersion=${inStreamDeckVersion}&pluginVersion=${inPluginVersion}`); // Select the first in case user cancels the setup document.getElementById('bridge-select').selectedIndex = 0; } else if (inEvent.target.value === 'no-bridges') { // If no bridge was selected, do nothing } else { settings.bridge = inEvent.target.value; instance.saveSettings(); instance.loadBridges(); } } // Light select changed function lightsChanged(inEvent) { if (inEvent.target.value === 'no-lights' || inEvent.target.value === 'no-groups') { // If no light or group was selected, do nothing } else if (inEvent.detail !== undefined) { // If the light was changed via code if (inEvent.detail.manual === true) { // do nothing } } else { settings.light = inEvent.target.value; instance.saveSettings(); // If this is a scene PI if (instance instanceof ScenePI) { //Load the scenes instance.loadScenes(); } instance.sendToPlugin({ piEvent: 'lightsChanged', }); } } // Private function to return the action identifier function getAction() { let action // Find out type of action if (instance instanceof PowerPI) { action = 'com.elgato.philips-hue.power'; } else if (instance instanceof ColorPI) { action = 'com.elgato.philips-hue.color'; } else if (instance instanceof CyclePI) { action = 'com.elgato.philips-hue.cycle'; } else if (instance instanceof BrightnessPI) { action = 'com.elgato.philips-hue.brightness'; } else if (instance instanceof TemperaturePI) { action = 'com.elgato.philips-hue.temperature'; } else if (instance instanceof BrightnessRelPI) { action = 'com.elgato.philips-hue.brightness-rel'; } else if (instance instanceof ScenePI) { action = 'com.elgato.philips-hue.scene'; } return action; } // Public function to save the settings this.saveSettings = () => { saveSettings(getAction(), inContext, settings); } // Public function to send data to the plugin this.sendToPlugin = inData => { sendToPlugin(getAction(), inContext, inData); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/powerPI.js ================================================ /** @file powerPI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function PowerPI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/scenePI.js ================================================ /** @file scenePI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function ScenePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion) { // Init ScenePI let instance = this; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Hide lights from light select document.getElementById('lights').style.display = 'none'; // Remove groups label from lights select let groups = document.getElementById('groups'); let groupsChildren = document.getElementById('groups').children; let lightSelect = document.getElementById('light-select'); lightSelect.removeChild(groups); lightSelect.appendChild(groupsChildren[0]); // Before overwriting parent method, save a copy of it let piLocalize = this.localize; // Localize the UI this.localize = () => { // Call PIs localize method piLocalize.call(instance); // Localize the scene select document.getElementById('lights-label').innerHTML = instance.localization['Group']; document.getElementById('scene-label').innerHTML = instance.localization['Scene']; document.getElementById('no-scenes').innerHTML = instance.localization['NoScenes']; }; // Add scene select document.getElementById('placeholder').innerHTML = `
`; // Add event listener document.getElementById('scene-select').addEventListener('change', sceneChanged); // Scenes changed function sceneChanged(inEvent) { if (inEvent.target.value === 'no-scenes') { // do nothing } else { // Save the new scene settings settings.scene = inEvent.target.value; instance.saveSettings(); // Inform the plugin that a new scene is set instance.sendToPlugin({ piEvent: 'valueChanged', }); } } // Show all scenes this.loadScenes = () => { // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache)) { return; } // Find the configured bridge let bridgeCache = cache[settings.bridge]; // Check if any light is configured if (!('light' in settings)) { return; } // Check if the light was set to a group if (!(settings.light.indexOf('g-') !== -1)) { return; } // Check if the configured group is in the cache if (!(settings.light in bridgeCache.groups)) { return; } // Find the configured group let groupCache = bridgeCache.groups[settings.light]; // Remove previously shown scenes let scenes = document.getElementsByClassName('scenes'); while (scenes.length > 0) { scenes[0].parentNode.removeChild(scenes[0]); } // Check if the group has at least one scene if (Object.keys(groupCache.scenes).length > 0) { // Hide the 'No Scenes' option document.getElementById('no-scenes').style.display = 'none'; // Sort the scenes alphabetically let sceneIDsSorted = Object.keys(groupCache.scenes).sort((a, b) => { return groupCache.scenes[a].name.localeCompare(groupCache.scenes[b].name); }); // Add the scenes sceneIDsSorted.forEach((inSceneID) => { // Add the scene let scene = groupCache.scenes[inSceneID]; let option = ``; document.getElementById('no-scenes').insertAdjacentHTML('beforebegin', option); }); } else { // Show the 'No Scenes' option document.getElementById('no-scenes').style.display = 'block'; } // Check if scene is already setup if (settings.scene !== undefined) { // Check if the configured scene is in this group if (!(settings.scene in groupCache.scenes)) { return; } // Select the currently configured scene document.getElementById('scene-select').value = settings.scene; } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/temperaturePI.js ================================================ /** @file temperaturePI.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function TemperaturePI(inContext, inLanguage, inStreamDeckVersion, inPluginVersion, isEncoder) { // Init TemperaturePI let instance = this; // Inherit from PI PI.call(this, inContext, inLanguage, inStreamDeckVersion, inPluginVersion); // Before overwriting parent method, save a copy of it let piLocalize = this.localize; // Localize the UI this.localize = () => { // Call PIs localize method piLocalize.call(instance); // Localize the brightness label document.getElementById('temperature-label').innerHTML = instance.localization['Temperature']; if(isEncoder) { document.getElementById('scaleticks-label').innerHTML = instance.localization['Scale Ticks'] || 'Scale Ticks'; } }; // Add brightness slider document.getElementById('placeholder').innerHTML = `
0% 100%
${this.getEncoderOptions(settings.scaleTicks, isEncoder)} `; // Initialize the tooltips initToolTips(); // Add event listener document.getElementById('temperature-input').addEventListener('input', temperatureChanged); if(isEncoder) { document.getElementById('scaleticks-input').addEventListener('change', scaleticksChanged); } // Brightness changed function temperatureChanged(inEvent) { // Save the new brightness settings settings.temperature = inEvent.target.value; instance.saveSettings(); // Inform the plugin that a new brightness is set instance.sendToPlugin({ piEvent: 'valueChanged', }); } function scaleticksChanged(inEvent) { settings.scaleTicks = inEvent.target.value; instance.saveSettings(); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/pi/js/tooltips.js ================================================ //============================================================================== /** @file tooltips.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. This source code is licensed under the MIT-style license found in the LICENSE file. **/ //============================================================================== function rangeToPercent(value, min, max) { return ((value - min) / (max - min)); } function initToolTips() { const tooltip = document.querySelector('.sdpi-info-label'); const arrElements = document.querySelectorAll('.floating-tooltip'); arrElements.forEach((e,i) => { initToolTip(e, tooltip) }) } function initToolTip(element, tooltip) { const tw = tooltip.getBoundingClientRect().width; const suffix = element.getAttribute('data-suffix') || ''; const updateTooltip = () => { const elementRect = element.getBoundingClientRect(); const w = elementRect.width - tw / 2; const percent = rangeToPercent( element.value, element.min, element.max, ); tooltip.textContent = suffix !== '' ? `${element.value} ${suffix}` : String(element.value); tooltip.style.left = `${elementRect.left + Math.round(w * percent) - tw / 4}px`; tooltip.style.top = `${elementRect.top - 32}px`; }; if (element) { element.addEventListener('mouseenter', () => { tooltip.classList.remove('hidden'); tooltip.classList.add('shown'); updateTooltip(); }, false); element.addEventListener('mouseout', () => { tooltip.classList.remove('shown'); tooltip.classList.add('hidden'); updateTooltip(); }, false); element.addEventListener('input', updateTooltip, false); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/index.html ================================================ com.elgato.philips-hue ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/action.js ================================================ /** @file action.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents an action function Action(inContext, inSettings, jsn) { // Init Action let instance = this; let debounceDelay = 50; // Private variable containing the context of the action let context = inContext; this.isEncoder = jsn?.payload?.controller == 'Encoder'; this.isInMultiAction = jsn?.payload?.isInMultiAction; this.savedValue = -1; this.savedPower = null; // Private variable containing the settings of the action let settings = inSettings; let updateActionsEvent = new CustomEvent('updateActions', {detail: {sender: this}} ); // Set the default values setDefaults(); // Public function returning the context this.getContext = () => { return context; }; // Public function returning the settings this.getSettings = () => { return settings; }; // Public function for settings the settings this.setSettings = inSettings => { settings = inSettings; }; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Set default settings setDefaults(inCallback); }; this.updateAllActions = () => { document.dispatchEvent(updateActionsEvent); }; this.updateActionIfCacheAvailable = (ctx) => { // update the action and its display const cacheSize = Object.keys(cache.data).length; if(cacheSize === 0) { // after a willAppear event, the cache is not yet available wait(1000).then(() => { this.updateAction(); }); } else { this.updateAction(); } } this.setFeedback = (context, value, opacity) => { console.assert(websocket, 'no connection to websocket'); if(websocket && this.isEncoder) { // send the values to the encoder (SD+) setFeedback(context, { value: { value, opacity }, indicator: { value, opacity } }); } }; this.updateDisplay = (lightOrGroup, property) => { if(!lightOrGroup) { if(!this.getCurrentLightOrGroup) return; const curLightOrGroup = this.getCurrentLightOrGroup(); if(curLightOrGroup) { lightOrGroup = curLightOrGroup.objCache; this.savedValue = -1; // force update } console.assert(lightOrGroup, 'no light or group', curLightOrGroup); if(!lightOrGroup) return; }; if(this.isInMultiAction || !this.isEncoder) return; const powerHue = property == 'power' ? !lightOrGroup?.power : lightOrGroup?.power; let actionValue = lightOrGroup?.[this.property]; // check if the values have changed if(actionValue === this.savedValue && powerHue === this.savedPower) { return; } // cache the values this.savedValue = actionValue; this.savedPower = powerHue; // values in hue are 0-254, convert to 0-100 // !this is not true for temperature let value; if(this.property == 'temperature') { const ct = lightOrGroup.originalValue?.capabilities?.control?.ct; console.assert(ct, 'no ct in capabilities', lightOrGroup); if(!ct) return; value = parseInt(Utils.percent(lightOrGroup.temperature, ct.min, ct.max)); } else { value = parseInt(actionValue / 2.54); } // if the light is off, set the opacity to 0.5 const opacity = powerHue ? 1 :0.5; this.setFeedback(inContext, value, opacity); }; this.togglePower = (inContext) => { const target = this.getCurrentLightOrGroup(); if(!target) return; const targetState = !target.objCache.power; target.obj.setPower(targetState, (success, error) => { if (success) { target.objCache.power = targetState; // cache.refresh(); this.updateAllActions(); } else { log(error); showAlert(inContext); } }); return target; }; this.getVerifiedSettings = function(inContext, requiredPropertySetting = null) { // Check if any bridge is configured if(!('bridge' in settings)) { log('No bridge configured'); showAlert(inContext); return false; } // Check if the configured bridge is in the cache if(!(settings.bridge in cache.data)) { log(`Bridge ${settings.bridge} not found in cache`); showAlert(inContext); return false; } // Check if any light is configured if(!('light' in settings)) { log('No light or group configured'); showAlert(inContext); return false; } if(requiredPropertySetting) { if(!(requiredPropertySetting in settings)) { log(`No ${requiredPropertySetting} configured`); showAlert(inContext); return; } } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; if(bridgeCache === false) { console.warn('getVerifiedSettings: no bridge in cache'); return false; }; // Check if the configured light or group is in the cache if(!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) { log(`Light or group ${settings.light} not found in cache`, settings, bridgeCache); showAlert(inContext); return false; } return settings; }; // Private function to set the defaults function setDefaults(inCallback) { // If at least one bridge is paired if (!(Object.keys(cache.data).length > 0)) { // If a callback function was given if (inCallback !== undefined) { // Execute the callback function inCallback(); } return; } // Find out type of action let action; if (instance instanceof PowerAction) { action = 'com.elgato.philips-hue.power'; } else if (instance instanceof ColorAction) { action = 'com.elgato.philips-hue.color'; } else if (instance instanceof CycleAction) { action = 'com.elgato.philips-hue.cycle'; } else if (instance instanceof BrightnessAction) { action = 'com.elgato.philips-hue.brightness'; } else if (instance instanceof BrightnessRelAction) { action = 'com.elgato.philips-hue.brightness-rel'; } else if (instance instanceof SceneAction) { action = 'com.elgato.philips-hue.scene'; } // If no bridge is set for this action if (!('bridge' in settings)) { // Sort the bridges alphabetically let bridgeIDsSorted = Object.keys(cache.data).sort((a, b) => { return cache.data[a].name.localeCompare(cache.data[b].name); }); // Set the bridge automatically to the first one settings.bridge = bridgeIDsSorted[0]; // Save the settings saveSettings(action, inContext, settings); } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; // If no light is set for this action if (!('light' in settings)) { // First try to set a group, because scenes only support groups // If the bridge has at least one group if (Object.keys(bridgeCache.groups).length > 0) { // Sort the groups automatically let groupIDsSorted = Object.keys(bridgeCache.groups).sort((a, b) => { return bridgeCache.groups[a].name.localeCompare(bridgeCache.groups[b].name); }); // Set the light automatically to the first group settings.light = groupIDsSorted[0]; // Save the settings saveSettings(action, inContext, settings); } else if (Object.keys(bridgeCache.lights).length > 0) { // Sort the lights automatically let lightIDsSorted = Object.keys(bridgeCache.lights).sort((a, b) => { return bridgeCache.lights[a].name.localeCompare(bridgeCache.lights[b].name); }); // Set the light automatically to the first light settings.light = lightIDsSorted[0]; // Save the settings saveSettings(action, inContext, settings); } } // If a callback function was given if (inCallback !== undefined) { // Execute the callback function inCallback(); } } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/brightnessAction.js ================================================ /** @file brightnessAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ function BrightnessAction(inContext, inSettings, jsn) { this.property = 'brightness'; // Inherit from PropertyAction PropertyAction.call(this, inContext, inSettings, jsn); } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/brightnessRelAction.js ================================================ /** @file brightnessRelAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a brightness action function BrightnessRelAction(inContext, inSettings) { // Init BrightnessRelAction let instance = this; // Inherit from Action Action.call(this, inContext, inSettings); // Set the default values setDefaults(); // Public function called on key up event this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => { // If onKeyUp was triggered manually, load settings if (inSettings === undefined) { inSettings = instance.getSettings(); } // Set icon according to relative value setState(inContext, inSettings.brightnessRel >= 0 ? 0 : 1); // Check if any bridge is configured if (!('bridge' in inSettings)) { log('No bridge configured'); showAlert(inContext); return; } // Check if the configured bridge is in the cache if (!(inSettings.bridge in cache.data)) { log(`Bridge ${inSettings.bridge} not found in cache`); showAlert(inContext); return; } // Find the configured bridge let bridgeCache = cache.data[inSettings.bridge]; // Check if any light is configured if (!('light' in inSettings)) { log('No light or group configured'); showAlert(inContext); return; } // Check if the configured light or group is in the cache if (!(inSettings.light in bridgeCache.lights || inSettings.light in bridgeCache.groups)) { log(`Light or group ${inSettings.light} not found in cache`); showAlert(inContext); return; } // Check if any brightness is configured if (!('brightnessRel' in inSettings)) { log('No relative brightness configured'); showAlert(inContext); return; } // Create a bridge instance let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); // Create a light or group object let objCache, obj; if (inSettings.light.indexOf('l') !== -1) { objCache = bridgeCache.lights[inSettings.light]; obj = new Light(bridge, objCache.id); } else { objCache = bridgeCache.groups[inSettings.light]; obj = new Group(bridge, objCache.id); } // Convert brightness let brightness; if (objCache.power) { let brightnessRel = (objCache.brightness / 2.54) + parseInt(inSettings.brightnessRel); brightness = Math.round(brightnessRel * 2.54); } else { brightness = parseInt(inSettings.brightnessRel); } if (brightness > 254) { brightness = 254; } else if (brightness < 0) { brightness = 0; } // Turn lights off if brightness is 0 if (brightness <= 0) { obj.setPower(false, (inSuccess, inError) => { if (inSuccess) { objCache.power = false; } else { log(inError); showAlert(inContext); } }); } else { // Set light or group state obj.setBrightness(brightness, (inSuccess, inError) => { if (inSuccess) { objCache.brightness = brightness; } else { log(inError); showAlert(inContext); } }); } }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Set defaults setDefaults(); // Call the callback function inCallback(); }); }; // Private function to set the defaults function setDefaults() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // If brightness is already set for this action if ('brightnessRel' in settings) { return; } // Set the relative brightness to 0 settings.brightnessRel = 0; // Save the settings saveSettings('com.elgato.philips-hue.brightness-rel', context, settings); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/colorAction.js ================================================ /** @file colorAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a color action function ColorAction(inContext, inSettings) { // Init ColorAction let instance = this; // Inherit from Action Action.call(this, inContext, inSettings); // Set the default values setDefaults(); // Public function called on key up event this.onKeyUp = (inContext) => { const settings = this.getVerifiedSettings(inContext, 'color'); if(false === settings) return; let bridgeCache = cache.data[settings.bridge]; // Create a bridge instance let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); // Create a light or group object let objCache, obj; if (settings.light.indexOf('l') !== -1) { objCache = bridgeCache.lights[settings.light]; obj = new Light(bridge, objCache.id); } else { objCache = bridgeCache.groups[settings.light]; obj = new Group(bridge, objCache.id); } // Check if this is a color or temperature light if (settings.color.indexOf('#') !== -1) { // Convert light color to hardware independent XY color let xy = Bridge.hex2xy(settings.color); // Set light or group state obj.setXY(xy, (inSuccess, inError) => { if (inSuccess) { objCache.xy = xy; } else { log(inError); showAlert(inContext); } }); } else { // Note: Some lights do not support the full range let min = 153.0; let max = 500.0; let minK = 2000.0; let maxK = 6500.0; // Convert light color let percentage = (settings.color - minK) / (maxK - minK); let invertedPercentage = -1 * (percentage - 1.0); let temperature = Math.round(invertedPercentage * (max - min) + min); // Set light or group state obj.setTemperature(temperature, (inSuccess, inError) => { if (inSuccess) { objCache.ct = temperature; } else { log(inError); showAlert(inContext); } }); } }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Set defaults setDefaults(); // Call the callback function inCallback(); }); }; // Private function to set the defaults function setDefaults() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache.data)) { return; } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; // Check if a light was set for this action if (!('light' in settings)) { return; } // Check if the configured light or group is in the cache if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) { return; } // Get a light or group cache let lightCache; if (settings.light.indexOf('l-') !== -1) { lightCache = bridgeCache.lights[settings.light]; } else { lightCache = bridgeCache.groups[settings.light]; } // Check if any color is configured if ('color' in settings) { // Check if the set color is supported by the light if (settings.color.charAt(0) === '#' && lightCache.xy != null) { return; } else if (settings.color.charAt(0) !== '#' && lightCache.xy == null) { return; } } // Check if the light supports all colors if (lightCache.xy != null) { // Set white as the default color settings.color = '#ffffff'; } else { // Set white as the default temperature settings.color = '4250'; } // Save the settings saveSettings('com.elgato.philips-hue.color', context, settings); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/cycleAction.js ================================================ /** @file cycleAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a color action function CycleAction(inContext, inSettings) { // Init CycleAction let instance = this; // Index of current active Color let currentColor = -1; // Inherit from Action Action.call(this, inContext, inSettings); // Set the default values setDefaults(); // Public function called on key up event this.onKeyUp = (inContext) => { const settings = this.getVerifiedSettings(inContext, 'colors'); if(false === settings) return; let bridgeCache = cache.data[settings.bridge]; // Create a bridge instance let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); // Create a light or group object let objCache, obj; if(settings.light.indexOf('l') !== -1) { objCache = bridgeCache.lights[settings.light]; obj = new Light(bridge, objCache.id); } else { objCache = bridgeCache.groups[settings.light]; obj = new Group(bridge, objCache.id); } // Reset current Color index if(currentColor + 1 >= settings.colors.length) { currentColor = -1; } let colorIndex = currentColor + 1; // Check if this is a color or temperature light if(settings.colors[colorIndex].indexOf('#') !== -1) { // Convert light color to hardware independent XY color let xy = Bridge.hex2xy(settings.colors[colorIndex]); // Set light or group state obj.setXY(xy, (inSuccess, inError) => { if(inSuccess) { objCache.xy = xy; ++currentColor; } else { log(inError); showAlert(inContext); } }); } else { // Note: Some lights do not support the full range let min = 153.0; let max = 500.0; let minK = 2000.0; let maxK = 6500.0; // Convert light color let percentage = (settings.colors[colorIndex] - minK) / (maxK - minK); let invertedPercentage = -1 * (percentage - 1.0); let temperature = Math.round(invertedPercentage * (max - min) + min); // Set light or group state obj.setTemperature(temperature, (inSuccess, inError) => { if(inSuccess) { objCache.ct = temperature; ++currentColor; } else { log(inError); showAlert(inContext); } }); } }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Set defaults setDefaults(); // Call the callback function inCallback(); }); }; // Private function to set the defaults function setDefaults() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // Check if any bridge is configured if(!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if(!(settings.bridge in cache.data)) { return; } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; // Check if a light was set for this action if(!('light' in settings)) { return; } // Check if the configured light or group is in the cache if(!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) { return; } // Get a light or group cache let lightCache; if(settings.light.indexOf('l-') !== -1) { lightCache = bridgeCache.lights[settings.light]; } else { lightCache = bridgeCache.groups[settings.light]; } // Check if any color is configured if('colors' in settings) { // Check if the set color is supported by the light if(settings.colors[0].charAt(0) === '#' && lightCache.xy != null) { return; } else if(settings.colors[0].charAt(0) !== '#' && lightCache.xy == null) { return; } } // Check if the light supports all colors if(lightCache.xy != null) { // Set white as the default color settings.colors = ['#ff0000', '#00ff00', '#0000ff']; } else { // Set white as the default temperature settings.colors = ['2230', '4250', '6410']; } // Save the settings saveSettings('com.elgato.philips-hue.cycle', context, settings); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/main.js ================================================ /** @file main.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Global web socket var websocket = null; // Global cache var cache = {}; // Global settings var globalSettings = {}; const throttleDialRotate = Utils.throttle((fn) => { if (fn) fn(); }, 60); const debounceDialRotate = Utils.debounce((jsonObj) => { console.log('debounceDialRotate', jsonObj); }, 300); // Setup the websocket and handle communication function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) { // Create array of currently used actions let actions = {}; window.MACTIONS = actions; // Create a cache cache = new Cache(); // Open the web socket to Stream Deck // Use 127.0.0.1 because Windows needs 300ms to resolve localhost websocket = new WebSocket(`ws://127.0.0.1:${inPort}`); const _info = JSON.parse(inInfo); const [ version, major, minor, build ] = _info.application.version.split(".").map(e => parseInt(e, 10)); const hasDialPress = version == 6 && major < 4; // Web socket is connected websocket.onopen = () => { // Register plugin to Stream Deck registerPluginOrPI(inRegisterEvent, inPluginUUID); // Request the global settings of the plugin requestGlobalSettings(inPluginUUID); }; document.addEventListener('updateActions', (e) => { // updateAction carries the sender of the event so we can skip it const sender = e.detail?.sender; Object.keys(actions).forEach(inContext => { if(actions[inContext].updateAction) { // don't update the sender if(actions[inContext] === sender) return; actions[inContext].updateAction(); } }); }, false); // Add event listener document.addEventListener('newCacheAvailable', () => { // When a new cache is available Object.keys(actions).forEach(inContext => { // Inform all used actions that a new cache is available actions[inContext].newCacheAvailable(() => { let action; // Find out type of action if (actions[inContext] instanceof PowerAction) { action = 'com.elgato.philips-hue.power'; } else if (actions[inContext] instanceof ColorAction) { action = 'com.elgato.philips-hue.color'; } else if (actions[inContext] instanceof CycleAction) { action = 'com.elgato.philips-hue.cycle'; } else if (actions[inContext] instanceof BrightnessAction) { action = 'com.elgato.philips-hue.brightness'; if(actions[inContext].updateAction) { actions[inContext].updateAction(); } } else if (actions[inContext] instanceof TemperatureAction) { action = 'com.elgato.philips-hue.temperature'; if(actions[inContext].updateAction) { actions[inContext].updateAction(); } } else if (actions[inContext] instanceof BrightnessRelAction) { action = 'com.elgato.philips-hue.brightness-rel'; } else if (actions[inContext] instanceof SceneAction) { action = 'com.elgato.philips-hue.scene'; } // Inform PI of new cache sendToPropertyInspector(action, inContext, cache.data); }); }); }, false); // Web socked received a message websocket.onmessage = inEvent => { // Parse parameter from string to object let jsonObj = JSON.parse(inEvent.data); // Extract payload information let event = jsonObj['event']; let action = jsonObj['action']; let context = jsonObj['context']; let jsonPayload = jsonObj['payload']; let settings; if(event === 'dialRotate') { if(actions[context]?.onDialRotate) { throttleDialRotate(() => { actions[context].onDialRotate(jsonObj); }); // debounceDialRotate(jsonObj); // actions[context].onDialRotate(jsonObj); } } else if(!hasDialPress && event === 'dialUp') { if(actions[ context ]?.onDialUp) { actions[ context ].onDialUp(jsonObj); } } else if(!hasDialPress && event === 'dialDown') { if(actions[ context ]?.onDialDown) { actions[ context ].onDialDown(jsonObj); } } else if(hasDialPress && event === 'dialPress') { if(actions[context]?.onDialPress) { actions[context].onDialPress(jsonObj); } } else if(event === 'touchTap') { if(actions[context]?.onTouchTap) { actions[context].onTouchTap(jsonObj); } } else if (event === 'keyUp') { settings = jsonPayload['settings']; let coordinates = jsonPayload['coordinates']; let userDesiredState = jsonPayload['userDesiredState']; let state = jsonPayload['state']; // Send onKeyUp event to actions if (context in actions) { actions[context].onKeyUp(context, settings, coordinates, userDesiredState, state); } // Refresh the cache cache.refresh(); } else if (event === 'willAppear') { settings = jsonPayload['settings']; // If this is the first visible action if (Object.keys(actions).length === 0) { // Start polling cache.startPolling(); } // Add current instance is not in actions array if (!(context in actions)) { // Add current instance to array if (action === 'com.elgato.philips-hue.power') { actions[context] = new PowerAction(context, settings); } else if (action === 'com.elgato.philips-hue.color') { actions[context] = new ColorAction(context, settings); } else if (action === 'com.elgato.philips-hue.cycle') { actions[context] = new CycleAction(context, settings); } else if (action === 'com.elgato.philips-hue.brightness') { actions[context] = new BrightnessAction(context, settings, jsonObj); } else if (action === 'com.elgato.philips-hue.temperature') { actions[context] = new TemperatureAction(context, settings, jsonObj); } else if (action === 'com.elgato.philips-hue.brightness-rel') { actions[context] = new BrightnessRelAction(context, settings); } else if (action === 'com.elgato.philips-hue.scene') { actions[context] = new SceneAction(context, settings); } } } else if (event === 'willDisappear') { // Remove current instance from array if (context in actions) { delete actions[context]; } // If this is the last visible action if (Object.keys(actions).length === 0) { // Stop polling cache.stopPolling(); } } else if (event === 'didReceiveGlobalSettings') { // Set global settings globalSettings = jsonPayload['settings']; // If at least one action is active if (Object.keys(actions).length > 0) { // Refresh the cache cache.refresh(); } } else if (event === 'didReceiveSettings') { settings = jsonPayload['settings']; // Set settings if (context in actions) { actions[context].setSettings(settings); } // Refresh the cache cache.refresh(); } else if (event === 'propertyInspectorDidAppear') { // Send cache to PI sendToPropertyInspector(action, context, cache.data); } else if (event === 'sendToPlugin') { let piEvent = jsonPayload['piEvent']; if (piEvent === 'valueChanged') { // Only color, brightness and scene support live preview if (action !== 'com.elgato.philips-hue.power' && action !== 'com.elgato.philips-hue.cycle') { // Send manual onKeyUp event to action if (context in actions) { actions[context].onKeyUp(context); } } } else if (piEvent === 'lightsChanged') { // console.log("lightsChanged", action, context, jsonPayload); if (context in actions) { if(actions[context].updateDisplay) { actions[context].updateDisplay(); }; } } } }; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/philips/cache.js ================================================ /** @file cache.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype for a data cache function Cache() { // Init Cache let instance = this; // Refresh time of the cache in seconds let autoRefreshTime = 60; // Private timer instance let timer = null; // Private bridge discovery let discovery = null; // Public variable containing the cached data this.data = {}; // Private function to discover all bridges on the network function buildDiscovery(inCallback) { // Check if discovery ran already if (discovery != null) { inCallback(true); return; } // Init discovery variable to indicate that it ran already discovery = {}; // Run discovery Bridge.discover((inSuccess, inBridges) => { // If the discovery was not successful if (!inSuccess) { log(inBridges); inCallback(false); return; } // For all discovered bridges inBridges.forEach(inBridge => { // Add new bridge to discovery object discovery[inBridge.getID()] = { ip: inBridge.getIP() }; }); inCallback(true); }); } // Gather all required information by a Bridge via ID function refreshBridge(pairedBridgeID, pairedBridge) { // Older Bridges in Settings may have the ID stored inside the object if (!pairedBridge.id) { pairedBridge.id = pairedBridgeID; } // Older Bridges in Settings may have no IP stored if (!pairedBridge.ip) { // Trying to receive the IP trough auto-discovery if (discovery[pairedBridge.id]) { pairedBridge.ip = discovery[pairedBridge.id].ip; } // If no IP can be found for this Bridge we need to stop here else { log(`No IP found for paired Bridge ID: ${pairedBridge.id}`); return; } } // Create a bridge instance let bridge = new Bridge(pairedBridge.ip, pairedBridge.id, pairedBridge.username); // Create bridge cache let bridgeCache = { 'lights': {}, 'groups': {} }; bridgeCache.id = bridge.getID(); bridgeCache.ip = bridge.getIP(); bridgeCache.username = bridge.getUsername(); // Load the bridge name bridge.getName((inSuccess, inName) => { // If getName was not successful if (!inSuccess) { log(inName); return; } // Save the name bridgeCache.name = inName; // Add bridge to the cache // instance.data[bridge.getID()] = bridgeCache; // Request all lights of the bridge bridge.getLights((inSuccess, inLights) => { // If getLights was not successful if (!inSuccess) { log(inLights); return; } // Create cache for each light inLights.forEach(inLight => { // Add light to cache bridgeCache.lights['l-' + inLight.getID()] = { id: inLight.getID(), name: inLight.getName(), type: inLight.getType(), power: inLight.getPower(), brightness: inLight.getBrightness(), xy: inLight.getXY(), temperature: inLight.getTemperature(), originalValue: inLight.originalValue, }; }); // Request all groups of the bridge bridge.getGroups((inSuccess, inGroups) => { // If getGroups was not successful if (!inSuccess) { log(inGroups); return; } // Create cache for each group inGroups.forEach(inGroup => { // Add group to cache bridgeCache.groups['g-' + inGroup.getID()] = { id: inGroup.getID(), name: inGroup.getName(), type: inGroup.getType(), power: inGroup.getPower(), brightness: inGroup.getBrightness(), xy: inGroup.getXY(), temperature: inGroup.getTemperature(), scenes: {}, }; // If this is the last group if (Object.keys(bridgeCache.groups).length === inGroups.length) { // Request all scenes of the bridge bridge.getScenes((inSuccess, inScenes) => { // If getScenes was not successful if (!inSuccess) { log(inScenes); return; } // Create cache for each scene inScenes.forEach(inScene => { // Check if this is a group scene if (inScene.getType() !== 'GroupScene') { return; } // If scenes group is in cache if ('g-' + inScene.getGroup() in bridgeCache.groups) { // Add scene to cache bridgeCache.groups['g-' + inScene.getGroup()].scenes[inScene.getID()] = { id: inScene.getID(), name: inScene.getName(), type: inScene.getType(), group: inScene.getGroup(), }; } }); // console.log(bridgeCache); instance.data[bridge.getID()] = bridgeCache; // Inform keys that updated cache is available let event = new CustomEvent('newCacheAvailable'); document.dispatchEvent(event); }); } }); }); }); }); } // Public function to start polling this.startPolling = () => { // Log to the global log file log('Start polling to create cache'); // Start a timer instance.refresh(); timer = setInterval(instance.refresh, autoRefreshTime * 1000); } // Public function to stop polling this.stopPolling = () => { // Log to the global log file log('Stop polling to create cache'); // Invalidate the timer clearInterval(timer); timer = null; } this.refresh = Utils.debounce(function () { // Build discovery if necessary buildDiscovery(() => { if (globalSettings.bridges) { Object.keys(globalSettings.bridges).forEach(bridgeID => refreshBridge(bridgeID, globalSettings.bridges[bridgeID])); } }) }, 200); // avoid multiple calls in a short time // Private function to build a cache this.refresh2 = () => { // Build discovery if necessary buildDiscovery(() => { if (globalSettings.bridges) { Object.keys(globalSettings.bridges).forEach(bridgeID => refreshBridge(bridgeID, globalSettings.bridges[bridgeID])); } }) }; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/philips/meethue.js ================================================ /** @file meethue.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ const MDEBOUNCEDELAYMS = 80; // Prototype which represents a Philips Hue bridge function Bridge(ip = null, id = null, username = null) { // Init Bridge let instance = this; // Public function to pair with a bridge this.pair = (callback) => { if (ip) { let url = `http://${ip}/api`; let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('POST', url, true); xhr.timeout = 2500; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { if (xhr.response !== undefined && xhr.response != null) { let result = xhr.response[0]; if ('success' in result) { username = result['success']['username']; callback(true, result); } else { let message = result['error']['description']; callback(false, message); } } else { callback(false, 'Bridge response is undefined or null.'); } } else { callback(false, 'Could not connect to the bridge.'); } }; xhr.onerror = () => { callback(false, 'Unable to connect to the bridge.'); }; xhr.ontimeout = () => { callback(false, 'Connection to the bridge timed out.'); }; let obj = {}; obj.devicetype = 'stream_deck'; let data = JSON.stringify(obj); xhr.send(data); } else { callback(false, 'No IP address given.'); } }; // Public function to retrieve the username this.getUsername = () => { return username; }; // Public function to retrieve the IP address this.getIP = () => { return ip; }; // Public function to retrieve the ID this.getID = () => { return id; }; // Public function to retrieve the name this.getName = callback => { let url = `http://${ip}/api/${username}/config`; let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('GET', url, true); xhr.timeout = 5000; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { let result = xhr.response; if (result !== undefined && result != null) { if ('name' in result) { let name = result['name']; callback(true, name); } else { let message = result[0]['error']['description']; callback(false, message); } } else { callback(false, 'Bridge response is undefined or null.'); } } else { callback(false, 'Could not connect to the bridge.'); } }; xhr.onerror = () => { callback(false, 'Unable to connect to the bridge.'); }; xhr.ontimeout = () => { callback(false, 'Connection to the bridge timed out.'); }; xhr.send(); }; // Private function to retrieve objects function getMeetHues(type, callback) { let url; if (type === 'light') { url = `http://${ip}/api/${username}/lights`; } else if (type === 'group') { url = `http://${ip}/api/${username}/groups`; } else if (type === 'scene') { url = `http://${ip}/api/${username}/scenes`; } else { callback(false, 'Type does not exist.'); return; } let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('GET', url, true); xhr.timeout = 5000; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { let result = xhr.response; if (result !== undefined && result != null) { if (!Array.isArray(result)) { let objects = []; Object.keys(result).forEach(key => { let value = result[key]; if (type === 'light') { // console.log("Light", value.name, value.capabilities?.control); let light = new Light(instance, key, value.name, value.type, value.state.on, value.state.bri, value.state.xy, value.state.ct); light.originalValue = value; objects.push(light); } else if (type === 'group') { objects.push(new Group(instance, key, value.name, value.type, value.state.all_on, value.action.bri, value.action.xy, value.action.ct)); } else if (type === 'scene') { objects.push(new Scene(instance, key, value.name, value.type, value.group)); } }); callback(true, objects); } else { let message = result[0]['error']['description']; callback(false, message); } } else { callback(false, 'Bridge response is undefined or null.'); } } else { callback(false, 'Unable to get objects of type ' + type + '.'); } }; xhr.onerror = () => { callback(false, 'Unable to connect to the bridge.'); }; xhr.ontimeout = () => { callback(false, 'Connection to the bridge timed out.'); }; xhr.send(); } // Public function to retrieve the lights this.getLights = callback => { getMeetHues('light', callback); }; // Public function to retrieve the groups this.getGroups = callback => { getMeetHues('group', callback); }; // Public function to retrieve the scenes this.getScenes = callback => { getMeetHues('scene', callback); }; } // Static function to discover bridges Bridge.discover = callback => { let url = 'https://discovery.meethue.com'; let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('GET', url, true); xhr.timeout = 10000; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { if (xhr.response !== undefined && xhr.response != null) { let bridges = []; xhr.response.forEach(bridge => { bridges.push(new Bridge(bridge.internalipaddress, bridge.id)); }); callback(true, bridges); } else { callback(false, 'Meethue server response is undefined or null.'); } } else { callback(false, 'Unable to discover bridges.'); } }; xhr.onerror = () => { callback(false, 'Unable to connect to the internet.'); }; xhr.ontimeout = () => { callback(false, 'Connection to the internet timed out.'); }; xhr.send(); }; // Check if a Bridge is available under a certain IP address // If a username is set it will check that too Bridge.check = (ip, username, callback) => { let url = username ? `http://${ip}/api/${username}config` : `http://${ip}/api/config`; let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('GET', url, true); xhr.timeout = 10000; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200 && xhr.response !== undefined && xhr.response != null && xhr.response.hasOwnProperty('bridgeid') && (!username || xhr.response.hasOwnProperty('ipaddress')) ) { // at this point the bridge has been found and added to list callback(true, { ip: ip, id: xhr.response.bridgeid.toLowerCase(), }); } callback(false); }; xhr.onerror = xhr.ontimeout = () => { callback(false); }; xhr.send(); }; // Static function to convert hex to rgb Bridge.hex2rgb = inHex => { // Remove hash if it exists if (inHex.charAt(0) === '#') { inHex = inHex.substr(1); } // Split hex into RGB components let rgbArray = inHex.match(/.{1,2}/g); // Convert RGB component into decimals let red = parseInt(rgbArray[0], 16); let green = parseInt(rgbArray[1], 16); let blue = parseInt(rgbArray[2], 16); return { r: red, g: green, b: blue, }; } // Static function to convert rgb to hex Bridge.rgb2hex = inRGB => { return '#' + ((1 << 24) + (inRGB.r << 16) + (inRGB.g << 8) + inRGB.b).toString(16).slice(1); } // Static function to convert rgb to hsv Bridge.rgb2hsv = inRGB => { // Calculate the brightness and saturation value let max = Math.max(inRGB.r, inRGB.g, inRGB.b); let min = Math.min(inRGB.r, inRGB.g, inRGB.b); let d = max - min; let s = (max === 0 ? 0 : d / max); let v = max / 255; // Calculate the hue value let h; switch (max) { case min: h = 0; break; case inRGB.r: h = (inRGB.g - inRGB.b) + d * (inRGB.g < inRGB.b ? 6: 0); h /= 6 * d; break; case inRGB.g: h = (inRGB.b - inRGB.r) + d * 2; h /= 6 * d; break; case inRGB.b: h = (inRGB.r - inRGB.g) + d * 4; h /= 6 * d; break; } return {h, s, v}; } // Static function to convert hsv to rgb Bridge.hsv2rgb = inHSV => { let r = null; let g = null; let b = null; let i = Math.floor(inHSV.h * 6); let f = inHSV.h * 6 - i; let p = inHSV.v * (1 - inHSV.s); let q = inHSV.v * (1 - f * inHSV.s); let t = inHSV.v * (1 - (1 - f) * inHSV.s); // Calculate red, green and blue switch (i % 6) { case 0: r = inHSV.v; g = t; b = p; break; case 1: r = q; g = inHSV.v; b = p; break; case 2: r = p; g = inHSV.v; b = t; break; case 3: r = p; g = q; b = inHSV.v; break; case 4: r = t; g = p; b = inHSV.v; break; case 5: r = inHSV.v; g = p; b = q; break; } // Convert rgb values to int let red = Math.round(r * 255); let green = Math.round(g * 255); let blue = Math.round(b * 255); return { r: red, g: green, b: blue, }; } // Static function to convert hex to hsv Bridge.hex2hsv = inHex => { // Convert hex to rgb let rgb = Bridge.hex2rgb(inHex); // Convert rgb to hsv return Bridge.rgb2hsv(rgb); } // Static function to convert hsv to hex Bridge.hsv2hex = inHSV => { // Convert hsv to rgb let rgb = Bridge.hsv2rgb(inHSV); // Convert rgb to hex return Bridge.rgb2hex(rgb); } // Static function to convert hex to xy Bridge.hex2xy = inHex => { // Convert hex to rgb let rgb = Bridge.hex2rgb(inHex); // Concert RGB components to floats let red = rgb.r / 255; let green = rgb.g / 255; let blue = rgb.b / 255; // Convert RGB to XY let r = red > 0.04045 ? Math.pow(((red + 0.055) / 1.055), 2.4000000953674316) : red / 12.92; let g = green > 0.04045 ? Math.pow(((green + 0.055) / 1.055), 2.4000000953674316) : green / 12.92; let b = blue > 0.04045 ? Math.pow(((blue + 0.055) / 1.055), 2.4000000953674316) : blue / 12.92; let x = r * 0.664511 + g * 0.154324 + b * 0.162028; let y = r * 0.283881 + g * 0.668433 + b * 0.047685; let z = r * 8.8E-5 + g * 0.07231 + b * 0.986039; // Convert XYZ zo XY let xy = [x / (x + y + z), y / (x + y + z)]; if (isNaN(xy[0])) { xy[0] = 0.0; } if (isNaN(xy[1])) { xy[1] = 0.0; } return xy; }; // Prototype which represents a Philips Hue object function MeetHue(bridge = null, id = null, name = null, type = null) { // Init MeetHue let instance = this; // Override in child prototype let url = null; this.originalValue = null; // Public function to retrieve the type this.getType = () => { return type; }; // Public function to retrieve the name this.getName = () => { return name; }; // Public function to retrieve the ID this.getID = () => { return id; }; // Public function to retrieve the URL this.getURL = () => { return url; }; // Public function to set the URL this.setURL = inURL => { url = inURL; } // Public function to set light state this.setState = (state, callback) => { // Check if the URL was set if (instance.getURL() == null) { callback(false, 'URL is not set.'); return; } let xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.open('PUT', instance.getURL(), true); xhr.timeout = 2500; xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { if (xhr.response !== undefined && xhr.response != null) { let result = xhr.response[0]; if ('success' in result) { callback(true, result); } else { let message = result['error']['description']; callback(false, message); } } else { callback(false, 'Bridge response is undefined or null.'); } } else { callback(false, 'Could not set state.'); } }; xhr.onerror = () => { callback(false, 'Unable to connect to the bridge.'); }; xhr.ontimeout = () => { callback(false, 'Connection to the bridge timed out.'); }; let data = JSON.stringify(state); xhr.send(data); }; } // Prototype which represents a scene function Scene(bridge = null, id = null, name = null, type = null, group = null) { // Init Scene let instance = this; // Inherit from MeetHue MeetHue.call(this, bridge, id, name, type); // Set the URL this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/groups/0/action`); // Public function to retrieve the group this.getGroup = () => { return group; }; // Public function to set the scene this.on = callback => { // Define state object let state = {}; state.scene = id; // Send new state instance.setState(state, callback); }; } // Prototype which represents an illumination function Illumination(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) { // Init Illumination let instance = this; // Inherit from MeetHue MeetHue.call(this, bridge, id, name, type); // Public function to retrieve the power state this.getPower = () => { return power; }; // Public function to retrieve the brightness this.getBrightness = () => { return brightness; }; // Public function to retrieve xy this.getXY = () => { return xy; }; // Public function to retrieve the temperature this.getTemperature = () => { return temperature; }; // Public function to set the power status of the light this.setPower = (power, callback) => { // Define state object let state = {}; state.on = power; // Send new state instance.setState(state, callback); }; // Public function to set the brightness this.setBrightness = Utils.debounce((brightness, callback) => { // Define state object let state = {}; state.bri = brightness; // To modify the brightness, the light needs to be on state.on = true; // Send new state instance.setState(state, callback); }, MDEBOUNCEDELAYMS); // Public function set the xy value this.setXY = (xy, callback) => { // Define state object let state = {}; state.xy = xy; // To modify the color, the light needs to be on state.on = true; // Send new state instance.setState(state, callback); }; // Public function set the temperature value this.setTemperature = Utils.debounce((temperature, callback) => { // Define state object let state = {}; state.ct = temperature; // To modify the temperature, the light needs to be on state.on = true; // Send new state instance.setState(state, callback); }, MDEBOUNCEDELAYMS); } // Prototype which represents a light function Light(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) { // Inherit from Illumination Illumination.call(this, bridge, id, name, type, power, brightness, xy, temperature); // Set the URL this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/lights/${id}/state`); } // Prototype which represents a group function Group(bridge = null, id = null, name = null, type = null, power = null, brightness = null, xy = null, temperature = null) { // Inherit from Illumination Illumination.call(this, bridge, id, name, type, power, brightness, xy, temperature); // Set the URL this.setURL(`http://${bridge.getIP()}/api/${bridge.getUsername()}/groups/${id}/action`); } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/powerAction.js ================================================ /** @file powerAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a power action function PowerAction(inContext, inSettings) { // Init PowerAction let instance = this; // Inherit from Action Action.call(this, inContext, inSettings); // Update the state updateState(); this.updateAction = function() { updateState(); }; // Public function called on key up event this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => { const settings = this.getVerifiedSettings(inContext); if(false === settings) return; let bridgeCache = cache.data[settings.bridge]; // Create a bridge instance let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); // Create a light or group object let objCache, obj; if (settings.light.indexOf('l-') !== -1) { objCache = bridgeCache.lights[settings.light]; obj = new Light(bridge, objCache.id); } else { objCache = bridgeCache.groups[settings.light]; obj = new Group(bridge, objCache.id); } // Check for multi action let targetState; if (inUserDesiredState !== undefined) { targetState = !inUserDesiredState; } else { targetState = !objCache.power; } // Set light or group state obj.setPower(targetState, (success, error) => { if (success) { setActionState(inContext, targetState ? 0 : 1); objCache.power = targetState; cache.refresh(); } else { log(error); setActionState(inContext, inState); showAlert(inContext); } }); }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Update the state updateState(); // Call the callback function inCallback(); }); }; function updateState() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache.data)) { return; } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; // Check if a light was set for this action if (!('light' in settings)) { return; } // Check if the configured light or group is in the cache if (!(settings.light in bridgeCache.lights || settings.light in bridgeCache.groups)) { return; } // Find out if it is a light or a group let objCache; if (settings.light.indexOf('l-') !== -1) { objCache = bridgeCache.lights[settings.light]; } else { objCache = bridgeCache.groups[settings.light]; } // Set the target state let targetState = objCache.power; // Set the new action state setActionState(context, targetState ? 0 : 1); } // Private function to set the state function setActionState(inContext, inState) { setState(inContext, inState); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/propertyAction.js ================================================ /** @file propertyAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a brightness action function PropertyAction(inContext, inSettings, jsn) { let instance = this; this.keyIsDown = false; this.actionTriggered = false; const setStateFunction = `set${Utils.capitalize(this.property)}`; // Inherit from Action Action.call(this, inContext, inSettings, jsn); // Set the default values setDefaults(); this.updateAction = function() { const target = this.getCurrentLightOrGroup(); if(target === false) return; this.updateDisplay(target.objCache, this.property); }; if(this.isEncoder) { let timer = setInterval(() => { this.updateAction(); }, 5000); } this.getCurrentLightOrGroup = function() { let settings = this.getVerifiedSettings(inContext); if(settings === false) return false; // break if settings are not valid let bridgeCache = cache.data[settings.bridge]; // we have a valid bridge (was checked in getVerifiedSettings) let objCache = {}; let obj = {}; let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); if(settings.light.indexOf('l') !== -1) { objCache = bridgeCache.lights[settings.light]; if(objCache) { obj = new Light(bridge, objCache.id); } } else { objCache = bridgeCache.groups[settings.light]; if(objCache) { obj = new Group(bridge, objCache.id); } } return {obj, objCache}; }; this.setValue = function(inValue, jsn) { const target = this.getCurrentLightOrGroup(); if(target) { if(target.objCache.power === false) return; let value = inValue ? inValue : target.objCache[this.property]; if(jsn?.payload?.ticks) { let settings = this.getSettings(); const scaleTicks = settings?.scaleTicks || 1; const multiplier = scaleTicks * jsn.payload.ticks; value = Utils.minmax(parseInt(value + multiplier * 2.55), 0,255); // value = parseInt(value + jsn.payload.ticks * 2.55); } // just update the panel optimistically // note: this didn't work well for me, so I'm not using it // this.setFeedback(inContext, parseInt(value / 2.54), 1); target.obj[setStateFunction](value, (inSuccess, inError) => { if(inSuccess) { target.objCache[this.property] = value; this.updateDisplay(target.objCache, this.property); this.updateAllActions(); } else { log(inError); showAlert(inContext); } }); } }; this.onDialUp = function(jsn) { // console.log('onDialUp', jsn); if(this.getVerifiedSettings(inContext) === false) return; this.keyIsDown = false; if(!this.actionTriggered) { if(this.isEncoder) { return this.togglePower(inContext); } const target = this.getCurrentLightOrGroup(); // check if light is off, and if it is, turn it on if(target.objCache.power === false) { this.togglePower(inContext); this.updateDisplay(target.objCache, 'power'); } else { // otherwise, just change the property to the configured value this.onKeyUp(inContext); } } }; this.onDialDown = function(jsn) { // console.log('onDialDown', jsn); if(this.getVerifiedSettings(inContext) === false) return; // temporarily set a flag to mark that the key is down this.keyIsDown = true; this.actionTriggered = false; setTimeout(function() { if(instance.keyIsDown) { // console.log("***** long keypress detected:", instance.keyIsDown,inContext); const target = instance.togglePower(inContext); instance.updateDisplay(target.objCache, 'power'); instance.actionTriggered = true; } instance.keyIsDown = false; }, 500); }; this.onDialPress = function(jsn) { if(this.getVerifiedSettings(inContext) === false) return; if(jsn?.payload?.pressed === true) { // dial pressed == down this.onDialDown(jsn); } else { // dial released == up this.onDialUp(jsn); } }; this.onDialRotate = function(jsn) { this.setValue(null, jsn); }; this.onTouchTap = function(jsn) { this.togglePower(inContext); }; // Public function called on key up event this.onKeyUp = (inContext) => { const settings = this.getVerifiedSettings(inContext); if(settings === false) return; // Convert value // Hack to circumvent original code that converts values from 0-255 let value = this.property == 'temperature' ? Number(settings[this.property]) : Math.round(settings[this.property] * 2.54); this.setValue(value); }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = inCallback => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Set defaults setDefaults(); // Call the callback function inCallback(); }); }; // Private function to set the defaults function setDefaults() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // If property is already set for this action if(this.property in settings) { return; } // Set the property to 100 settings[this.property] = 100; // Save the settings saveSettings(`com.elgato.philips-hue.${this.property}`, context, settings); } // update the action and its display this.updateActionIfCacheAvailable(inContext); } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/sceneAction.js ================================================ /** @file sceneAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Prototype which represents a scene action function SceneAction(inContext, inSettings) { // Init SceneAction let instance = this; // Inherit from Action Action.call(this, inContext, inSettings); // Set the default values setDefaults(); // Public function called on key up event this.onKeyUp = (inContext, inSettings, inCoordinates, inUserDesiredState, inState) => { const settings = this.getVerifiedSettings(inContext, 'scene'); if(false === settings) return; let bridgeCache = cache.data[settings.bridge]; // Find the configured group let groupCache = bridgeCache.groups[inSettings.light]; // Check if any scene is configured if (!('scene' in inSettings)) { log('No scene configured'); showAlert(inContext); return; } // Check if the configured scene is in the group cache if (!(settings.scene in groupCache.scenes)) { log(`Scene ${settings.scene} not found in cache`); showAlert(inContext); return; } // Find the configured scene let sceneCache = groupCache.scenes[inSettings.scene]; // Create a bridge instance let bridge = new Bridge(bridgeCache.ip, bridgeCache.id, bridgeCache.username); // Create a scene instance let scene = new Scene(bridge, sceneCache.id); // Set scene scene.on((inSuccess, inError) => { // Check if setting the scene was successful if (!(inSuccess)) { log(inError); showAlert(inContext); } }); }; // Before overwriting parent method, save a copy of it let actionNewCacheAvailable = this.newCacheAvailable; // Public function called when new cache is available this.newCacheAvailable = (inCallback) => { // Call actions newCacheAvailable method actionNewCacheAvailable.call(instance, () => { // Set defaults setDefaults(); // Call the callback function inCallback(); }); }; // Private function to set the defaults function setDefaults() { // Get the settings and the context let settings = instance.getSettings(); let context = instance.getContext(); // Check if any bridge is configured if (!('bridge' in settings)) { return; } // Check if the configured bridge is in the cache if (!(settings.bridge in cache.data)) { return; } // Find the configured bridge let bridgeCache = cache.data[settings.bridge]; // Check if a light was set for this action if (!('light' in settings)) { return; } // Check if the light was set to a group if (!(settings.light.indexOf('g-') !== -1)) { return; } // Check if the configured group is in the cache if (!(settings.light in bridgeCache.groups)) { return; } // Find the configured group let groupCache = bridgeCache.groups[settings.light]; // Check if a scene was configured for this action if ('scene' in settings) { // Check if the scene is part of the set group if (settings.scene in groupCache.scenes) { return; } } // Check if the group has at least one scene if (!(Object.keys(groupCache.scenes).length > 0)) { return; } // Sort the scenes alphabetically let sceneIDsSorted = Object.keys(groupCache.scenes).sort((a, b) => { return groupCache.scenes[a].name.localeCompare(groupCache.scenes[b].name); }); // Set the action automatically to the first one settings.scene = sceneIDsSorted[0]; // Save the settings saveSettings('com.elgato.philips-hue.scene', context, settings); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/temperatureAction.js ================================================ /** @file temperatureAction.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ /** * Color temperature range of Philips lights is: 2200K to 6500K == 455 to 154 Mired. //154 is the coolest, 500 is the warmest */ const percentOfRange = (value, min = 0, max = 100) => { return parseInt((max - min) * (value / 100) + min + 1); }; function TemperatureAction(inContext, inSettings, jsn) { this.property = 'temperature'; const setStateFunction = `set${Utils.capitalize(this.property)}`; // Inherit from PropertyAction PropertyAction.call(this, inContext, inSettings, jsn); // setValue is sent from the 'keyUp' event and // contains the value of the slider (0-100) this.setValue = (inValue, jsn) => { const target = this.getCurrentLightOrGroup(); if(target === false) return; if(target.objCache.power === false) return; const ct = target.objCache?.originalValue?.capabilities?.control?.ct; if(!ct) return; let value = inValue ? percentOfRange(inValue, ct.min, ct.max) : target.objCache[this.property]; if(jsn?.payload?.ticks) { const settings = this.getSettings(); const scaleTicks = settings?.scaleTicks || 1; const multiplier = scaleTicks * jsn.payload.ticks; let addThis = (ct.max - ct.min) * (multiplier / 100); addThis = addThis > 0 ? Math.floor(addThis) : Math.ceil(addThis); value = Utils.minmax(parseInt(value + addThis), ct.min, ct.max); } target.obj[setStateFunction](value, (inSuccess, inError) => { if(inSuccess) { target.objCache[this.property] = value; this.updateDisplay(target.objCache, this.property, jsn); this.updateAllActions(); } else { log(inError); showAlert(inContext); } }); }; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/timers.js ================================================ /* global ESDTimerWorker */ /*eslint no-unused-vars: "off"*/ /*eslint-env es6*/ let ESDTimerWorker = new Worker(URL.createObjectURL( new Blob([timerFn.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '')], {type: 'text/javascript'}) )); ESDTimerWorker.timerId = 1; ESDTimerWorker.timers = {}; const ESDDefaultTimeouts = { timeout: 0, interval: 10 }; Object.freeze(ESDDefaultTimeouts); function _setTimer(callback, delay, type, params) { const id = ESDTimerWorker.timerId++; ESDTimerWorker.timers[id] = {callback, params}; ESDTimerWorker.onmessage = (e) => { if (ESDTimerWorker.timers[e.data.id]) { if (e.data.type === 'clearTimer') { delete ESDTimerWorker.timers[e.data.id]; } else { const cb = ESDTimerWorker.timers[e.data.id].callback; if (cb && typeof cb === 'function') cb(...ESDTimerWorker.timers[e.data.id].params); } } }; ESDTimerWorker.postMessage({type, id, delay}); return id; } function _setTimeoutESD(...args) { let [callback, delay = 0, ...params] = [...args]; return _setTimer(callback, delay, 'setTimeout', params); } function _setIntervalESD(...args) { let [callback, delay = 0, ...params] = [...args]; return _setTimer(callback, delay, 'setInterval', params); } function _clearTimeoutESD(id) { ESDTimerWorker.postMessage({type: 'clearTimeout', id}); // ESDTimerWorker.postMessage({type: 'clearInterval', id}); = same thing delete ESDTimerWorker.timers[id]; } window.setTimeout = _setTimeoutESD; window.setInterval = _setIntervalESD; window.clearTimeout = _clearTimeoutESD; //timeout and interval share the same timer-pool window.clearInterval = _clearTimeoutESD; /** This is our worker-code * It is executed in it's own (global) scope * which is wrapped above @ `let ESDTimerWorker` */ function timerFn() { /*eslint indent: ["error", 4, { "SwitchCase": 1 }]*/ let timers = {}; let debug = false; let supportedCommands = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval']; function log(e) { console.log('Worker-Info::Timers', timers); } function clearTimerAndRemove(id) { if (timers[id]) { if (debug) console.log('clearTimerAndRemove', id, timers[id], timers); clearTimeout(timers[id]); delete timers[id]; postMessage({type: 'clearTimer', id: id}); if (debug) log(); } } onmessage = function (e) { // first see, if we have a timer with this id and remove it // this automatically fulfils clearTimeout and clearInterval supportedCommands.includes(e.data.type) && timers[e.data.id] && clearTimerAndRemove(e.data.id); if (e.data.type === 'setTimeout') { timers[e.data.id] = setTimeout(() => { postMessage({id: e.data.id}); clearTimerAndRemove(e.data.id); //cleaning up }, Math.max(e.data.delay || 0)); } else if (e.data.type === 'setInterval') { timers[e.data.id] = setInterval(() => { postMessage({id: e.data.id}); }, Math.max(e.data.delay || ESDDefaultTimeouts.interval)); } }; } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/plugin/js/utils.js ================================================ /** @file utils.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Register the plugin or PI function registerPluginOrPI(inEvent, inUUID) { if (websocket) { websocket.send(JSON.stringify({ event: inEvent, uuid: inUUID, })); } } // Save settings function saveSettings(inAction, inUUID, inSettings) { if (websocket) { websocket.send(JSON.stringify({ action: inAction, event: 'setSettings', context: inUUID, payload: inSettings, })); } } // Save global settings function saveGlobalSettings(inUUID) { if (websocket) { websocket.send(JSON.stringify({ event: 'setGlobalSettings', context: inUUID, payload: globalSettings, })); } } // Request global settings for the plugin function requestGlobalSettings(inUUID) { if (websocket) { websocket.send(JSON.stringify({ event: 'getGlobalSettings', context: inUUID, })); } } // Log to the global log file function logToFile(inMessage) { // Log to the developer console let timeString = new Date().toLocaleString(); // Log to the Stream Deck log file if (websocket) { websocket.send(JSON.stringify({ event: 'logMessage', payload: { message: inMessage, }, })); } } const log = console.log.bind( console, '%c [HUE]', 'color: #66c', ); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // const debug = true; // Object.defineProperty(this, "log", { // get: function () { // return debug ? console.log.bind(window.console, test(), '[DEBUG]') : function(){}; // } // }); const debounce = (callback, time = 800) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => callback(...args), time); }; }; // Show alert icon on the key function showAlert(inUUID) { if (websocket) { websocket.send(JSON.stringify({ event: 'showAlert', context: inUUID, })); } } // Set the state of a key function setState(inContext, inState) { if (websocket) { websocket.send(JSON.stringify({ event: 'setState', context: inContext, payload: { state: inState, }, })); } } // Set data to PI function sendToPropertyInspector(inAction, inContext, inData) { if (websocket) { websocket.send(JSON.stringify({ action: inAction, event: 'sendToPropertyInspector', context: inContext, payload: inData, })); } } // Set data to plugin function sendToPlugin(inAction, inContext, inData) { if (websocket) { websocket.send(JSON.stringify({ action: inAction, event: 'sendToPlugin', context: inContext, payload: inData, })); } } // Send feedback to the Stream Deck+ panel (SD+) function setFeedback(inContext, inPayload) { if (websocket) { websocket.send(JSON.stringify({ event: 'setFeedback', context: inContext, payload: inPayload, })); } } // Load the localizations function getLocalization(inLanguage, inCallback) { let url = `../${inLanguage}.json`; let xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = () => { if (xhr.readyState === XMLHttpRequest.DONE) { try { let data = JSON.parse(xhr.responseText); let localization = data['Localization']; inCallback(true, localization); } catch(e) { inCallback(false, 'Localizations is not a valid json.'); } } else { inCallback(false, 'Could not load the localizations.'); } }; xhr.onerror = () => { inCallback(false, 'An error occurred while loading the localizations.'); }; xhr.ontimeout = () => { inCallback(false, 'Localization timed out.'); }; xhr.send(); } const Utils = {}; Utils.debounce = function(func, wait = 100) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => { func.apply(this, args); }, wait); }; }; Utils.throttle = function(fn, threshold = 250, context) { let last, timer; return function() { var ctx = context || this; var now = new Date().getTime(), args = arguments; if(last && now < last + threshold) { clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(ctx, args); }, threshold); } else { last = now; fn.apply(ctx, args); } }; }; Utils.capitalize = function(str) { return str.charAt(0).toUpperCase() + str.slice(1); }; Utils.minmax = function(v = 0, min = 0, max = 100) { return Math.min(max, Math.max(min, v)); }; Utils.percent = (value, min, max) => { return ((value - min) / (max - min)) * 100; }; ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/css/main.css ================================================ /** @file main.css @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ html, body { margin: 0; color: #e6e6e6; background-color: #2d2d2d; font-family: 'Helvetica Light', 'Helvetica', Arial, sans-serif; height: 100%; -webkit-user-select: none; user-select: none; overflow: hidden; } h1, h2 { text-align: center; } h1 { margin-top: 10px; margin-bottom: 10px; } h2 { color: #969696; font-weight: 100; margin-bottom: 5px; } .header { padding: 25px 25px 5px; } #content { padding: 0 25px 15px; } .main { min-width: 400px; } @media (min-width: 600px) { .main { max-width: 500px; height: 100%; width: 100%; display: table; margin-left: auto; margin-right: auto; } .center { display: table-cell; height: 100%; vertical-align: middle; } .border { border: 1px solid #3c3c3c; border-radius: 5px; margin: 10px; min-height: 625px; } } p, li { line-height: 1.5; text-align: center; } .status-bar { display: table; width: 100%; border-spacing: 3px; border-collapse: separate; } .status-row { display: table-row; } .status-cell { display: table-cell; background-color: #3d3d3d; height: 10px; } .status-cell.active { background-color: #007dff; } .image { width: 250px; height: auto; display: block; margin-left: auto; margin-right: auto; padding-top: 5px; padding-bottom: 20px; } label { width: 100%; font-weight: bold; text-align: center; display: inline-block; box-sizing: border-box; } input { width: 65%; color: #fff; border: 1px solid #8d8d8d; background-color: #3d3d3d; border-radius: 4px; padding: 8px; margin: 8px auto 16px; text-align: center; font-size: 1rem; display: block; box-sizing: border-box; } input:focus, input:active { border-color: #007dff; } .button, .button-main, .button-transparent { padding: 8px 50px; display: table; margin: 8px auto; cursor: pointer; border-radius: 4px; } .button.block, .button-main.block { width: 65%; padding: 8px 16px; display: block; box-sizing: border-box; text-align: center; } .button { background-color: #3d3d3d; } .button-main { background-color: #535353; } .button:hover, .button-main:hover { background-color: #007dff; } .button-transparent:hover { color: #969696; } .hide { display: none; } .error-container > div { color: #fff; background-color: rgba(255, 0, 0, .2); border: 1px solid rgba(255, 0, 0, .5); border-radius: 4px; text-align: center; width: 80%; box-sizing: border-box; margin: 0 auto 24px; padding: 8px; } #loader { width: 40px; height: 40px; background-color: #666; margin: 20px auto; -webkit-animation: sk-rotateplane 1.2s infinite ease-in-out; animation: sk-rotateplane 1.2s infinite ease-in-out; } @-webkit-keyframes sk-rotateplane { 0% { -webkit-transform: perspective(120px) } 50% { -webkit-transform: perspective(120px) rotateY(180deg) } 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) } } @keyframes sk-rotateplane { 0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) } 50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/index.html ================================================ com.elgato.philips-hue.setup

Philips Hue

================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/discoveryView.js ================================================ /** @file discoveryView.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Load the discovery view function loadDiscoveryView() { // Delay the result for 1.5 seconds let resultDelay = 1500; // Set the status bar setStatusBar('discovery'); // Fill the title document.getElementById('title').innerHTML = localization['Discovery']['Title']; // Fill the content area document.getElementById('content').innerHTML = `

 

${localization['Discovery']['Title']}
`; // Start the discovery autoDiscovery(); // Discover all bridges function autoDiscovery() { Bridge.discover((status, data) => { if (status) { // Bridge discovery request was successful bridges = data; // Delay displaying the result setTimeout(() => { if (bridges.length === 0) { // No bridges were found // Fill the title document.getElementById('title').innerHTML = localization['Discovery']['TitleNone']; // Fill the content area document.getElementById('content').innerHTML = `

${localization['Discovery']['DescriptionNone']}

${localization['Discovery']['TitleNone']}
${localization['Discovery']['Retry']}
${localization['Discovery']['Close']}
`; // Add event listener document.getElementById('retry').addEventListener('click', retry); document.addEventListener('enterPressed', retry); document.getElementById('close').addEventListener('click', close); document.addEventListener('escPressed', close); } else { // At least one bridge was found let content; if (bridges.length === 1) { // Exactly one bridge was found // Fill the title document.getElementById('title').innerHTML = localization['Discovery']['TitleOne']; // Fill the content area content = `

${localization['Discovery']['DescriptionFound']}

${localization['Discovery']['TitleOne']} `; } else { // At least 2 bridges were found // Fill the title document.getElementById('title').innerHTML = localization['Discovery']['TitleMultiple'].replace('{{ number }}', bridges.length); // Fill the content area content = `

${localization['Discovery']['DescriptionFound']}

${localization['Discovery']['TitleMultiple'].replace('{{ number }}', bridges.length)} `; } document.getElementById('content').innerHTML = content + `
${localization['Discovery']['Pair']}
${localization['Discovery']['Retry']}
`; // Add event listener document.getElementById('pair').addEventListener('click', pair); document.addEventListener('enterPressed', pair); document.getElementById('retry').addEventListener('click', retry); document.addEventListener('escPressed', retry); } }, resultDelay); } else { // An error occurred while contacting the meethue discovery service document.getElementById('content').innerHTML = `

${data}

`; } }); } // Open pairing view function pair() { unloadDiscoveryView(); loadPairingView(); } // Retry discovery by reloading the view function retry() { unloadDiscoveryView(); loadDiscoveryView(); } // Close the window function close() { window.close(); } // Unload view function unloadDiscoveryView() { // Remove event listener document.removeEventListener('enterPressed', retry); document.removeEventListener('enterPressed', pair); document.removeEventListener('escPressed', close); document.removeEventListener('escPressed', retry); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/introView.js ================================================ /** @file introView.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Load the intro view function loadIntroView() { // Set the status bar setStatusBar('intro'); // Fill the title document.getElementById('title').innerHTML = localization['Intro']['Title']; // Fill the content area document.getElementById('content').innerHTML = `

${localization['Intro']['Description']}

${localization['Intro']['Title']}
${localization['Intro']['Start']}
${localization['Intro']['Manual']}
${localization['Intro']['Close']}
`; // Add event listener document.getElementById('start').addEventListener('click', startPairing); document.addEventListener('enterPressed', startPairing); document.getElementById('manual').addEventListener('click', startManual); document.getElementById('close').addEventListener('click', close); document.addEventListener('escPressed', close); // Load the pairing view function startPairing() { unloadIntroView(); loadDiscoveryView(); } // Load the manual view function startManual() { unloadIntroView(); loadManualView(); } // Close the window function close() { window.close(); } // Unload view function unloadIntroView() { // Remove event listener document.removeEventListener('enterPressed', startPairing); document.removeEventListener('escPressed', close); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/main.js ================================================ /** @file main.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Global variable containing the localizations var localization = null; // Global variable containing the discovered bridges var bridges = []; // Global variable containing the paired bridge var bridge = null; // Global function to set the status bar to the correct view function setStatusBar(view) { // Remove active status from all status cells let statusCells = document.getElementsByClassName('status-cell'); Array.from(statusCells).forEach((cell) => { cell.classList.remove('active'); }); // Set it only to the current one document.getElementById('status-' + view).classList.add('active'); } // Main function run after the page is fully loaded window.onload = () => { // Bind enter and ESC keys document.addEventListener('keydown', (e) => { if (e.key === 'Enter') { let event = new CustomEvent('enterPressed'); document.dispatchEvent(event); } else if (e.key === 'Esc' || e.key === 'Escape') { let event = new CustomEvent('escPressed'); document.dispatchEvent(event); } }); // Get the url parameter let url = new URL(window.location.href); let language = url.searchParams.get('language'); // Load the localizations getLocalization(language, (inStatus, inLocalization) => { if (inStatus) { // Save the localizations globally localization = inLocalization['Setup']; // Show the intro view loadIntroView(); } else { document.getElementById('content').innerHTML = `

${inLocalization}

`; } }); }; ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/manualView.js ================================================ /** @file manualView.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Load the manual view function loadManualView() { // Set the status bar setStatusBar('discovery'); // Fill the title document.getElementById('title').innerHTML = localization['Manual']['Title']; // Fill the content area document.getElementById('content').innerHTML = `

${localization['Manual']['Description']}


${localization['Manual']['Check']}
${localization['Manual']['Close']}
`; // Set cursor to input field document.getElementById('ip').focus(); // Add event listener document.getElementById('check').addEventListener('click', check); document.addEventListener('enterPressed', check); document.getElementById('close').addEventListener('click', close); document.addEventListener('escPressed', close); // Print error message function printError(error) { document.getElementById('ip-validation').innerHTML = `
${error}
`; } // Check ip address function check() { let ip = document.getElementById('ip').value.trim(); // check if input is empty if (!ip) { printError(localization['Manual']['Error']['Empty']); return; } // check if ip is invalid let ipV4Regex = '^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$'; if (!(new RegExp(ipV4Regex)).test(ip)) { printError(localization['Manual']['Error']['Invalid']); return; } Bridge.check(ip, null, (success, data) => { if (success) { bridges = [ new Bridge(data.ip, data.id), ]; pair(); } else { printError(localization['Manual']['Error']['Unreachable']); } }); } // Open pairing view function pair() { unloadManualView(); loadPairingView(); } // Close the window function close() { window.close(); } // Unload view function unloadManualView() { // Remove event listener document.removeEventListener('enterPressed', check); document.removeEventListener('escPressed', close); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/pairingView.js ================================================ /** @file pairingView.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Load the pairing view function loadPairingView() { // Time used to automatically pair bridges let autoPairingTimeout = 30; // Define local timer let timer = null; // Set the status bar setStatusBar('pairing'); // Fill the title document.getElementById('title').innerHTML = localization['Pairing']['Title']; // Fill the content area document.getElementById('content').innerHTML = `

${localization['Pairing']['Description']}

${localization['Pairing']['Title']}
`; // Start the pairing autoPairing(); // For n seconds try to connect to the bridge automatically function autoPairing() { // Define local timer counter let timerCounter = 0; // Start a new timer to auto connect to the bridges timer = setInterval(() => { if (timerCounter < autoPairingTimeout) { // Try to connect for n seconds pair(); timerCounter++; } else { // If auto connect was not successful for n times, // stop auto connecting and show controls // Stop the timer clearInterval(timer); timer = null; // Hide the loader animation document.getElementById('loader').classList.add('hide'); // Show manual user controls instead document.getElementById('controls').innerHTML = `
${localization['Pairing']['Retry']}
${localization['Pairing']['Close']}
`; // Add event listener document.getElementById('retry').addEventListener('click', retry); document.addEventListener('enterPressed', retry); document.getElementById('close').addEventListener('click', close); document.addEventListener('escPressed', close); } }, 1000) } // Try to pair with all discovered bridges function pair() { bridges.forEach(item => { item.pair((status, data) => { if (status) { // Pairing was successful bridge = item; // Show the save view unloadPairingView(); loadSaveView(); } }); }); } // Retry pairing by reloading the view function retry() { unloadPairingView(); loadPairingView(); } // Close the window function close() { window.close(); } // Unload view function unloadPairingView() { // Stop the timer clearInterval(timer); timer = null; // Remove event listener document.removeEventListener('escPressed', retry); document.removeEventListener('enterPressed', close); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/setup/js/saveView.js ================================================ /** @file saveView.js @brief Philips Hue Plugin @copyright (c) 2019, Corsair Memory, Inc. @license This source code is licensed under the MIT-style license found in the LICENSE file. */ // Load the save view function loadSaveView() { // Set the status bar setStatusBar('save'); // Fill the title document.getElementById('title').innerHTML = localization['Save']['Title']; // Fill the content area document.getElementById('content').innerHTML = `

${localization['Save']['Description']}

${localization['Save']['Title']}
${localization['Save']['Save']}
`; // Add event listener document.getElementById('close').addEventListener('click', close); document.addEventListener('enterPressed', close); // Save the bridge window.opener.document.dispatchEvent(new CustomEvent('saveBridge', { detail: { ip: bridge.getIP(), id: bridge.getID(), username: bridge.getUsername(), } })); // Close this window function close() { window.close(); } } ================================================ FILE: Sources/com.elgato.philips-hue.sdPlugin/zh_CN.json ================================================ { "Name": "Philips Hue", "Category": "Philips Hue", "Description": "控制您的 Philips Hue 灯。", "com.elgato.philips-hue.power": { "Name": "开/关", "Tooltip": "打开或关闭灯。", "States": [ { "Name": "开" }, { "Name": "关" } ] }, "com.elgato.philips-hue.color": { "Name": "颜色", "Tooltip": "设置灯的颜色。" }, "com.elgato.philips-hue.cycle": { "Name": "颜色循环", "Tooltip": "在灯光颜色之间循环。" }, "com.elgato.philips-hue.brightness": { "Name": "亮度", "Tooltip": "设置灯的亮度。", "Encoder": { "TriggerDescription": { "Rotate": "调整亮度", "Touch": "打开/关闭灯光" } } }, "com.elgato.philips-hue.brightness-rel": { "Name": "相对亮度", "Tooltip": "相对于当前亮度,调节照明亮度。" }, "com.elgato.philips-hue.scene": { "Name": "场景", "Tooltip": "设置场景。" }, "Localization": { "PI": { "Bridge": "桥接", "NoBridges": "无需桥接", "AddBridge": "新增", "Lights": "灯", "Group": "组", "LightsTitle": "灯", "GroupsTitle": "组", "NoLights": "没有灯", "NoGroups": "没有组", "Color": "颜色", "Colors": "颜色", "Temperature": "温度", "Brightness": "亮度", "Steps": "步长", "Scene": "场景", "NoScenes": "没有场景" }, "Setup": { "Intro": { "Title": "添加桥接", "Description": "与 Philips Hue 桥接配对以控制您的灯。", "Start": "发现桥接", "Manual": "手动添加桥接", "Close": "现在不" }, "Discovery": { "Title": "正在发现桥接...", "TitleNone": "没有找到桥接", "TitleOne": "找到一个桥接", "TitleMultiple": "找到 {{ number }} 个桥接", "DescriptionFound": "现在开始配对?", "DescriptionNone": "确保桥接打开并已联网。", "Pair": "配对", "Close": "关闭", "Retry": "重试" }, "Manual": { "Title": "手动添加桥接", "Description": "如果不支持自动发现,则需要通过 IP 地址手动将 Philips Hue 桥接添加到您的网络中。", "IPAddress": "桥接的 IP 地址", "Check": "验证输入", "Close": "关闭", "Error": { "Empty": "请添加桥接的 IP 地址。", "Invalid": "输入的 IP 地址格式无效。请检查输入。", "Unreachable": "桥接无法接通。请检查 IP 地址。" } }, "Pairing": { "Title": "正在配对...", "Description": "现在请按桥接上的链接按钮。", "Close": "关闭", "Retry": "重试" }, "Save": { "Title": "桥接已添加", "Description": "桥接配对成功。", "Save": "完成" } } } }