├── CHANGELOG.md ├── LICENSE ├── README.md ├── homebridgeStatusWidget.js └── images ├── config.png ├── example_parameter_setup.jpeg ├── ios16LockscreenWidget.jpg ├── notAvailable_purple.jpg ├── notification_homebridge_stopped.jpg ├── notification_homebridge_update.jpg ├── notification_homebridge_update_extended.jpg ├── siri_shortcut.PNG ├── unknown.jpg ├── use_config_via_parameter.jpeg ├── widget_black_dark.jpg ├── widget_black_light.jpg ├── widget_custom_blue.jpg ├── widget_custom_blue_green_charts.jpg ├── widget_display.jpg ├── widget_purple_dark.jpg └── widget_purple_light.jpg /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | I try to list the changes i make in this changelog: 2 | 3 | #16.10.2023 13:00 4 | - switched the source URL for the homebridge logo because the file does not exist anymore at the old location 5 | 6 | #16.11.2022 20:00 7 | - added support for iOS16 lockscreen widgets (they are much more smaller) 8 | - there are now more properties persisted in the CONFIGURATION. The old config is migrated. 9 | 10 | #15.01.2021 17:35 11 | - just added pretty-printing for the json files (as requested) 12 | 13 | #1.12.2020 21:21 14 | - fix for issue when json was not downloaded from icloud yet (also for the json files now...) 15 | 16 | #26.11.2020 17:00 17 | - small fixes nothing special 18 | - your existing config file can be used with this update (configuration class did not change yet) 19 | 20 | #21.11.2020 12:00 21 | - major improvements for customization -> tried to put everything in the configuration (which is persisted) -> this unfortunately means your old config is not compatible anymore so it will be deleted and recreated (sorry) 22 | - now basically almost everything is settable by you: shown all texts, textcolor, chartcolor, icons, iconcolor ... etc 23 | - refactoring of status panel (top right area) -> now you can change the shown text and RE-ARRANGE the status columns so that they align perfectly again after the text changed (you must play around with the values here) 24 | - support for custom light/dark mode (set bgColorMode to CUSTOM and adaptToLightOrDarkMode to true and change the customBackgroundColorX_light and _dark as well as the fontColor_light and _dark as well as chartColor_light and _dark) 25 | - added special gui that is shown if you run this script with siri shortcut -> this gui shows a list of all (not ignored) software that has an update 26 | - added voice feedback when running the script via siri BUT this seems to be bugged atm (i wrote the developer of Scriptable about it. Maybe this doesn't work as i expect it should) 27 | 28 | #19.11.2020 22:26 29 | - hope you can figure out how to use the configuration :) : 30 | - added support for persisting and loading configurations -> now you can configure everything once as you like, create a configuration.json file, save it in e.g. iCloud and from there you can easily use this file via a widget parameter and don't have to think about reconfiguring everything after updating the script itself 31 | - the new parameter must have the form USE_CONFIG:yourfilename.json (so it starts with USE_CONFIG: and ends with .json. The middle part can be chosen by you) 32 | - tried my best to make this as stylable as possible 33 | - support for dark and light versions of purple and black (can be set automatically) 34 | - you can now set chart color, font color, set own background colors and more 35 | - added x axis label for charts 36 | - did a pretty large code refactoring without function changes 37 | - added support for Homebridge Config UI X Authentication Mode 'none' -> so if you chose 'none' and don't need credentials to show your UI in the browser, you now don't need it for this widget anymore 38 | - new screenshots 39 | 40 | #17.11.2020 22:08 41 | - complete overhaul of the section above the updated date and below the title for perfect alignment (now only using stacks and spacers) 42 | - only updated one picture (the main pic on top of readme) 43 | - no functionality changed 44 | 45 | #17.11.2020 20:31 46 | - just added a new background color BLUE_TO_RED 47 | 48 | #17.11.2020 20:14 49 | - added missing reset of notification state (not critical), so that notifications are fired again after everything was back to normal (e.g. HB down -> notification, HB up again -> reset, HB down -> again notification) 50 | - you can also get notified now when a state changed back to normal (but is disabled by default) 51 | - now if temperature is unknown -> it is not shown at all 52 | - added possibility to ignore plugins, Homebridge or Node.js during checking for updates 53 | - for plugins, enter their npm name (e.g. 'homebridge-fritz') as string in the given empty array 54 | - for Homebridge enter 'HOMEBRIDGE_UTD' and for Node enter 'NODEJS_UTD' in the empty array 55 | 56 | #16.11.2020 22:24 57 | - added support for entering credentials via widget parameter 58 | - parameter must have the format like admin,,mypassword123,,http://192.168.178.33:8581 59 | - added some concrete error information that should help setting up the widget 60 | 61 | #16.11.2020 20:39 62 | - added support for notifications when some status changed 63 | - supported: Homebridge stopped, Homebridge Update available, Plugin Update available, Node.js Update available 64 | - new screenshots 65 | - added changelog 66 | - some filemanager usage refactoring 67 | 68 | #15.11.2020 14:26 69 | - overhaul of the status icons, now not using emoji anymore but SFSymbols 70 | - more handling when some requests return undefined 71 | - new screenshots 72 | 73 | 74 | #15.11.2020 11:25 75 | - fixed the black text color when user uses light mode (now text is always white) 76 | - added unknown status if API requests return undefined 77 | - now user can choose between default purple background and a black background 78 | - now user can set the icons used at a central spot 79 | 80 | 81 | #14.11.2020 21:44 82 | - added possibility to switch the file manager to local via a variable 83 | 84 | 85 | #14.11.2020 21:26 86 | - added support to show temperature in Fahrenheit 87 | - more version infos 88 | 89 | 90 | #14.11.2020 20:48 91 | - just added some infos about the versions of all the systems 92 | 93 | 94 | #14.11.2020 20:36 95 | - fixed a critical bug (forgot to include the logic for node.js UTD) 96 | 97 | 98 | #14.11.2020 18:16 99 | - initial commit of the first version of the script -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 lwitzani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](images/widget_display.jpg) 2 | 3 | # Homebridge Status Widget 4 | - Script for the iOS App Scriptable that shows a small summary of your Homebridge instance 5 | - All infos shown are based and provided by the Homebridge Config UI X found at https://github.com/oznu/homebridge-config-ui-x 6 | - Thanks to the github user oznu for providing such a nice programm! 7 | - This script does not work if you don't have the Homebridge service (Homebridge Config UI X) running 8 | - This script was developed with Homebridge Config UI X in version 4.32.0 (2020-11-06), Homebridge at version 1.1.6 and Scriptable app in version 1.6.1 on iOS 14.2. Maybe you need to update the UI-service OR Homebridge OR the Scriptable app OR your iPhone if this script does not work for you 9 | - also thanks to github user kevinkub for providing a line chart example at https://gist.github.com/kevinkub/b74f9c16f050576ae760a7730c19b8e2 10 | 11 | # How to use (3 setup possibilities) 12 | - the best way (updatable, supporting the install script from https://scriptdu.de (very recommended)): 13 | - the script has a configuration mechanism that saves all configurations (in the Configuration class) to iCloud persistently 14 | - this means the configuration you make can be reused once you install a newer version of this script 15 | - following variables exist in the configuration: ![](images/config.png) 16 | - three variables controll this mechanism: 17 | - configurationFileName = 'purple.json' // change this to an own name e.g. 'configBlack.json' . This name can then be given as a widget parameter in the form 'USE_CONFIG:yourfilename.json' so you don't loose your preferred configuration across script updates (but you will loose it if i have to change the configuration format) 18 | - usePersistedConfiguration = true; // false would mean to use the visible configuration below; true means the state saved in iCloud (or locally) will be used 19 | - overwritePersistedConfig = false; // if you like your configuration, run the script ONCE with this param to true, then it is saved and can be used via 'USE_CONFIG:yourfilename.json' in widget params 20 | - so basically what you need to do is: 21 | - choose a configurationFileName (must end with '.json') 22 | - set overwritePersistedConfig to true 23 | - configure every configuration-variable exactly as you want (including the CREDENTIALS and the URL!) 24 | - run the script once (this creates a json file in icloud, you can always delete it to start from scratch) 25 | - set overwritePersistedConfig to false 26 | - set the widget up with a single parameter in the format 'USE_CONFIG:yourfilename.json' ![](images/use_config_via_parameter.jpeg) 27 | - as long as overwritePersistedConfig is false, any change to the config won't take any effect because the persisted one is used if usePersistedConfiguration is true 28 | - another updatable way (but configuration is lost): 29 | - set the widget up with parameter in the format \,,\,,\ 30 | - a valid real example: "admin,,mypassword123,,http://192.168.178.33:8581" 31 | - if you have authentication set to non in UI-X then just provide any char. Valid would be e.g. "x,,x,,http://192.168.178.33:8581" 32 | - maybe you need to set usePersistedConfiguration in the config to false to use this older way 33 | - screenshot of an example when setting it up: ![](images/example_parameter_setup.jpeg) 34 | 35 | - hard coded way in the script (not recommended): you need to configure 36 | - the **URL** of the system running the Homebridge Config UI X (the hb-service), including the port e.g. http://192.168.178.33:8581 37 | - **username** of the administrator of the homebridge-config-ui-x instance (not the actual linux user) 38 | - **password** of the administrator of the homebridge-config-ui-x instance 39 | - the residual parameter can be tweaked a bit for your needs 40 | - e.g. fileManagerMode, must be set to LOCAL if you do not use iCloud Drive. Default is ICLOUD 41 | - e.g. the systemGuiName, the name of your system running the Homebridge Config UI X (the hb-service) 42 | - e.g. the timeout could be increased if your system does not respond within 2 second 43 | - e.g. set the temperatureUnitConfig to 'FAHRENHEIT' to use °F instead of °C 44 | - if your homebridge-config-ui-x instance is not reached within the specified timeout (currently 2sec) the following screen is shown: ![](images/notAvailable_purple.jpg) 45 | 46 | # Notifications 47 | - the widget now can notify you when a status has changed 48 | - you will get a notification if: 49 | - your Homebridge stopped running 50 | - there is an update available for Homebridge 51 | - there is an update available for one of your plugins 52 | - there is an update available for node.js 53 | - disable notifications by setting notificationEnabled to false 54 | - enable getting notification when any status was red and is now back to green (normal) by setting the variable disableStateBackToNormalNotifications to false 55 | - edit the variable notificationIntervalInDays to lengthen or shorten the time between getting the same notification (e.g. plugin update available) again 56 | - 0 means you get a notification every time the script runs (not recommended) 57 | - 1 means you get each possible notification to a maximum of 1 time per day 58 | - 0.5 means you get each possible notification to a maximum of 2 times per day 59 | - Open a notification to reveal the "Show me!" button which takes you directly to Homebridge Config UI X 60 | - Here are some screenshots: 61 | ![](images/notification_homebridge_stopped.jpg) 62 | ![](images/notification_homebridge_update.jpg) 63 | ![](images/notification_homebridge_update_extended.jpg) 64 | 65 | # Ignoring specific plugin or software updates 66 | - by filling the empty array of the variable pluginsOrSwUpdatesToIgnore with strings, you can now configure to ignore plugins, Homebridge or Node.js during checking for updates 67 | - succesfully ignored software will not influence the shown status (e.g. ignoring homebridge UTD status will result in showing the green status always even if there is an update available) 68 | - for ignoring plugins, enter their npm name (e.g. 'homebridge-fritz') as string in the given empty array 69 | - for ignoring Homebridge enter 'HOMEBRIDGE_UTD' and for Node enter 'NODEJS_UTD' in the empty array 70 | - a valid example of the variable would be const pluginsOrSwUpdatesToIgnore = ['homebridge-fritz', 'HOMEBRIDGE_UTD', 'NODEJS_UTD']; 71 | - if you specify something and run the script inside the Scriptable app, you will get a log output to let you know that you ignored something successfully 72 | 73 | # Special GUI when running the script via Siri shortcut 74 | - when you set up a shortcut that executes the script, a different GUI is shown 75 | - the Siri GUI shows a simple list of available updates so you can check now which of the software have an update 76 | - also i coded in to let siri speak an answer. This can be disabled by setting the according property in the configuration. 77 | - this is what it looks like: ![](images/siri_shortcut.PNG) 78 | 79 | # Support for iOS 16 lock screen widgets 80 | - you can configure the widget to show on iOS 16's lock screen 81 | - there is nothing to do additionally, the configuration is just as before 82 | - there is only support for the widget that takes up 2 of the 4 slots 83 | - this is what it looks like: ![](images/ios16LockscreenWidget.jpg) 84 | 85 | # Styling 86 | - all things shown below are saved in the configuration file and can be reused in the future after the script logic updates 87 | - all important texts can be changed to your own texts 88 | - if you change the top right texts you probably need to adapt the spacing (play around with the variables spacer_beforeFirstStatusColumn, etc and also with spaces in the text) 89 | - at the top of the script there is a variable bgColorMode that you can set to 'PURPLE_LIGHT', 'PURPLE_DARK', 'BLACK_LIGHT', BLACK_DARK', or 'CUSTOM' 90 | ![](images/widget_purple_light.jpg) 91 | ![](images/widget_purple_dark.jpg) 92 | ![](images/widget_black_light.jpg) 93 | ![](images/widget_black_dark.jpg) 94 | - in CUSTOM mode the values defined in customBackgroundColor1_light, customBackgroundColor2_light, customBackgroundColor1_dark and customBackgroundColor2_dark are used (you can choose!) 95 | ![](images/widget_custom_blue.jpg) 96 | ![](images/widget_custom_blue_green_charts.jpg) 97 | - adaptToLightOrDarkMode toggles to react to light/dark mode automatically 98 | - if you use adaptToLightOrDarkMode with mode CUSTOM then customBackgroundColor1_light and customBackgroundColor2_light together are used in the light version and of course customBackgroundColor1_dark and customBackgroundColor2_dark in the dark version 99 | - fontColor_light and fontColor_dark sets all texts to your chosen color (default is fontColor_light). Works together with adaptToLightOrDarkMode 100 | - chartColor_light and chartColor_dark controls which color the charts have (default is chartColor_light). Works together with adaptToLightOrDarkMode 101 | - you even can experiment with logoUrl and choose another logo to download from anywhere 102 | - you can change the SFSymbols used together with their color (the icons) and the emojis failIcon = ❌ and bulletPointIcon = 🔸 by providing any other emoji 103 | 104 | # Infos shown in the widget 105 | - if Homebridge is running 106 | - if Homebridge is up to date 107 | - if all of the installed plugins (including Homebridge Config UI X) are up to date 108 | - if node.js is up to date 109 | - CPU load 110 | - CPU temperature 111 | - RAM usage 112 | - Uptime for the system the hb-service is running on 113 | - Uptime for the hb-service (Homebridge Config UI X) 114 | 115 | # Troubleshoot 116 | - if the temperature is not shown for you, then the information is not available on your machine 117 | - triple check the credentials (2FA currently not supported) 118 | - consider increasing the requestTimeoutInterval variable 119 | - if some error occurs always check that you have the matching versions 120 | - the Scriptable app 1.6.1 121 | - Homebridge Config UI X 4.32.0 (2020-11-06) 122 | - Homebridge 1.1.6 123 | - iOS 14.2 124 | - if your Homebridge Config UI X is reachable and the authentication process succeeded but the further API requests take to long or fail you will get a screen similar to ![](images/unknown.jpg) 125 | - open a github issue if you can't figure it out what the problem is -------------------------------------------------------------------------------- /homebridgeStatusWidget.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: blue; icon-glyph: magic; 4 | // Check the readme at https://github.com/lwitzani/homebridgeStatusWidget for setup instructions, troubleshoots and also for updates of course! 5 | // Code Version: 16.11.2022 6 | // ********* 7 | // For power users: 8 | // I added a configuration mechanism so you don't need to reconfigure it every time you update the script! 9 | // Please check the readme for instructions on how to use the persist mechanism for the configuration 10 | let configurationFileName = 'purple.json' // change this to an own name e.g. 'configBlack.json' . This name can then be given as a widget parameter in the form 'USE_CONFIG:yourfilename.json' so you don't loose your preferred configuration across script updates (but you will loose it if i have to change the configuration format) 11 | const usePersistedConfiguration = true; // false would mean to use the visible configuration below; true means the state saved in iCloud (or locally) will be used 12 | const overwritePersistedConfig = false; // if you like your configuration, run the script ONCE with this param to true, then it is saved and can be used via 'USE_CONFIG:yourfilename.json' in widget params 13 | // ********* 14 | 15 | const CONFIGURATION_JSON_VERSION = 3; // never change this! If i need to change the structure of configuration class, i will increase this counter. Your created config files sadly won't be compatible afterwards. 16 | // CONFIGURATION ////////////////////// 17 | class Configuration { 18 | // you must at least configure the next 3 lines to make this script work or use credentials in parameter when setting up the widget (see the readme on github) 19 | // if you don't use credentials, just enter the URL and it should work 20 | // as soon as credentials + URL are correct, a configuration is saved and then used. to make changes after that set overwritePersistedConfig to true 21 | hbServiceMachineBaseUrl = '>enter the ip with the port here<'; // location of your system running the hb-service, e.g. http://192.168.178.33:8581 22 | userName = '>enter username here<'; // username of administrator of the hb-service 23 | password = '>enter password here<'; // password of administrator of the hb-service 24 | notificationEnabled = true; // set to false to disable all notifications 25 | 26 | notificationIntervalInDays = 1; // minimum amount of days between the notification about the same topic; 0 means notification everytime the script is run (SPAM). 1 means you get 1 message per status category per day (maximum of 4 messages per day since there are 4 categories). Can also be something like 0.5 which means in a day you can get up to 8 messages 27 | disableStateBackToNormalNotifications = true; // set to false, if you want to be notified e.g. when Homebridge is running again after it stopped 28 | fileManagerMode = 'ICLOUD'; // default is ICLOUD. If you don't use iCloud Drive use option LOCAL 29 | temperatureUnitConfig = 'CELSIUS'; // options are CELSIUS or FAHRENHEIT 30 | requestTimeoutInterval = 3; // in seconds; If requests take longer, the script is stopped. Increase it if it doesn't work or you 31 | pluginsOrSwUpdatesToIgnore = []; // a string array; enter the exact npm-plugin-names e.g. 'homebridge-fritz' or additionally 'HOMEBRIDGE_UTD' or 'NODEJS_UTD' if you do not want to have them checked for their latest versions 32 | adaptToLightOrDarkMode = true; // if one of the purple or black options is chosen, the widget will adapt to dark/light mode if true 33 | bgColorMode = 'PURPLE_LIGHT'; // default is PURPLE_LIGHT. Other options: PURPLE_DARK, BLACK_LIGHT, BLACK_DARK, CUSTOM (custom colors will be used, see below) 34 | customBackgroundColor1_light = '#3e00fa'; // if bgColorMode CUSTOM is used a LinearGradient is created from customBackgroundColor1_light and customBackgroundColor2_light 35 | customBackgroundColor2_light = '#7a04d4'; // you can use your own colors here; they are saved in the configuration 36 | customBackgroundColor1_dark = '#3e00fa'; // if bgColorMode CUSTOM together with adaptToLightOrDarkMode = true is used, the light and dark custom values are used depending on the active mode 37 | customBackgroundColor2_dark = '#7a04d4'; 38 | chartColor_light = '#FFFFFF'; // _light is the default color if adaptToLightOrDarkMode is false 39 | chartColor_dark = '#FFFFFF'; 40 | fontColor_light = '#FFFFFF'; // _light the default color if adaptToLightOrDarkMode is false 41 | fontColor_dark = '#FFFFFF'; 42 | failIcon = '❌'; 43 | bulletPointIcon = '🔸'; 44 | decimalChar = ','; // if you like a dot as decimal separator make the comma to a dot here 45 | jsonVersion = CONFIGURATION_JSON_VERSION; // do not change this 46 | enableSiriFeedback = true; // when running script via Siri, she should speak the text that is defined below BUT might be bugged atm, i wrote the dev about it 47 | 48 | // logo is downloaded only the first time! It is saved in iCloud and then loaded from there everytime afterwards 49 | logoUrl = 'https://raw.githubusercontent.com/homebridge/branding/latest/logos/homebridge-silhouette-round-white.png'; 50 | 51 | // icons: 52 | icon_statusGood = 'checkmark.circle.fill'; // can be any SFSymbol 53 | icon_colorGood = '#' + Color.green().hex; // must have form like '#FFFFFF' 54 | icon_statusBad = 'exclamationmark.triangle.fill'; // can be any SFSymbol 55 | icon_colorBad = '#' + Color.red().hex;// must have form like '#FFFFFF' 56 | icon_statusUnknown = 'questionmark.circle.fill'; // can be any SFSymbol 57 | icon_colorUnknown = '#' + Color.yellow().hex; // must have form like '#FFFFFF' 58 | 59 | // internationalization: 60 | status_hbRunning = 'Running'; 61 | status_hbUtd = 'UTD'; 62 | status_pluginsUtd = 'Plugins UTD '; // maybe add spaces at the end if you see '...' in the widget 63 | status_nodejsUtd = 'Node.js UTD '; 64 | // if you change the descriptions in the status columns, you must adapt the spacers between the columns, so that it looks good again :) 65 | spacer_beforeFirstStatusColumn = 8; 66 | spacer_betweenStatusColumns = 5; 67 | spacer_afterSecondColumn = 0; 68 | 69 | title_cpuLoad = 'CPU Load: '; 70 | title_cpuTemp = 'CPU Temp: '; 71 | title_ramUsage = 'RAM Usage: '; 72 | title_uptimes = 'Uptimes:'; 73 | 74 | title_uiService = 'UI-Service: '; 75 | title_systemGuiName = 'Raspberry Pi: '; // name of the system your service is running on 76 | 77 | notification_title = 'Homebridge Status changed:'; 78 | notification_expandedButtonText = 'Show me!'; 79 | notification_ringTone = 'event'; // all ringtones of Scriptable are possible: default, accept, alert, complete, event, failure, piano_error, piano_success, popup 80 | 81 | 82 | notifyText_hbNotRunning = 'Your Homebridge instance stopped 😱'; 83 | notifyText_hbNotUtd = 'Update available for Homebridge 😎'; 84 | notifyText_pluginsNotUtd = 'Update available for one of your Plugins 😎'; 85 | 86 | notifyText_nodejsNotUtd = 'Update available for Node.js 😎'; 87 | notifyText_hbNotRunning_backNormal = 'Your Homebridge instance is back online 😁'; 88 | notifyText_hbNotUtd_backNormal = 'Homebridge is now up to date ✌️'; 89 | notifyText_pluginsNotUtd_backNormal = 'Plugins are now up to date ✌️'; 90 | notifyText_nodejsNotUtd_backNormal = 'Node.js is now up to date ✌️'; 91 | 92 | siriGui_title_update_available = 'Available Updates:'; 93 | siriGui_title_all_UTD = 'Everything is up to date!'; 94 | siriGui_icon_version = 'arrow.right.square.fill'; // can be any SFSymbol 95 | siriGui_icon_version_color = '#' + Color.blue().hex; // must have form like '#FFFFFF' 96 | siri_spokenAnswer_update_available = 'At least one update is available'; 97 | siri_spokenAnswer_all_UTD = 'Everything is up to date'; 98 | 99 | error_noConnectionText = ' ' + this.failIcon + ' UI-Service not reachable!\n ' + this.bulletPointIcon + ' Server started?\n ' + this.bulletPointIcon + ' UI-Service process started?\n ' + this.bulletPointIcon + ' Server-URL ' + this.hbServiceMachineBaseUrl + ' correct?\n ' + this.bulletPointIcon + ' Are you in the same network?'; 100 | error_noConnectionLockScreenText = ' ' + this.failIcon + ' UI-Service not reachable!\n ' + this.bulletPointIcon + ' Server started?\n ' + this.bulletPointIcon + ' UI-Service process started?\n ' + this.bulletPointIcon + ' ' + this.hbServiceMachineBaseUrl + ' correct?\n ' + this.bulletPointIcon + ' Are you in the same network?'; 101 | 102 | widgetTitle = ' Homebridge '; 103 | dateFormat = 'dd.MM.yyyy HH:mm:ss'; // for US use 'MM/dd/yyyy HH:mm:ss'; 104 | hbLogoFileName = Device.model() + 'hbLogo.png'; 105 | headerFontSize = 12; 106 | informationFontSize = 10; 107 | chartAxisFontSize = 7; 108 | dateFontSize = 7; 109 | notificationJsonFileName = 'notificationState.json'; // multiple scripts for different homebridge instances should point to a different notificationJsonFileName 110 | } 111 | 112 | // CONFIGURATION END ////////////////////// 113 | 114 | let CONFIGURATION = new Configuration(); 115 | const noAuthUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/auth/noauth'; 116 | const authUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/auth/login'; 117 | const cpuUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/cpu'; 118 | const overallStatusUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/homebridge'; 119 | const ramUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/ram'; 120 | const uptimeUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/uptime'; 121 | const pluginsUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/plugins'; 122 | const hbVersionUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/homebridge-version'; 123 | const nodeJsUrl = () => CONFIGURATION.hbServiceMachineBaseUrl + '/api/status/nodejs'; 124 | 125 | 126 | const timeFormatter = new DateFormatter(); 127 | const maxLineWidth = 300; // if layout doesn't look good for you, 128 | const normalLineHeight = 35; // try to tweak the (font-)sizes & remove/add spaces below 129 | let headerFont, infoFont, chartAxisFont, updatedAtFont, token, fileManager; 130 | 131 | let infoPanelFont = Font.semiboldMonospacedSystemFont(10); 132 | let iconSize = 13; 133 | let verticalSpacerInfoPanel = 5; 134 | 135 | const purpleBgGradient_light = createLinearGradient('#421367', '#481367'); 136 | const purpleBgGradient_dark = createLinearGradient('#250b3b', '#320d47'); 137 | const blackBgGradient_light = createLinearGradient('#707070', '#3d3d3d'); 138 | const blackBgGradient_dark = createLinearGradient('#111111', '#222222'); 139 | 140 | const UNAVAILABLE = 'UNAVAILABLE'; 141 | 142 | const NOTIFICATION_JSON_VERSION = 1; // never change this! 143 | 144 | const INITIAL_NOTIFICATION_STATE = { 145 | 'jsonVersion': NOTIFICATION_JSON_VERSION, 146 | 'hbRunning': {'status': true}, 147 | 'hbUtd': {'status': true}, 148 | 'pluginsUtd': {'status': true}, 149 | 'nodeUtd': {'status': true} 150 | }; 151 | 152 | class LineChart { 153 | // LineChart by https://kevinkub.de/ 154 | // taken from https://gist.github.com/kevinkub/b74f9c16f050576ae760a7730c19b8e2 155 | constructor(width, height, values) { 156 | this.ctx = new DrawContext(); 157 | this.ctx.size = new Size(width, height); 158 | this.values = values; 159 | } 160 | 161 | _calculatePath() { 162 | let maxValue = Math.max(...this.values); 163 | let minValue = Math.min(...this.values); 164 | let difference = maxValue - minValue; 165 | let count = this.values.length; 166 | let step = this.ctx.size.width / (count - 1); 167 | let points = this.values.map((current, index, all) => { 168 | let x = step * index; 169 | let y = this.ctx.size.height - (current - minValue) / difference * this.ctx.size.height; 170 | return new Point(x, y); 171 | }); 172 | return this._getSmoothPath(points); 173 | } 174 | 175 | _getSmoothPath(points) { 176 | let path = new Path(); 177 | path.move(new Point(0, this.ctx.size.height)); 178 | path.addLine(points[0]); 179 | for (let i = 0; i < points.length - 1; i++) { 180 | let xAvg = (points[i].x + points[i + 1].x) / 2; 181 | let yAvg = (points[i].y + points[i + 1].y) / 2; 182 | let avg = new Point(xAvg, yAvg); 183 | let cp1 = new Point((xAvg + points[i].x) / 2, points[i].y); 184 | let next = new Point(points[i + 1].x, points[i + 1].y); 185 | let cp2 = new Point((xAvg + points[i + 1].x) / 2, points[i + 1].y); 186 | path.addQuadCurve(avg, cp1); 187 | path.addQuadCurve(next, cp2); 188 | } 189 | path.addLine(new Point(this.ctx.size.width, this.ctx.size.height)); 190 | path.closeSubpath(); 191 | return path; 192 | } 193 | 194 | configure(fn) { 195 | let path = this._calculatePath(); 196 | if (fn) { 197 | fn(this.ctx, path); 198 | } else { 199 | this.ctx.addPath(path); 200 | this.ctx.fillPath(path); 201 | } 202 | return this.ctx; 203 | } 204 | } 205 | 206 | class HomeBridgeStatus { 207 | overallStatus; 208 | hbVersionInfos; 209 | hbUpToDate; 210 | pluginVersionInfos; 211 | pluginsUpToDate; 212 | nodeJsVersionInfos; 213 | nodeJsUpToDate; 214 | 215 | constructor() { 216 | } 217 | 218 | async initialize() { 219 | this.overallStatus = await getOverallStatus(); 220 | this.hbVersionInfos = await getHomebridgeVersionInfos(); 221 | this.hbUpToDate = this.hbVersionInfos === undefined ? undefined : !this.hbVersionInfos.updateAvailable; 222 | this.pluginVersionInfos = await getPluginVersionInfos(); 223 | this.pluginsUpToDate = this.pluginVersionInfos === undefined ? undefined : !this.pluginVersionInfos.updateAvailable; 224 | this.nodeJsVersionInfos = await getNodeJsVersionInfos(); 225 | this.nodeJsUpToDate = this.nodeJsVersionInfos === undefined ? undefined : !this.nodeJsVersionInfos.updateAvailable; 226 | return this; 227 | } 228 | } 229 | 230 | // WIDGET INIT ////////////////////// 231 | await initializeFileManager_Configuration_TimeFormatter_Fonts_AndToken(); 232 | if (token === UNAVAILABLE) { 233 | await showNotAvailableWidget(); 234 | // script ends after the next line 235 | return; 236 | } 237 | const homeBridgeStatus = await new HomeBridgeStatus().initialize(); 238 | await handleConfigPersisting(); 239 | await handleNotifications(homeBridgeStatus.overallStatus, homeBridgeStatus.hbUpToDate, homeBridgeStatus.pluginsUpToDate, homeBridgeStatus.nodeJsUpToDate); 240 | await createAndShowWidget(homeBridgeStatus); 241 | return; 242 | 243 | // WIDGET INIT END ////////////////// 244 | 245 | async function initializeFileManager_Configuration_TimeFormatter_Fonts_AndToken() { 246 | // fileManagerMode must be LOCAL if you do not use iCloud drive 247 | fileManager = CONFIGURATION.fileManagerMode === 'LOCAL' ? FileManager.local() : FileManager.iCloud(); 248 | 249 | if (args.widgetParameter) { 250 | // you can either provide as parameter: 251 | // - the config.json file name you want to load the credentials from (must be created before it can be used but highly recommended) 252 | // valid example: 'USE_CONFIG:yourfilename.json' (the 'yourfilename' part can be changed by you) 253 | // this single parameter must start with USE_CONFIG: and end with .json 254 | // - credentials + URL directly (all other changes to the script are lost when you update it e.g. via https://scriptdu.de ) 255 | // credentials must be separated by two commas like ,,,, 256 | // a valid real example: admin,,mypassword123,,http://192.168.178.33:8581 257 | // If no password is needed for you to login just enter anything: xyz,,xyz,,http://192.168.178.33:8581 258 | if (args.widgetParameter.length > 0) { 259 | let foundCredentialsInParameter = useCredentialsFromWidgetParameter(args.widgetParameter); 260 | let fileNameSuccessfullySet = false; 261 | if (!foundCredentialsInParameter) { 262 | fileNameSuccessfullySet = checkIfConfigFileParameterIsProvided(args.widgetParameter); 263 | } 264 | if (!foundCredentialsInParameter && !fileNameSuccessfullySet) { 265 | throw('Format of provided parameter not valid\n2 Valid examples: 1. USE_CONFIG:yourfilename.json\n2. admin,,mypassword123,,http://192.168.178.33:8581'); 266 | } 267 | } 268 | } 269 | if (usePersistedConfiguration && !overwritePersistedConfig) { 270 | CONFIGURATION = await getPersistedObject(getFilePath(configurationFileName), CONFIGURATION_JSON_VERSION, CONFIGURATION, false); 271 | log('Configuration ' + configurationFileName + ' is used! Trying to authenticate...'); 272 | } 273 | timeFormatter.dateFormat = CONFIGURATION.dateFormat; 274 | initializeFonts(); 275 | await initializeToken(); 276 | } 277 | 278 | async function createAndShowWidget(homeBridgeStatus) { 279 | if (config.runsInAccessoryWidget) { 280 | await createAndShowLockScreenWidget(homeBridgeStatus); 281 | } else { 282 | let widget = new ListWidget(); 283 | handleSettingOfBackgroundColor(widget); 284 | if (!config.runsWithSiri) { 285 | await buildUsualGui(widget, homeBridgeStatus); 286 | } else if (config.runsWithSiri) { 287 | await buildSiriGui(widget, homeBridgeStatus); 288 | } 289 | finalizeAndShowWidget(widget); 290 | } 291 | } 292 | 293 | async function createAndShowLockScreenWidget(homeBridgeStatus) { 294 | let widget = new ListWidget(); 295 | handleSettingOfBackgroundColor(widget); 296 | overwriteSizesForLockScreen(); 297 | await buildLockScreenWidgetHeader(widget); 298 | await buildLockScreenWidgetBody(widget, homeBridgeStatus); 299 | await widget.presentSmall(); 300 | Script.setWidget(widget); 301 | Script.complete(); 302 | } 303 | 304 | async function handleConfigPersisting() { 305 | if (usePersistedConfiguration || overwritePersistedConfig) { 306 | // if here, the configuration seems valid -> save it for next time 307 | log('The valid configuration ' + configurationFileName + ' has been saved. Changes can only be applied if overwritePersistedConfig is set to true. Should be set to false after applying changes again!') 308 | persistObject(CONFIGURATION, getFilePath(configurationFileName)); 309 | } 310 | } 311 | 312 | function buildStatusPanelInHeader(titleStack, homeBridgeStatus) { 313 | titleStack.addSpacer(CONFIGURATION.spacer_beforeFirstStatusColumn); 314 | let statusInfo = titleStack.addStack(); 315 | let firstColumn = statusInfo.addStack(); 316 | firstColumn.layoutVertically(); 317 | addStatusInfo(firstColumn, homeBridgeStatus.overallStatus, CONFIGURATION.status_hbRunning); 318 | firstColumn.addSpacer(verticalSpacerInfoPanel); 319 | addStatusInfo(firstColumn, homeBridgeStatus.pluginsUpToDate, CONFIGURATION.status_pluginsUtd); 320 | 321 | statusInfo.addSpacer(CONFIGURATION.spacer_betweenStatusColumns); 322 | 323 | let secondColumn = statusInfo.addStack(); 324 | secondColumn.layoutVertically(); 325 | addStatusInfo(secondColumn, homeBridgeStatus.hbUpToDate, CONFIGURATION.status_hbUtd); 326 | secondColumn.addSpacer(verticalSpacerInfoPanel); 327 | addStatusInfo(secondColumn, homeBridgeStatus.nodeJsUpToDate, CONFIGURATION.status_nodejsUtd); 328 | 329 | titleStack.addSpacer(CONFIGURATION.spacer_afterSecondColumn); 330 | } 331 | 332 | async function showNotAvailableWidget() { 333 | if (!config.runsInAccessoryWidget) { 334 | let widget = new ListWidget(); 335 | handleSettingOfBackgroundColor(widget); 336 | let mainStack = widget.addStack(); 337 | await initializeLogoAndHeader(mainStack); 338 | addNotAvailableInfos(widget, mainStack); 339 | finalizeAndShowWidget(widget); 340 | } else { 341 | overwriteSizesForLockScreen(); 342 | let widget = new ListWidget(); 343 | handleSettingOfBackgroundColor(widget); 344 | await buildLockScreenWidgetHeader(widget); 345 | widget.addSpacer(2); 346 | addStyledText(widget, CONFIGURATION.error_noConnectionLockScreenText, updatedAtFont); 347 | await widget.presentSmall(); 348 | Script.setWidget(widget); 349 | Script.complete(); 350 | } 351 | } 352 | 353 | async function finalizeAndShowWidget(widget) { 354 | if (!config.runsInWidget) { 355 | await widget.presentMedium(); 356 | } 357 | Script.setWidget(widget); 358 | Script.complete(); 359 | } 360 | 361 | async function initializeToken() { 362 | // authenticate against the hb-service 363 | token = await getAuthToken(); 364 | if (token === undefined) { 365 | throw('Credentials not valid'); 366 | } 367 | } 368 | 369 | async function initializeLogoAndHeader(titleStack) { 370 | titleStack.size = new Size(maxLineWidth, normalLineHeight); 371 | const logo = await getHbLogo(); 372 | const imgWidget = titleStack.addImage(logo); 373 | imgWidget.imageSize = new Size(40, 30); 374 | 375 | let headerText = addStyledText(titleStack, CONFIGURATION.widgetTitle, headerFont); 376 | headerText.size = new Size(60, normalLineHeight); 377 | } 378 | 379 | function initializeFonts() { 380 | headerFont = Font.boldMonospacedSystemFont(CONFIGURATION.headerFontSize); 381 | infoFont = Font.systemFont(CONFIGURATION.informationFontSize); 382 | chartAxisFont = Font.systemFont(CONFIGURATION.chartAxisFontSize); 383 | updatedAtFont = Font.systemFont(CONFIGURATION.dateFontSize); 384 | } 385 | 386 | async function buildSiriGui(widget, homeBridgeStatus) { 387 | widget.addSpacer(10); 388 | let titleStack = widget.addStack(); 389 | await initializeLogoAndHeader(titleStack); 390 | buildStatusPanelInHeader(titleStack, homeBridgeStatus); 391 | widget.addSpacer(10); 392 | let mainColumns = widget.addStack(); 393 | mainColumns.size = new Size(maxLineWidth, 100); 394 | 395 | let verticalStack = mainColumns.addStack(); 396 | verticalStack.layoutVertically(); 397 | if (homeBridgeStatus.hbVersionInfos.updateAvailable || homeBridgeStatus.pluginVersionInfos.updateAvailable || homeBridgeStatus.nodeJsVersionInfos.updateAvailable) { 398 | speakUpdateStatus(true); 399 | addStyledText(verticalStack, CONFIGURATION.siriGui_title_update_available, infoFont); 400 | if (homeBridgeStatus.hbVersionInfos.updateAvailable) { 401 | verticalStack.addSpacer(5); 402 | addUpdatableElement(verticalStack, CONFIGURATION.bulletPointIcon + homeBridgeStatus.hbVersionInfos.name + ': ', homeBridgeStatus.hbVersionInfos.installedVersion, homeBridgeStatus.hbVersionInfos.latestVersion); 403 | } 404 | if (homeBridgeStatus.pluginVersionInfos.updateAvailable) { 405 | for (plugin of homeBridgeStatus.pluginVersionInfos.plugins) { 406 | if (CONFIGURATION.pluginsOrSwUpdatesToIgnore.includes(plugin.name)) { 407 | continue; 408 | } 409 | if (plugin.updateAvailable) { 410 | verticalStack.addSpacer(5); 411 | addUpdatableElement(verticalStack, CONFIGURATION.bulletPointIcon + plugin.name + ': ', plugin.installedVersion, plugin.latestVersion); 412 | } 413 | } 414 | } 415 | if (homeBridgeStatus.nodeJsVersionInfos.updateAvailable) { 416 | verticalStack.addSpacer(5); 417 | addUpdatableElement(verticalStack, CONFIGURATION.bulletPointIcon + homeBridgeStatus.nodeJsVersionInfos.name + ': ', homeBridgeStatus.nodeJsVersionInfos.currentVersion, homeBridgeStatus.nodeJsVersionInfos.latestVersion); 418 | } 419 | } else { 420 | speakUpdateStatus(false); 421 | verticalStack.addSpacer(30); 422 | addStyledText(verticalStack, CONFIGURATION.siriGui_title_all_UTD, infoFont); 423 | } 424 | } 425 | 426 | function speakUpdateStatus(updateAvailable) { 427 | if (CONFIGURATION.enableSiriFeedback) { 428 | if (updateAvailable) { 429 | Speech.speak(CONFIGURATION.siri_spokenAnswer_update_available); 430 | } else { 431 | Speech.speak(CONFIGURATION.siri_spokenAnswer_all_UTD); 432 | } 433 | } 434 | } 435 | 436 | async function buildUsualGui(widget, homeBridgeStatus) { 437 | widget.addSpacer(10); 438 | let titleStack = widget.addStack(); 439 | await initializeLogoAndHeader(titleStack); 440 | buildStatusPanelInHeader(titleStack, homeBridgeStatus); 441 | widget.addSpacer(10); 442 | let cpuData = await fetchData(cpuUrl()); 443 | let ramData = await fetchData(ramUrl()); 444 | let usedRamText = getUsedRamString(ramData); 445 | let uptimesArray = await getUptimesArray(); 446 | if (cpuData && ramData) { 447 | let mainColumns = widget.addStack(); 448 | mainColumns.size = new Size(maxLineWidth, 77); 449 | mainColumns.addSpacer(4) 450 | 451 | let cpuColumn = mainColumns.addStack(); 452 | cpuColumn.layoutVertically(); 453 | addStyledText(cpuColumn, CONFIGURATION.title_cpuLoad + getAsRoundedString(cpuData.currentLoad, 1) + '%', infoFont); 454 | addChartToWidget(cpuColumn, cpuData.cpuLoadHistory); 455 | cpuColumn.addSpacer(7); 456 | 457 | let temperatureString = getTemperatureString(cpuData?.cpuTemperature.main); 458 | if (temperatureString) { 459 | let cpuTempText = addStyledText(cpuColumn, CONFIGURATION.title_cpuTemp + temperatureString, infoFont); 460 | cpuTempText.size = new Size(150, 30); 461 | setTextColor(cpuTempText); 462 | } 463 | 464 | mainColumns.addSpacer(11); 465 | 466 | let ramColumn = mainColumns.addStack(); 467 | ramColumn.layoutVertically(); 468 | addStyledText(ramColumn, CONFIGURATION.title_ramUsage + usedRamText + '%', infoFont); 469 | addChartToWidget(ramColumn, ramData.memoryUsageHistory); 470 | ramColumn.addSpacer(7); 471 | 472 | if (uptimesArray) { 473 | let uptimesStack = ramColumn.addStack(); 474 | 475 | let upStack = uptimesStack.addStack(); 476 | addStyledText(upStack, CONFIGURATION.title_uptimes, infoFont); 477 | 478 | let vertPointsStack = upStack.addStack(); 479 | vertPointsStack.layoutVertically(); 480 | 481 | addStyledText(vertPointsStack, CONFIGURATION.bulletPointIcon + CONFIGURATION.title_systemGuiName + uptimesArray[0], infoFont); 482 | addStyledText(vertPointsStack, CONFIGURATION.bulletPointIcon + CONFIGURATION.title_uiService + uptimesArray[1], infoFont); 483 | } 484 | 485 | widget.addSpacer(10); 486 | 487 | // BOTTOM UPDATED TEXT ////////////////////// 488 | let updatedAt = addStyledText(widget, 't: ' + timeFormatter.string(new Date()), updatedAtFont); 489 | updatedAt.centerAlignText(); 490 | } 491 | } 492 | 493 | async function buildLockScreenWidgetHeader(widget) { 494 | let mainStack = widget.addStack(); 495 | const logo = await getHbLogo(); 496 | const imgWidget = mainStack.addImage(logo); 497 | imgWidget.imageSize = new Size(14, 14); 498 | addStyledText(mainStack, CONFIGURATION.widgetTitle, headerFont); 499 | } 500 | 501 | async function buildLockScreenWidgetBody(widget, homeBridgeStatus) { 502 | let verticalStack = widget.addStack(); 503 | verticalStack.layoutVertically(); 504 | buildStatusPanelInHeader(verticalStack, homeBridgeStatus); 505 | await buildCpuRamInfoForLockScreen(verticalStack); 506 | } 507 | 508 | function overwriteSizesForLockScreen() { 509 | infoFont = Font.systemFont(7); 510 | infoPanelFont = Font.semiboldMonospacedSystemFont(7); 511 | iconSize = 8; 512 | CONFIGURATION.spacer_betweenStatusColumns = 2; 513 | CONFIGURATION.spacer_beforeFirstStatusColumn = 2; 514 | verticalSpacerInfoPanel = 1; 515 | timeFormatter.dateFormat = 'HH:mm:ss'; 516 | updatedAtFont = Font.systemFont(6); 517 | } 518 | 519 | async function buildCpuRamInfoForLockScreen(verticalStack) { 520 | let cpuData = await fetchData(cpuUrl()); 521 | let ramData = await fetchData(ramUrl()); 522 | 523 | verticalStack.addSpacer(CONFIGURATION.spacer_beforeFirstStatusColumn); 524 | let statusInfo = verticalStack.addStack(); 525 | let cpuInfos = statusInfo.addStack(); 526 | 527 | let cpuFirstColumn = cpuInfos.addStack(); 528 | cpuFirstColumn.layoutVertically(); 529 | addStyledText(cpuFirstColumn, 'CPU:', infoFont); 530 | cpuInfos.addSpacer(2); 531 | 532 | let cpuSecondColumn = cpuInfos.addStack(); 533 | cpuSecondColumn.layoutVertically(); 534 | addStyledText(cpuSecondColumn, getAsRoundedString(cpuData.currentLoad, 1) + '%', infoFont); 535 | cpuSecondColumn.addSpacer(2); 536 | 537 | let temperatureString = getTemperatureString(cpuData?.cpuTemperature.main); 538 | if (temperatureString) { 539 | addStyledText(cpuSecondColumn, temperatureString, infoFont); 540 | } 541 | 542 | cpuInfos.addSpacer(17); 543 | 544 | let ramInfos = statusInfo.addStack(); 545 | let usedRamText = getUsedRamString(ramData); 546 | 547 | let ramFirstColumn = cpuInfos.addStack(); 548 | ramFirstColumn.layoutVertically(); 549 | addStyledText(ramFirstColumn, 'RAM:', infoFont); 550 | cpuInfos.addSpacer(2); 551 | ramFirstColumn.addSpacer(2); 552 | 553 | let ramSecondColumn = cpuInfos.addStack(); 554 | ramSecondColumn.layoutVertically(); 555 | addStyledText(ramSecondColumn, usedRamText + '%', infoFont); 556 | ramSecondColumn.addSpacer(5); 557 | 558 | addStyledText(ramSecondColumn, ' t: ' + timeFormatter.string(new Date()), updatedAtFont); 559 | } 560 | 561 | function addUpdatableElement(stackToAdd, elementTitle, versionCurrent, versionLatest) { 562 | let itemStack = stackToAdd.addStack(); 563 | itemStack.addSpacer(17); 564 | addStyledText(itemStack, elementTitle, infoFont); 565 | 566 | let vertPointsStack = itemStack.addStack(); 567 | vertPointsStack.layoutVertically(); 568 | 569 | let versionStack = vertPointsStack.addStack(); 570 | addStyledText(versionStack, versionCurrent, infoFont); 571 | versionStack.addSpacer(3); 572 | addIcon(versionStack, CONFIGURATION.siriGui_icon_version, new Color(CONFIGURATION.siriGui_icon_version_color)); 573 | versionStack.addSpacer(3); 574 | addStyledText(versionStack, versionLatest, infoFont); 575 | } 576 | 577 | function handleSettingOfBackgroundColor(widget) { 578 | if (!CONFIGURATION.adaptToLightOrDarkMode) { 579 | switch (CONFIGURATION.bgColorMode) { 580 | case "CUSTOM": 581 | widget.backgroundGradient = createLinearGradient(CONFIGURATION.customBackgroundColor1_light, CONFIGURATION.customBackgroundColor2_light); 582 | break; 583 | case "BLACK_LIGHT": 584 | widget.backgroundGradient = blackBgGradient_light; 585 | break; 586 | case "BLACK_DARK": 587 | widget.backgroundGradient = blackBgGradient_dark; 588 | break; 589 | case "PURPLE_DARK": 590 | widget.backgroundGradient = purpleBgGradient_dark; 591 | break; 592 | case "PURPLE_LIGHT": 593 | default: 594 | widget.backgroundGradient = purpleBgGradient_light; 595 | } 596 | } else { 597 | switch (CONFIGURATION.bgColorMode) { 598 | case "CUSTOM": 599 | setGradient(widget, 600 | createLinearGradient(CONFIGURATION.customBackgroundColor1_light, CONFIGURATION.customBackgroundColor2_light), 601 | createLinearGradient(CONFIGURATION.customBackgroundColor1_dark, CONFIGURATION.customBackgroundColor2_dark)); 602 | break; 603 | case "BLACK_LIGHT": 604 | case "BLACK_DARK": 605 | setGradient(widget, blackBgGradient_light, blackBgGradient_dark); 606 | break; 607 | case "PURPLE_DARK": 608 | case "PURPLE_LIGHT": 609 | default: 610 | setGradient(widget, purpleBgGradient_light, purpleBgGradient_dark); 611 | } 612 | } 613 | } 614 | 615 | function setGradient(widget, lightOption, darkOption) { 616 | if (Device.isUsingDarkAppearance()) { 617 | widget.backgroundGradient = darkOption; 618 | } else { 619 | widget.backgroundGradient = lightOption; 620 | } 621 | } 622 | 623 | function getChartColorToUse() { 624 | if (CONFIGURATION.adaptToLightOrDarkMode && Device.isUsingDarkAppearance()) { 625 | return new Color(CONFIGURATION.chartColor_dark); 626 | } else { 627 | return new Color(CONFIGURATION.chartColor_light); 628 | } 629 | } 630 | 631 | function setTextColor(textWidget) { 632 | if (CONFIGURATION.adaptToLightOrDarkMode && Device.isUsingDarkAppearance()) { 633 | textWidget.textColor = new Color(CONFIGURATION.fontColor_dark); 634 | } else { 635 | textWidget.textColor = new Color(CONFIGURATION.fontColor_light); 636 | } 637 | } 638 | 639 | function createLinearGradient(color1, color2) { 640 | const gradient = new LinearGradient(); 641 | gradient.locations = [0, 1]; 642 | gradient.colors = [new Color(color1), new Color(color2)]; 643 | return gradient; 644 | } 645 | 646 | function addStyledText(stackToAddTo, text, font) { 647 | let textHandle = stackToAddTo.addText(text); 648 | textHandle.font = font; 649 | setTextColor(textHandle); 650 | return textHandle; 651 | } 652 | 653 | function addChartToWidget(column, chartData) { 654 | let horizontalStack = column.addStack(); 655 | horizontalStack.addSpacer(5); 656 | let yAxisLabelsStack = horizontalStack.addStack(); 657 | yAxisLabelsStack.layoutVertically(); 658 | 659 | addStyledText(yAxisLabelsStack, getMaxString(chartData, 2) + '%', chartAxisFont); 660 | yAxisLabelsStack.addSpacer(6); 661 | addStyledText(yAxisLabelsStack, getMinString(chartData, 2) + '%', chartAxisFont); 662 | yAxisLabelsStack.addSpacer(6); 663 | 664 | horizontalStack.addSpacer(2); 665 | 666 | let chartImage = new LineChart(500, 100, chartData).configure((ctx, path) => { 667 | ctx.opaque = false; 668 | ctx.setFillColor(getChartColorToUse()); 669 | ctx.addPath(path); 670 | ctx.fillPath(path); 671 | }).getImage(); 672 | 673 | let vertChartImageStack = horizontalStack.addStack(); 674 | vertChartImageStack.layoutVertically(); 675 | 676 | let chartImageHandle = vertChartImageStack.addImage(chartImage); 677 | chartImageHandle.imageSize = new Size(100, 25); 678 | 679 | let xAxisStack = vertChartImageStack.addStack(); 680 | xAxisStack.size = new Size(100, 10); 681 | 682 | addStyledText(xAxisStack, 't-10m', chartAxisFont); 683 | xAxisStack.addSpacer(75); 684 | addStyledText(xAxisStack, 't', chartAxisFont); 685 | } 686 | 687 | function checkIfConfigFileParameterIsProvided(givenParameter) { 688 | if (givenParameter.trim().startsWith('USE_CONFIG:') && givenParameter.trim().endsWith('.json')) { 689 | configurationFileName = givenParameter.trim().split('USE_CONFIG:')[1]; 690 | if (!fileManager.fileExists(getFilePath(configurationFileName))) { 691 | throw('Config file with provided name ' + configurationFileName + ' does not exist!\nCreate it first by running the script once providing the name in variable configurationFileName and maybe with variable overwritePersistedConfig set to true'); 692 | } 693 | return true; 694 | } 695 | return false; 696 | } 697 | 698 | function useCredentialsFromWidgetParameter(givenParameter) { 699 | if (givenParameter.includes(',,')) { 700 | let credentials = givenParameter.split(',,'); 701 | if (credentials.length === 3 && credentials[0].length > 0 && credentials[1].length > 0 && 702 | credentials[2].length > 0 && credentials[2].startsWith('http')) { 703 | CONFIGURATION.userName = credentials[0].trim(); 704 | CONFIGURATION.password = credentials[1].trim(); 705 | CONFIGURATION.hbServiceMachineBaseUrl = credentials[2].trim(); 706 | return true; 707 | } 708 | } 709 | return false; 710 | } 711 | 712 | async function getAuthToken() { 713 | if (CONFIGURATION.hbServiceMachineBaseUrl === '>enter the ip with the port here<') { 714 | throw('Base URL to machine not entered! Edit variable called hbServiceMachineBaseUrl') 715 | } 716 | let req = new Request(noAuthUrl()); 717 | req.timeoutInterval = CONFIGURATION.requestTimeoutInterval; 718 | const headers = { 719 | 'accept': '*\/*', 'Content-Type': 'application/json' 720 | }; 721 | req.method = 'POST'; 722 | req.headers = headers; 723 | req.body = JSON.stringify({}); 724 | let authData; 725 | try { 726 | authData = await req.loadJSON(); 727 | } catch (e) { 728 | return UNAVAILABLE; 729 | } 730 | if (authData.access_token) { 731 | // no credentials needed 732 | return authData.access_token; 733 | } 734 | 735 | req = new Request(authUrl()); 736 | req.timeoutInterval = CONFIGURATION.requestTimeoutInterval; 737 | let body = { 738 | 'username': CONFIGURATION.userName, 739 | 'password': CONFIGURATION.password, 740 | 'otp': 'string' 741 | }; 742 | req.body = JSON.stringify(body); 743 | req.method = 'POST'; 744 | req.headers = headers; 745 | try { 746 | authData = await req.loadJSON(); 747 | } catch (e) { 748 | return UNAVAILABLE; 749 | } 750 | return authData.access_token; 751 | } 752 | 753 | async function fetchData(url) { 754 | let req = new Request(url); 755 | req.timeoutInterval = CONFIGURATION.requestTimeoutInterval; 756 | let headers = { 757 | 'accept': '*\/*', 'Content-Type': 'application/json', 758 | 'Authorization': 'Bearer ' + token 759 | }; 760 | req.headers = headers; 761 | let result; 762 | try { 763 | result = req.loadJSON(); 764 | } catch (e) { 765 | return undefined; 766 | } 767 | return result; 768 | } 769 | 770 | async function getOverallStatus() { 771 | const statusData = await fetchData(overallStatusUrl()); 772 | if (statusData === undefined) { 773 | return undefined; 774 | } 775 | return statusData.status === 'up'; 776 | } 777 | 778 | async function getHomebridgeVersionInfos() { 779 | if (CONFIGURATION.pluginsOrSwUpdatesToIgnore.includes('HOMEBRIDGE_UTD')) { 780 | log('You configured Homebridge to not be checked for updates. Widget will show that it\'s UTD!'); 781 | return {updateAvailable: false}; 782 | } 783 | const hbVersionData = await fetchData(hbVersionUrl()); 784 | if (hbVersionData === undefined) { 785 | return undefined; 786 | } 787 | return hbVersionData; 788 | } 789 | 790 | async function getNodeJsVersionInfos() { 791 | if (CONFIGURATION.pluginsOrSwUpdatesToIgnore.includes('NODEJS_UTD')) { 792 | log('You configured Node.js to not be checked for updates. Widget will show that it\'s UTD!'); 793 | return {updateAvailable: false}; 794 | } 795 | const nodeJsData = await fetchData(nodeJsUrl()); 796 | if (nodeJsData === undefined) { 797 | return undefined; 798 | } 799 | nodeJsData.name = 'node.js'; 800 | return nodeJsData; 801 | } 802 | 803 | async function getPluginVersionInfos() { 804 | const pluginsData = await fetchData(pluginsUrl()); 805 | if (pluginsData === undefined) { 806 | return undefined; 807 | } 808 | for (plugin of pluginsData) { 809 | if (CONFIGURATION.pluginsOrSwUpdatesToIgnore.includes(plugin.name)) { 810 | log('You configured ' + plugin.name + ' to not be checked for updates. Widget will show that it\'s UTD!'); 811 | continue; 812 | } 813 | if (plugin.updateAvailable) { 814 | return {plugins: pluginsData, updateAvailable: true}; 815 | } 816 | } 817 | return {plugins: pluginsData, updateAvailable: false}; 818 | } 819 | 820 | function getUsedRamString(ramData) { 821 | if (ramData === undefined) return 'unknown'; 822 | return getAsRoundedString(100 - 100 * ramData.mem.available / ramData.mem.total, 2); 823 | } 824 | 825 | async function getUptimesArray() { 826 | const uptimeData = await fetchData(uptimeUrl()); 827 | if (uptimeData === undefined) return undefined; 828 | 829 | return [formatSeconds(uptimeData.time.uptime), formatSeconds(uptimeData.processUptime)]; 830 | } 831 | 832 | function formatSeconds(value) { 833 | if (value > 60 * 60 * 24 * 10) { 834 | return getAsRoundedString(value / 60 / 60 / 24, 0) + 'd'; // more than 10 days 835 | } else if (value > 60 * 60 * 24) { 836 | return getAsRoundedString(value / 60 / 60 / 24, 1) + 'd'; 837 | } else if (value > 60 * 60) { 838 | return getAsRoundedString(value / 60 / 60, 1) + 'h'; 839 | } else if (value > 60) { 840 | return getAsRoundedString(value / 60, 1) + 'm'; 841 | } else { 842 | return getAsRoundedString(value, 1) + 's'; 843 | } 844 | } 845 | 846 | async function loadImage(imgUrl) { 847 | let req = new Request(imgUrl); 848 | req.timeoutInterval = CONFIGURATION.requestTimeoutInterval; 849 | let image = await req.loadImage(); 850 | return image; 851 | } 852 | 853 | async function getHbLogo() { 854 | let path = getFilePath(CONFIGURATION.hbLogoFileName); 855 | if (fileManager.fileExists(path)) { 856 | const fileDownloaded = await fileManager.isFileDownloaded(path); 857 | if (!fileDownloaded) { 858 | await fileManager.downloadFileFromiCloud(path); 859 | } 860 | return fileManager.readImage(path); 861 | } else { 862 | // logo did not exist -> download it and save it for next time the widget runs 863 | const logo = await loadImage(CONFIGURATION.logoUrl); 864 | fileManager.writeImage(path, logo); 865 | return logo; 866 | } 867 | } 868 | 869 | function getFilePath(fileName) { 870 | let dirPath = fileManager.joinPath(fileManager.documentsDirectory(), 'homebridgeStatus'); 871 | if (!fileManager.fileExists(dirPath)) { 872 | fileManager.createDirectory(dirPath); 873 | } 874 | return fileManager.joinPath(dirPath, fileName); 875 | } 876 | 877 | function addNotAvailableInfos(widget, titleStack) { 878 | let statusInfo = titleStack.addText(' '); 879 | setTextColor(statusInfo); 880 | statusInfo.size = new Size(150, normalLineHeight); 881 | let errorText = widget.addText(CONFIGURATION.error_noConnectionText); 882 | errorText.size = new Size(410, 130); 883 | errorText.font = infoFont; 884 | setTextColor(errorText); 885 | 886 | 887 | widget.addSpacer(15); 888 | let updatedAt = widget.addText('t: ' + timeFormatter.string(new Date())); 889 | updatedAt.font = updatedAtFont; 890 | setTextColor(updatedAt); 891 | updatedAt.centerAlignText(); 892 | 893 | return widget; 894 | } 895 | 896 | function getAsRoundedString(value, decimals) { 897 | let factor = Math.pow(10, decimals); 898 | return (Math.round((value + Number.EPSILON) * factor) / factor).toString().replace('.', CONFIGURATION.decimalChar); 899 | } 900 | 901 | function getMaxString(arrayOfNumbers, decimals) { 902 | let factor = Math.pow(10, decimals); 903 | return (Math.round((Math.max(...arrayOfNumbers) + Number.EPSILON) * factor) / factor).toString().replace('.', CONFIGURATION.decimalChar); 904 | } 905 | 906 | function getMinString(arrayOfNumbers, decimals) { 907 | let factor = Math.pow(10, decimals); 908 | return (Math.round((Math.min(...arrayOfNumbers) + Number.EPSILON) * factor) / factor).toString().replace('.', CONFIGURATION.decimalChar); 909 | } 910 | 911 | function getTemperatureString(temperatureInCelsius) { 912 | if (temperatureInCelsius === undefined || temperatureInCelsius < 0) return undefined; 913 | 914 | if (CONFIGURATION.temperatureUnitConfig === 'FAHRENHEIT') { 915 | return getAsRoundedString(convertToFahrenheit(temperatureInCelsius), 1) + '°F'; 916 | } else { 917 | return getAsRoundedString(temperatureInCelsius, 1) + '°C'; 918 | } 919 | } 920 | 921 | function convertToFahrenheit(temperatureInCelsius) { 922 | return temperatureInCelsius * 9 / 5 + 32; 923 | } 924 | 925 | function addStatusIcon(widget, statusBool) { 926 | let name = ''; 927 | let color; 928 | if (statusBool === undefined) { 929 | name = CONFIGURATION.icon_statusUnknown; 930 | color = new Color(CONFIGURATION.icon_colorUnknown); 931 | } else if (statusBool) { 932 | name = CONFIGURATION.icon_statusGood; 933 | color = new Color(CONFIGURATION.icon_colorGood); 934 | } else { 935 | name = CONFIGURATION.icon_statusBad; 936 | color = new Color(CONFIGURATION.icon_colorBad); 937 | } 938 | addIcon(widget, name, color); 939 | } 940 | 941 | function addStatusInfo(lineWidget, statusBool, shownText) { 942 | let itemStack = lineWidget.addStack(); 943 | addStatusIcon(itemStack, statusBool); 944 | itemStack.addSpacer(2); 945 | let text = itemStack.addText(shownText); 946 | text.font = infoPanelFont; 947 | setTextColor(text); 948 | } 949 | 950 | async function handleNotifications(hbRunning, hbUtd, pluginsUtd, nodeUtd) { 951 | if (!CONFIGURATION.notificationEnabled) { 952 | return; 953 | } 954 | let path = getFilePath(CONFIGURATION.notificationJsonFileName); 955 | let state = await getPersistedObject(path, NOTIFICATION_JSON_VERSION, INITIAL_NOTIFICATION_STATE, true); 956 | let now = new Date(); 957 | let shouldUpdateState = false; 958 | if (shouldNotify(hbRunning, state.hbRunning.status, state.hbRunning.lastNotified)) { 959 | state.hbRunning.status = hbRunning; 960 | state.hbRunning.lastNotified = now; 961 | shouldUpdateState = true; 962 | scheduleNotification(CONFIGURATION.notifyText_hbNotRunning); 963 | } else if (hbRunning && !state.hbRunning.status) { 964 | state.hbRunning.status = hbRunning; 965 | state.hbRunning.lastNotified = undefined; 966 | shouldUpdateState = true; 967 | if (!CONFIGURATION.disableStateBackToNormalNotifications) { 968 | scheduleNotification(CONFIGURATION.notifyText_hbNotRunning_backNormal); 969 | } 970 | } 971 | 972 | if (shouldNotify(hbUtd, state.hbUtd.status, state.hbUtd.lastNotified)) { 973 | state.hbUtd.status = hbUtd; 974 | state.hbUtd.lastNotified = now; 975 | shouldUpdateState = true; 976 | scheduleNotification(CONFIGURATION.notifyText_hbNotUtd); 977 | } else if (hbUtd && !state.hbUtd.status) { 978 | state.hbUtd.status = hbUtd; 979 | state.hbUtd.lastNotified = undefined; 980 | shouldUpdateState = true; 981 | if (!CONFIGURATION.disableStateBackToNormalNotifications) { 982 | scheduleNotification(CONFIGURATION.notifyText_hbNotUtd_backNormal); 983 | } 984 | } 985 | 986 | if (shouldNotify(pluginsUtd, state.pluginsUtd.status, state.pluginsUtd.lastNotified)) { 987 | state.pluginsUtd.status = pluginsUtd; 988 | state.pluginsUtd.lastNotified = now; 989 | shouldUpdateState = true; 990 | scheduleNotification(CONFIGURATION.notifyText_pluginsNotUtd); 991 | } else if (pluginsUtd && !state.pluginsUtd.status) { 992 | state.pluginsUtd.status = pluginsUtd; 993 | state.pluginsUtd.lastNotified = undefined; 994 | shouldUpdateState = true; 995 | if (!CONFIGURATION.disableStateBackToNormalNotifications) { 996 | scheduleNotification(CONFIGURATION.notifyText_pluginsNotUtd_backNormal); 997 | } 998 | } 999 | 1000 | if (shouldNotify(nodeUtd, state.nodeUtd.status, state.nodeUtd.lastNotified)) { 1001 | state.nodeUtd.status = nodeUtd; 1002 | state.nodeUtd.lastNotified = now; 1003 | shouldUpdateState = true; 1004 | scheduleNotification(CONFIGURATION.notifyText_nodejsNotUtd); 1005 | } else if (nodeUtd && !state.nodeUtd.status) { 1006 | state.nodeUtd.status = nodeUtd; 1007 | state.nodeUtd.lastNotified = undefined; 1008 | shouldUpdateState = true; 1009 | if (!CONFIGURATION.disableStateBackToNormalNotifications) { 1010 | scheduleNotification(CONFIGURATION.notifyText_nodejsNotUtd_backNormal); 1011 | } 1012 | } 1013 | 1014 | if (shouldUpdateState) { 1015 | persistObject(state, path); 1016 | } 1017 | } 1018 | 1019 | function shouldNotify(currentBool, boolFromLastTime, lastNotifiedDate) { 1020 | return (!currentBool && (boolFromLastTime || isTimeToNotifyAgain(lastNotifiedDate))); 1021 | } 1022 | 1023 | function isTimeToNotifyAgain(dateToCheck) { 1024 | if (dateToCheck === undefined) return true; 1025 | 1026 | let dateInThePast = new Date(dateToCheck); 1027 | let now = new Date(); 1028 | let timeBetweenDates = parseInt((now.getTime() - dateInThePast.getTime()) / 1000); // seconds 1029 | return timeBetweenDates > CONFIGURATION.notificationIntervalInDays * 24 * 60 * 60; 1030 | } 1031 | 1032 | function scheduleNotification(text) { 1033 | let not = new Notification(); 1034 | not.title = CONFIGURATION.notification_title; 1035 | not.body = text; 1036 | not.addAction(CONFIGURATION.notification_expandedButtonText, CONFIGURATION.hbServiceMachineBaseUrl, false); 1037 | not.sound = CONFIGURATION.notification_ringTone; 1038 | not.schedule(); 1039 | } 1040 | 1041 | async function getPersistedObject(path, versionToCheckAgainst, initialObjectToPersist, createIfNotExisting) { 1042 | if (fileManager.fileExists(path)) { 1043 | const fileDownloaded = await fileManager.isFileDownloaded(path); 1044 | if (!fileDownloaded) { 1045 | await fileManager.downloadFileFromiCloud(path); 1046 | } 1047 | let raw, persistedObject; 1048 | try { 1049 | raw = fileManager.readString(path); 1050 | persistedObject = JSON.parse(raw); 1051 | } catch (e) { 1052 | // file corrupted -> remove it 1053 | fileManager.remove(path); 1054 | } 1055 | 1056 | if (persistedObject && (persistedObject.jsonVersion === undefined || persistedObject.jsonVersion < versionToCheckAgainst)) { 1057 | log('Unfortunately, the configuration structure changed and your old config is not compatible anymore. It is now renamed, marked as deprecated and a new one is created with the initial configuration. ') 1058 | persistObject(persistedObject, getFilePath('DEPRECATED_' + configurationFileName)); 1059 | fileManager.remove(path); 1060 | let migratedConfig = {...initialObjectToPersist, ...persistedObject}; 1061 | migratedConfig.jsonVersion = CONFIGURATION_JSON_VERSION; 1062 | persistObject(migratedConfig, path); 1063 | return migratedConfig; 1064 | } else { 1065 | return persistedObject; 1066 | } 1067 | } 1068 | if (createIfNotExisting) { 1069 | // create a new state json 1070 | persistObject(initialObjectToPersist, path); 1071 | } 1072 | return initialObjectToPersist; 1073 | } 1074 | 1075 | function persistObject(object, path) { 1076 | let raw = JSON.stringify(object, null, 2); 1077 | fileManager.writeString(path, raw); 1078 | } 1079 | 1080 | function addIcon(widget, name, color) { 1081 | let sf = SFSymbol.named(name); 1082 | let iconImage = sf.image; 1083 | let imageWidget = widget.addImage(iconImage); 1084 | imageWidget.resizable = true; 1085 | imageWidget.imageSize = new Size(iconSize, iconSize); 1086 | imageWidget.tintColor = color; 1087 | } -------------------------------------------------------------------------------- /images/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/config.png -------------------------------------------------------------------------------- /images/example_parameter_setup.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/example_parameter_setup.jpeg -------------------------------------------------------------------------------- /images/ios16LockscreenWidget.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/ios16LockscreenWidget.jpg -------------------------------------------------------------------------------- /images/notAvailable_purple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/notAvailable_purple.jpg -------------------------------------------------------------------------------- /images/notification_homebridge_stopped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/notification_homebridge_stopped.jpg -------------------------------------------------------------------------------- /images/notification_homebridge_update.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/notification_homebridge_update.jpg -------------------------------------------------------------------------------- /images/notification_homebridge_update_extended.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/notification_homebridge_update_extended.jpg -------------------------------------------------------------------------------- /images/siri_shortcut.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/siri_shortcut.PNG -------------------------------------------------------------------------------- /images/unknown.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/unknown.jpg -------------------------------------------------------------------------------- /images/use_config_via_parameter.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/use_config_via_parameter.jpeg -------------------------------------------------------------------------------- /images/widget_black_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_black_dark.jpg -------------------------------------------------------------------------------- /images/widget_black_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_black_light.jpg -------------------------------------------------------------------------------- /images/widget_custom_blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_custom_blue.jpg -------------------------------------------------------------------------------- /images/widget_custom_blue_green_charts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_custom_blue_green_charts.jpg -------------------------------------------------------------------------------- /images/widget_display.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_display.jpg -------------------------------------------------------------------------------- /images/widget_purple_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_purple_dark.jpg -------------------------------------------------------------------------------- /images/widget_purple_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwitzani/homebridgeStatusWidget/0bf555ce48ee25e2f6e55978f7e6f9ec1ba41bef/images/widget_purple_light.jpg --------------------------------------------------------------------------------