├── Bundles ├── AbacusCountingMachine.zip ├── AbacusTimeTraveler.zip ├── AtHomeSimulator.zip ├── AveragingPlus.zip ├── BIControl.zip ├── ChromecastHelper.zip ├── DeviceCheckPlus.zip ├── DeviceTransformer.zip ├── DeviceWatchdog.zip ├── ErrorMonitor.zip ├── EventEngine.zip ├── FollowMe.zip ├── HomeTracker.zip ├── HubWatchdog.zip ├── Life360Tracker.zip ├── Life360withStates.zip ├── LifeEventCalendar.zip ├── MLBGameDayLive.zip ├── MyRadar.zip ├── NHLGameDayLive.zip ├── NormalStuffLibrary.zip ├── OneataTime.zip ├── OwnTracker.zip ├── PackageExplorer.zip ├── PatternsPlus.zip ├── PeriodicExpressions.zip ├── PresencePlus.zip ├── README.md ├── RemoteWellnessCheck.zip ├── RingKeypadSync.zip ├── SceneControl.zip ├── SendIP2IR.zip ├── SimpleDeviceTimer.zip ├── SimpleIrrigation.zip ├── SimpleKitchenTimer.zip ├── SimpleMultiTile.zip ├── Snapshot.zip ├── TheFlasher.zip ├── TileMaster.zip ├── aDeviceTest.zip ├── masterBundles.json └── myBundles.json ├── Docs ├── Adding a Tile to Dashboard.md ├── BPTWorld Apps and Data Collection.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── How to Install a Custom App or Driver.md ├── LICENSE ├── README.md ├── Things to Remember.md └── packageManifest - What is it.md ├── Drivers ├── Asthma Forecaster │ ├── AF-driver.groovy │ └── README.md ├── Inovelli_VZM36_Zigbee_Canopy │ ├── VZM36-Canopy-Driver.groovy │ ├── VZM36-Fan-Driver.groovy │ ├── VZM36-Light-Driver.groovy │ └── packageManifest.json ├── Marine Weather │ ├── MW-driver.groovy │ └── README.md ├── Pollen Forecaster │ ├── PF-driver.groovy │ └── README.md ├── Send to Hub with CATT │ ├── README.md │ └── STHWC-driver.groovy └── gCalendar │ ├── README.md │ ├── gc-driver.groovy │ └── packageManifest.json ├── LICENSE ├── README.md ├── _config.yml ├── apps ├── FlowEngine │ ├── drawflow-css.min.css │ ├── drawflow-extra.css │ ├── drawflow-js.min.js │ ├── favicon.ico │ ├── flowengine-child.groovy │ ├── flowengine-parent.groovy │ ├── flowengineeditor.html │ ├── flowvars.js │ ├── packageManifest.json │ ├── pickr.es5.min.js │ └── pickr.min.css ├── TheFlasher │ ├── packageManifest.json │ ├── theflasher-child.groovy │ └── theflasher-parent.groovy ├── devicewatchdog │ ├── dw-child.groovy │ ├── dw-parent.groovy │ ├── dw-tiledriver.groovy │ └── packageManifest.json └── simplepush │ ├── simplepush-Barebones.groovy │ ├── simplepushNotifications-app.groovy │ └── simplepushNotifications-driver.groovy ├── info.json ├── old-repo.json ├── repositories.json └── resources ├── README.md ├── images ├── L360-URL.png ├── L360-URL2a.png ├── L360-XMLError2.png ├── blank.png ├── button-green.png ├── button-power-green.png ├── button-power-red.png ├── button-red.png ├── checkMarkGreen2.png ├── cogWithWrench.png ├── door-closed.png ├── door-open.png ├── image-Bryan.png ├── instructions.png ├── logo.png ├── options-green.png ├── options-red.png ├── pp.png ├── pp2.jpg ├── question-mark-icon-2.jpg ├── question-mark-icon.png ├── readme.txt ├── reports.jpg ├── shield-checkmark.png └── shield-lock.png └── media ├── doorclose1.mp3 ├── dooropen1.mp3 ├── fastpops1.mp3 ├── pup1.mp3 └── readme.txt /Bundles/AbacusCountingMachine.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/AbacusCountingMachine.zip -------------------------------------------------------------------------------- /Bundles/AbacusTimeTraveler.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/AbacusTimeTraveler.zip -------------------------------------------------------------------------------- /Bundles/AtHomeSimulator.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/AtHomeSimulator.zip -------------------------------------------------------------------------------- /Bundles/AveragingPlus.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/AveragingPlus.zip -------------------------------------------------------------------------------- /Bundles/BIControl.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/BIControl.zip -------------------------------------------------------------------------------- /Bundles/ChromecastHelper.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/ChromecastHelper.zip -------------------------------------------------------------------------------- /Bundles/DeviceCheckPlus.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/DeviceCheckPlus.zip -------------------------------------------------------------------------------- /Bundles/DeviceTransformer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/DeviceTransformer.zip -------------------------------------------------------------------------------- /Bundles/DeviceWatchdog.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/DeviceWatchdog.zip -------------------------------------------------------------------------------- /Bundles/ErrorMonitor.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/ErrorMonitor.zip -------------------------------------------------------------------------------- /Bundles/EventEngine.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/EventEngine.zip -------------------------------------------------------------------------------- /Bundles/FollowMe.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/FollowMe.zip -------------------------------------------------------------------------------- /Bundles/HomeTracker.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/HomeTracker.zip -------------------------------------------------------------------------------- /Bundles/HubWatchdog.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/HubWatchdog.zip -------------------------------------------------------------------------------- /Bundles/Life360Tracker.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/Life360Tracker.zip -------------------------------------------------------------------------------- /Bundles/Life360withStates.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/Life360withStates.zip -------------------------------------------------------------------------------- /Bundles/LifeEventCalendar.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/LifeEventCalendar.zip -------------------------------------------------------------------------------- /Bundles/MLBGameDayLive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/MLBGameDayLive.zip -------------------------------------------------------------------------------- /Bundles/MyRadar.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/MyRadar.zip -------------------------------------------------------------------------------- /Bundles/NHLGameDayLive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/NHLGameDayLive.zip -------------------------------------------------------------------------------- /Bundles/NormalStuffLibrary.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/NormalStuffLibrary.zip -------------------------------------------------------------------------------- /Bundles/OneataTime.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/OneataTime.zip -------------------------------------------------------------------------------- /Bundles/OwnTracker.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/OwnTracker.zip -------------------------------------------------------------------------------- /Bundles/PackageExplorer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/PackageExplorer.zip -------------------------------------------------------------------------------- /Bundles/PatternsPlus.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/PatternsPlus.zip -------------------------------------------------------------------------------- /Bundles/PeriodicExpressions.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/PeriodicExpressions.zip -------------------------------------------------------------------------------- /Bundles/PresencePlus.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/PresencePlus.zip -------------------------------------------------------------------------------- /Bundles/README.md: -------------------------------------------------------------------------------- 1 | To Install a Bundle: 2 | 3 | In Github: 4 | - Click on the Bundle you want to install (Hubitat/Bundles/xxxx.zip) 5 | - Right-Click on the 'Raw' button and select 'Copy Link Address' 6 | 7 | Back in Hubitat: 8 | - Select 'Bundles' from the left-hand menu 9 | - Click 'Import Zip' 10 | - Select 'Import from URL' 11 | - Paste in the URL (copied from Github) and then click 'Import' 12 | 13 | REMEMBER - All bundles require 'NormalStuffLibrary.zip' to be installed first. 14 | 15 | Also remember, the files here are no longer supported or maintained by BPTWorld. 16 | -------------------------------------------------------------------------------- /Bundles/RemoteWellnessCheck.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/RemoteWellnessCheck.zip -------------------------------------------------------------------------------- /Bundles/RingKeypadSync.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/RingKeypadSync.zip -------------------------------------------------------------------------------- /Bundles/SceneControl.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SceneControl.zip -------------------------------------------------------------------------------- /Bundles/SendIP2IR.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SendIP2IR.zip -------------------------------------------------------------------------------- /Bundles/SimpleDeviceTimer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SimpleDeviceTimer.zip -------------------------------------------------------------------------------- /Bundles/SimpleIrrigation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SimpleIrrigation.zip -------------------------------------------------------------------------------- /Bundles/SimpleKitchenTimer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SimpleKitchenTimer.zip -------------------------------------------------------------------------------- /Bundles/SimpleMultiTile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/SimpleMultiTile.zip -------------------------------------------------------------------------------- /Bundles/Snapshot.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/Snapshot.zip -------------------------------------------------------------------------------- /Bundles/TheFlasher.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/TheFlasher.zip -------------------------------------------------------------------------------- /Bundles/TileMaster.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/TileMaster.zip -------------------------------------------------------------------------------- /Bundles/aDeviceTest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/Bundles/aDeviceTest.zip -------------------------------------------------------------------------------- /Bundles/masterBundles.json: -------------------------------------------------------------------------------- 1 | { 2 | "masterBundles": [{ 3 | "hubitatName": "BPTWorld", 4 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Bundles/myBundles.json" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /Docs/Adding a Tile to Dashboard.md: -------------------------------------------------------------------------------- 1 | # Adding a Tile to Dashboard 2 | 3 | Couple of things to check before trying to display data on a Tile: 4 | - Be sure to have the App setup and collecting data 5 | - Be sure to go into the Device and visually CHECK to make sure the Attribute actually contains the data you want to display on the tile 6 | - CLOSE all Dashboards at this point! This is a very important step. 7 | 8 | Adding The Tile to the Dashboard using Custom Attributes 9 | - Open up the Dashboard (Remember, they should have been Closed up to this point) 10 | - On the Dashboard you want to add the Custom Tile, click '+' in the upper right hand corner (Add A Tile) 11 | - Under 'Pick a Device', select the Device that contains the Custom Attribute you want to display 12 | - Under 'Pick a Template', select 'Attribute' 13 | - Under 'Pick an Attribute', Use the dropdown menu and select the Custom Attribute you want to display. 14 | 1. Remember, you can see the Custom Attributes in the Device you checked before starting to add the tile to the dashboard. 15 | 2. If you are not seeing any Custom Attributes, go back to the 'Couple of things to check' section and start again! 16 | 17 | That's it!
18 | You should now have an amazing new Custom Tile displayed on your Dashboard. 19 |

20 | Thanks,
21 | Bryan
22 | @BPTWorld 23 | -------------------------------------------------------------------------------- /Docs/BPTWorld Apps and Data Collection.md: -------------------------------------------------------------------------------- 1 | # BPTWorld Apps and Data Collection 2 | 3 |
4 | My apps do NOT collect any data of any kind from any users. 5 |

6 | Each app does reach out to GitHub to retrieve the Header and Footer messages. The file, info.json, is located in the main directory. This file is openly available to anyone who would want to see what's in it. Simply click on it to view its contents. 7 |

8 | I do not collect any IP addresses, download statistics, app usage or any other information - I ca not tell you how many people use or download any of my apps/drivers or even access the json file. ABSOLUTELY NOTHING. 9 |

10 | Remember - Your data is your data and what you do with it is your business and nobody else's. 11 |
12 |
13 | Thanks,
14 | Bryan
15 | @BPTWorld 16 | -------------------------------------------------------------------------------- /Docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pull Requests 2 | I encourage all to help make each app/driver better. But please remember that this is my code and if I don't understand what you are trying to add/change, it may not get added. After all, I am the one that will have to edit the code if someone has an issue. 3 | -------------------------------------------------------------------------------- /Docs/How to Install a Custom App or Driver.md: -------------------------------------------------------------------------------- 1 | # How to Install an App or Driver from BPTWorld 2 | Design Usage:
3 | Basic instructions on installing a custom app or driver from GitHub.

4 | 5 | New Install: Copying the Code from GitHub
6 | * Locate the code that you want to install on GitHub 7 | * Copy the Parent code from GitHub using 1 of 2 methods 8 | 1. Click the 'Raw' button, then use 'ctrl + a' to select all of the code and then 'ctrl + c' to copy it 9 | 2. Click the 'Raw button, then copy the URL by double clicking the URL and then using 'ctrl + c' to copy it 10 | * In Hubitat, select ‘Apps Code’, ‘New App’ and paste in the new code, again there are two ways 11 | 1. Be sure your cursor is active on line 1 in the code, use 'ctrl + v' to paste in the raw code 12 | 2. Click the 'Import' button and paste in the URL copied in the previous step, then click 'Ok' on the warning message 13 | * Be sure to click 'Save' each time you add or replace the code 14 | 15 | If the App contains a Child App: 16 | * Follow the same procedure as above to create each child app available 17 | 18 | If the App also contains a Custom Driver: 19 | * Follow the same instructions as above to copy the code 20 | * In Hubitat, select ‘Drivers Code’, ‘New Driver’ and paste in the new code, using one of the methods previously described 21 | * Be sure to click 'Save' each time you add or replace the code 22 | * Do this for each Driver available on GitHub 23 | 24 | New Install: Loading the App in Hubitat
25 | * In Hubitat, go to 'Apps' 26 | * Click ‘Add User Apps’ 27 | * Select ‘(the app you just created)’ ie. 'Follow Me' 28 | * Click 'Done', this will install the App and bring you back to the 'Apps' list 29 | * Now just scroll to the new App and click on it to open it 30 | 31 | That's it!
32 | Don't worry, it gets easier each time you do it! 33 | -------------------------------------------------------------------------------- /Docs/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Docs/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to BPTWorld Apps and Drivers 2 | 3 | Hopefully the information found here will make your installs a little easier! 4 | 5 | Click on each file above to view the contents. 6 | 7 | Thanks,
8 | Bryan
9 | @BPTWorld 10 | -------------------------------------------------------------------------------- /Docs/Things to Remember.md: -------------------------------------------------------------------------------- 1 | # A few things to Remember 2 | Just a few things to keep in mind when using BPTWorld Apps and Drivers 3 | 4 | - I am not a professional programmer, everything I do takes a lot of time and research (then MORE research)! Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: Paypal at: https://paypal.me/bptworld 5 | - When asking for support, please post a log (NOT ONE LINE). I need to see what led up to the error. To post a log, screenshots work best but if you do copy and paste, please do not format the text! This makes it not readable and you will not get the help you need. 6 | 7 | Thank you. 8 | -------------------------------------------------------------------------------- /Docs/packageManifest - What is it.md: -------------------------------------------------------------------------------- 1 | # packageManifest - What is it? 2 | Design Usage:
3 | For use with Hubitat Package Manager 4 | 5 | - Each app/driver will have a packageManifest.json which tells the Hubitat Package Manager all about the app. 6 | - Makes installs a breeze and most importantly, when there are updates! 7 | - There is no need for you to do anything with this file. 8 | 9 | Thanks 10 | -------------------------------------------------------------------------------- /Drivers/Asthma Forecaster/AF-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Asthma Forecaster 3 | * 4 | * Design Usage: 5 | * Retrieve data from asthmaforecast.com. For use with Hubitat dashboards. 6 | * 7 | * Copyright 2019 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to give a shout out on the Hubitat forums to let 10 | * people know that it exists! Thanks. 11 | * 12 | * Remember...I am not a programmer, everything I do takes a lot of time and research (then MORE research)! 13 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 14 | * 15 | * Paypal at: https://paypal.me/bptworld 16 | * 17 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 18 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 19 | * the community grow. Have FUN with it! 20 | * 21 | * ------------------------------------------------------------------------------------------------------------------------------ 22 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 23 | * in compliance with the License. You may obtain a copy of the License at: 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 28 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 29 | * for the specific language governing permissions and limitations under the License. 30 | * 31 | * ------------------------------------------------------------------------------------------------------------------------------ 32 | * 33 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 34 | * 35 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 36 | * 37 | * ------------------------------------------------------------------------------------------------------------------------------ 38 | * 39 | * Changes: 40 | * 41 | * v2.0.4 - 01/03/2020 - Adjustment for AW2 42 | * v2.0.3 - 08/29/2019 - App Watchdog compatible 43 | * v2.0.2 - 05/12/2019 - Added Yesterday data by request 44 | * v2.0.1 - 04/16/2019 - Code cleanup, added importUrl 45 | * v2.0.0 - 04/10/2019 - Code cleanup. Added 'Tomorrow forecast', Updated display for Hubitat dashboard tile (@bptworld) 46 | * Based on ST 'Pollen Virtual Sensor' - Author: jschlackman (james@schlackman.org) 47 | * 48 | */ 49 | 50 | def setVersion(){ 51 | appName = "AsthmaForecasterDriver" 52 | version = "v2.0.4" 53 | dwInfo = "${appName}:${version}" 54 | sendEvent(name: "dwDriverInfo", value: dwInfo, displayed: true) 55 | } 56 | 57 | def updateVersion() { 58 | log.info "In updateVersion" 59 | setVersion() 60 | } 61 | 62 | metadata { 63 | definition (name: "Asthma Forecaster Driver", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Asthma%20Forecaster/AF-driver.groovy") { 64 | capability "Actuator" 65 | capability "Sensor" 66 | capability "Polling" 67 | 68 | attribute "indexToday", "number" 69 | attribute "categoryToday", "string" 70 | attribute "triggersToday", "string" 71 | attribute "indexTomorrow", "number" 72 | attribute "categoryTomorrow", "string" 73 | attribute "triggersTomorrow", "string" 74 | attribute "location", "string" 75 | 76 | attribute "yesterdayTile", "string" 77 | attribute "todayTile", "string" 78 | attribute "tomorrowTile", "string" 79 | 80 | attribute "dwDriverInfo", "string" 81 | command "updateVersion" 82 | } 83 | 84 | preferences { 85 | input name: "about", type: "paragraph", element: "paragraph", title: "Asthma Forecaster", description: "Retrieve data from asthmaforecast.com. For use with Hubitat dashboards." 86 | input name: "zipCode", type: "text", title: "Zip Code", required: true, defaultValue: "${location.zipCode}" 87 | input "fontSizeIndex", "text", title: "Font Size-Index", required: true, defaultValue: "70" 88 | input "fontSizeTriggers", "text", title: "Font Size-Triggers", required: true, defaultValue: "60" 89 | input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: false 90 | } 91 | } 92 | 93 | def installed() { 94 | runEvery1Hour(poll) 95 | poll() 96 | } 97 | 98 | def updated() { 99 | poll() 100 | } 101 | 102 | def uninstalled() { 103 | unschedule() 104 | } 105 | 106 | def poll() { 107 | if(logEnable) log.debug "In poll..." 108 | def asthmaZip = null 109 | 110 | // Use hub zipcode if user has not defined their own 111 | if(zipCode) { 112 | asthmaZip = zipCode 113 | } else { 114 | asthmaZip = location.zipCode 115 | } 116 | 117 | if(logEnable) log.debug "Getting asthma data for ZIP: ${asthmaZip}" 118 | 119 | def params = [ 120 | uri: 'https://www.asthmaforecast.com/api/forecast/current/asthma/', 121 | path: asthmaZip, 122 | headers: [Referer:'https://www.asthmaforecast.com'] 123 | ] 124 | 125 | try { 126 | httpGet(params) {resp -> 127 | resp.data.Location.periods.each {period -> 128 | if(period.Type == 'Yesterday') { 129 | def catName = "" 130 | def indexNum = period.Index.toFloat() 131 | 132 | // Set the category according to index thresholds 133 | if (indexNum < 2.5) {catName = "Low"} 134 | else if (indexNum < 4.9) {catName = "Low-Medium"} 135 | else if (indexNum < 7.3) {catName = "Medium"} 136 | else if (indexNum < 9.7) {catName = "Medium-High"} 137 | else if (indexNum < 12) {catName = "High"} 138 | else {catName = "Unknown"} 139 | 140 | // Build the list of allergen triggers 141 | def triggersList = period.Triggers.inject([]) { result, entry -> 142 | result << "${entry.Name}" 143 | }.join(", ") 144 | state.indexYesterday = period.Index 145 | state.categoryYesterday = catName 146 | state.triggersYesterday = triggersList 147 | 148 | sendEvent(name: "indexYesterday", value: state.indexYesterday, displayed: true) 149 | sendEvent(name: "categoryYesterday", value: state.categoryYesterday, displayed: true) 150 | sendEvent(name: "triggersYesterday", value: state.triggersYesterday, displayed: true) 151 | } 152 | 153 | if(period.Type == 'Today') { 154 | def catName = "" 155 | def indexNum = period.Index.toFloat() 156 | 157 | // Set the category according to index thresholds 158 | if (indexNum < 2.5) {catName = "Low"} 159 | else if (indexNum < 4.9) {catName = "Low-Medium"} 160 | else if (indexNum < 7.3) {catName = "Medium"} 161 | else if (indexNum < 9.7) {catName = "Medium-High"} 162 | else if (indexNum < 12) {catName = "High"} 163 | else {catName = "Unknown"} 164 | 165 | // Build the list of allergen triggers 166 | def triggersList = period.Triggers.inject([]) { result, entry -> 167 | result << "${entry.Name}" 168 | }.join(", ") 169 | state.indexToday = period.Index 170 | state.categoryToday = catName 171 | state.triggersToday = triggersList 172 | 173 | sendEvent(name: "indexToday", value: state.indexToday, displayed: true) 174 | sendEvent(name: "categoryToday", value: state.categoryToday, displayed: true) 175 | sendEvent(name: "triggersToday", value: state.triggersToday, displayed: true) 176 | } 177 | 178 | if(period.Type == 'Tomorrow') { 179 | def catName = "" 180 | def indexNum = period.Index.toFloat() 181 | 182 | // Set the category according to index thresholds 183 | if (indexNum < 2.5) {catName = "Low"} 184 | else if (indexNum < 4.9) {catName = "Low-Medium"} 185 | else if (indexNum < 7.3) {catName = "Medium"} 186 | else if (indexNum < 9.7) {catName = "Medium-High"} 187 | else if (indexNum < 12) {catName = "High"} 188 | else {catName = "Unknown"} 189 | 190 | // Build the list of allergen triggers 191 | def triggersList = period.Triggers.inject([]) { result, entry -> 192 | result << "${entry.Name}" 193 | }.join(", ") 194 | state.indexTomorrow = period.Index 195 | state.categoryTomorrow = catName 196 | state.triggersTomorrow = triggersList 197 | 198 | sendEvent(name: "indexTomorrow", value: state.indexTomorrow, displayed: true) 199 | sendEvent(name: "categoryTomorrow", value: state.categoryTomorrow, displayed: true) 200 | sendEvent(name: "triggersTomorrow", value: state.triggersTomorrow, displayed: true) 201 | } 202 | state.location = resp.data.Location.DisplayLocation 203 | sendEvent(name: "location", value: state.location, displayed: true) 204 | } 205 | } 206 | } 207 | catch (SocketTimeoutException e) { 208 | if(logEnable) log.debug "Connection to asthmaforecast.com API timed out." 209 | sendEvent(name: "location", value: "Connection timed out while retrieving data from API", displayed: true) 210 | } 211 | catch (e) { 212 | if(logEnable) log.debug "Could not retrieve asthma data: $e" 213 | sendEvent(name: "location", value: "Could not retrieve data from API", displayed: true) 214 | } 215 | yesterdayTileMap() 216 | todayTileMap() 217 | tomorrowTileMap() 218 | } 219 | 220 | def configure() { 221 | poll() 222 | } 223 | 224 | def yesterdayTileMap() { 225 | if(logEnable) log.debug "In yesterdayTileMap..." 226 | state.appDataYesterday = "" 227 | state.appDataYesterday+= "" 228 | state.appDataYesterday+= "" 229 | state.appDataYesterday+= "" 230 | state.appDataYesterday+= "
Asthma Forecast Yesterday
${state.location}
${state.indexYesterday} - ${state.categoryYesterday}
${state.triggersYesterday}
" 231 | sendEvent(name: "yesterdayTile", value: state.appDataYesterday, displayed: true) 232 | } 233 | 234 | def todayTileMap() { 235 | if(logEnable) log.debug "In todayTileMap..." 236 | state.appDataToday = "" 237 | state.appDataToday+= "" 238 | state.appDataToday+= "" 239 | state.appDataToday+= "" 240 | state.appDataToday+= "
Asthma Forecast Today
${state.location}
${state.indexToday} - ${state.categoryToday}
${state.triggersToday}
" 241 | sendEvent(name: "todayTile", value: state.appDataToday, displayed: true) 242 | } 243 | 244 | def tomorrowTileMap() { 245 | if(logEnable) log.debug "In tomorrowTileMap..." 246 | state.appDataTomorrow = "" 247 | state.appDataTomorrow+= "" 248 | state.appDataTomorrow+= "" 249 | state.appDataTomorrow+= "" 250 | state.appDataTomorrow+= "
Asthma Forecast Tomorrow
${state.location}
${state.indexTomorrow} - ${state.categoryTomorrow}
${state.triggersTomorrow}
" 251 | sendEvent(name: "tomorrowTile", value: state.appDataTomorrow, displayed: true) 252 | } 253 | 254 | -------------------------------------------------------------------------------- /Drivers/Asthma Forecaster/README.md: -------------------------------------------------------------------------------- 1 | # Asthma Forecaster 2 | Design Usage:
3 | Retrieve data from asthmaforecast.com. For use with Hubitat dashboards.

4 | Please vist my Docs section for Install and other information! 5 |

6 | Thanks,
7 | Bryan
8 | @BPTWorld 9 | -------------------------------------------------------------------------------- /Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Canopy-Driver.groovy: -------------------------------------------------------------------------------- 1 | def getDriverDate() { return "2024-03-27" + " (Basic)" } // **** DATE OF THE DEVICE DRIVER 2 | /* 3 | * Replacement stripped down driver for the Inovelli VZM36 Zigbee Canopy. 4 | * 5 | * Remember...I am not a programmer, everything I do takes a lot of time and research! 6 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 7 | * 8 | * Paypal at: https://paypal.me/bptworld 9 | * https://github.com/bptworld/Hubitat 10 | * 11 | * ------------------------ Original Header ------------------------ 12 | * Author: Eric Maycock (erocm123) 13 | * Contributor: Mark Amber (marka75160) 14 | * Platform: Hubitat 15 | * 16 | * Copyright 2023 Eric Maycock / Inovelli 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 19 | * in compliance with the License. You may obtain a copy of the License at: 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 24 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 25 | * for the specific language governing permissions and limitations under the License. 26 | * 27 | * ------------------------------ 28 | * CHANGE LOG 29 | * ------------------------------ 30 | * 31 | * 2024-03-21 (BPTWorld) Stripped down BASIC Driver 32 | */ 33 | 34 | import groovy.json.JsonSlurper 35 | import groovy.json.JsonOutput 36 | import groovy.transform.Field 37 | import hubitat.device.HubAction 38 | import hubitat.device.HubMultiAction 39 | import hubitat.device.Protocol 40 | import hubitat.helper.HexUtils 41 | import java.security.MessageDigest 42 | 43 | metadata { 44 | definition (name: "Inovelli VZM36 Zigbee Canopy BASIC", namespace: "InovelliUSA", author: "M.Amber/E.Maycock", filename: "") { 45 | capability "Actuator" //device can "do" something (has commands) 46 | capability "Sensor" //device can "report" something (has attributes) 47 | 48 | capability "Configuration" 49 | capability "Initialize" 50 | 51 | attribute "internalTemp", "String" //Internal Temperature in Celsius (read-only P32) 52 | attribute "overHeat", "String" //Overheat Indicator (read-only P33) 53 | attribute "powerSource", "String" //Neutral/non-Neutral (read-only P21) 54 | 55 | 56 | command "getTemperature", [[name:"Get the switch internal operating temperature"]] 57 | command "initialize", [[name:"create child devices, refresh current states"]] 58 | command "updateFirmware", [[name:"Firmware in this channel may be \"beta\" quality"]] 59 | 60 | fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0003,0004,0005,0006,0008,0B05,1000,FC31,FC57", outClusters:"0019", model:"VZM36", manufacturer:"Inovelli" 61 | } 62 | 63 | preferences { 64 | input name: "about", type: "paragraph", element: "paragraph", title: "A BPTWorld Driver", description: "BASIC Driver for use with the Inovelli VZM36 Zigbee Canopy" 65 | input name: "logEnable", type: "bool", title: "Enable Debug Logging", defaultValue: true, description: "" 66 | input name: "disableLogging", type: "number", title: "Disable Debug Logging after this number of minutes", description: "(0=Do not disable, default=20)", defaultValue: 20 67 | } 68 | } 69 | 70 | def debugLogsOff() { 71 | log.warn "${device.displayName} " + "Disabling Debug logging after timeout" 72 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 73 | } 74 | 75 | def configure(option) { //THIS GETS CALLED AUTOMATICALLY WHEN NEW DEVICE IS ADDED OR WHEN CONFIGURE BUTTON SELECTED ON DEVICE PAGE 76 | option = (option==null||option==" ")?"":option 77 | if (logEnable) log.info "${device.displayName} configure($option)" 78 | state.lastCommandSent = "configure($option)" 79 | state.lastCommandTime = nowFormatted() 80 | def cmds = [] 81 | if (logEnable) log.info "${device.displayName} re-establish lifeline bindings to hub" 82 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0000 {${device.zigbeeId}} {}"] //Basic Cluster 83 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0000 {${device.zigbeeId}} {}"] //Basic Cluster ep2 84 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0003 {${device.zigbeeId}} {}"] //Identify Cluster 85 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0003 {${device.zigbeeId}} {}"] //Identify Cluster ep2 86 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0004 {${device.zigbeeId}} {}"] //Group Cluster 87 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0004 {${device.zigbeeId}} {}"] //Group Cluster ep2 88 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0005 {${device.zigbeeId}} {}"] //Scenes Cluster 89 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0005 {${device.zigbeeId}} {}"] //Scenes Cluster ep2 90 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster 91 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster ep2 92 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster 93 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster ep2 94 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0019 {${device.zigbeeId}} {}"] //OTA Upgrade Cluster 95 | 96 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x8021 {${device.zigbeeId}} {}"] //Binding Cluster 97 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x8022 {${device.zigbeeId}} {}"] //UnBinding Cluster 98 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0xFC31 {${device.zigbeeId}} {}"] //Private Cluster 99 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0xFC31 {${device.zigbeeId}} {}"] //Private Cluster ep2 100 | 101 | // Delete obsolete children 102 | getChildDevices().each {child-> 103 | if (!child.deviceNetworkId.startsWith(device.id) || child.deviceNetworkId == "${device.id}-00") { 104 | log.info "Deleting ${child.deviceNetworkId}" 105 | deleteChildDevice(child.deviceNetworkId) 106 | } 107 | } 108 | // Create Child Devices 109 | for (i in 1..2) { 110 | def childId = "${device.id}-0${i}" 111 | def existingChild = getChildDevices()?.find { it.deviceNetworkId == childId} 112 | if (existingChild) { 113 | log.info "${device.displayName} Child device ${childId} already exists (${existingChild})" 114 | } else { 115 | log.info "Creating device ${childId}" 116 | if(i == 1) { 117 | addChildDevice("InovelliUSA","Inovelli VZM36 Zigbee Canopy Light BASIC",childId,[isComponent:true,name:"Canopy Light EP0${i}",label: "${device.displayName} Light"]) 118 | } else { if(i == 2) { 119 | addChildDevice("InovelliUSA","Inovelli VZM36 Zigbee Canopy Fan BASIC", childId,[isComponent:true,name:"Canopy Fan EP0${i}", label: "${device.displayName} Fan"]) 120 | } 121 | } 122 | } 123 | } 124 | if (logEnable) log.debug "${device.displayName} configure $cmds" 125 | return delayBetween(cmds, longDelay) 126 | } 127 | 128 | private String getChildId(childDevice) { 129 | return childDevice.deviceNetworkId?.substring(childDevice.deviceNetworkId.length() - 2)?:"00" 130 | } 131 | 132 | def getRssiLQI(){ 133 | if (logEnable) log.info "${device.displayName} getRssiLQI()" 134 | state.lastCommandSent = "getRssiLQI()" 135 | state.lastCommandTime = nowFormatted() 136 | def cmds = [] 137 | cmds += zigbee.readAttribute(0x0b05, 0x011c, [destEndpoint: 0x01], 50) //CLUSTER_BASIC Mfg 138 | cmds += zigbee.readAttribute(0x0b05, 0x011d, [destEndpoint: 0x01], 50) 139 | return cmds 140 | } 141 | 142 | def getTemperature() { 143 | if (logEnable) log.info "${device.displayName} getTemperature()" 144 | state.lastCommandSent = "getTemperature()" 145 | state.lastCommandTime = nowFormatted() 146 | def cmds = [] 147 | cmds += zigbee.readAttribute(0xfc31, 0x0021, ["mfgCode": "0x122f"], 100) 148 | cmds += zigbee.readAttribute(0xfc31, 0x0020, ["mfgCode": "0x122f"], 100) 149 | return cmds 150 | } 151 | 152 | def initialize() { //CALLED DURING HUB BOOTUP IF "INITIALIZE" CAPABILITY IS DECLARED IN METADATA SECTION 153 | log.info "${device.displayName} initialize()" 154 | //save the group IDs before clearing all the state variables and reset them after 155 | state.clear() 156 | log.info state 157 | state.lastCommandSent = "initialize()" 158 | state.lastCommandTime = nowFormatted() 159 | state.driverDate = getDriverDate() 160 | state.model = device.getDataValue('model') 161 | device.removeSetting("parameter23level") 162 | device.removeSetting("parameter95custom") 163 | device.removeSetting("parameter96custom") 164 | if (logEnable) log.debug "${device.displayName} initialize $cmds" 165 | return 166 | } 167 | 168 | def installed() { //THIS IS CALLED WHEN A DEVICE IS INSTALLED 169 | log.info "${device.displayName} installed()" 170 | state.lastCommandSent = "installed()" 171 | state.lastCommandTime = nowFormatted() 172 | state.driverDate = getDriverDate() 173 | state.model = device.getDataValue('model') 174 | log.info "${device.displayName} Driver Date $state.driverDate" 175 | log.info "${device.displayName} Model=$state.model" 176 | return 177 | } 178 | 179 | def nowFormatted() { 180 | if(location.timeZone) return new Date().format("yyyy-MMM-dd h:mm:ss a", location.timeZone) 181 | else return new Date().format("yyyy MMM dd EEE h:mm:ss a") 182 | } 183 | 184 | def off(childDevice) { 185 | if (logEnable) log.info "${device.displayName} off($childDevice)" 186 | state.lastCommandSent = "off($childDevice)" 187 | state.lastCommandTime = nowFormatted() 188 | def cmds = [] 189 | cmds += "he cmd 0x${device.deviceNetworkId} 0xFF 0x0006 0x0 {}" 190 | if (logEnable) log.debug "${device.displayName} off $cmds" 191 | return cmds 192 | } 193 | 194 | def on(childDevice) { 195 | if (logEnable) log.info "${device.displayName} on($childDevice)" 196 | state.lastCommandSent = "on($childDevice)" 197 | state.lastCommandTime = nowFormatted() 198 | def cmds = [] 199 | cmds += "he cmd 0x${device.deviceNetworkId} 0xFF 0x0006 0x1 {}" 200 | if (logEnable) log.debug "${device.displayName} on $cmds" 201 | return cmds 202 | } 203 | 204 | def parse(String description) { 205 | Map descMap = zigbee.parseDescriptionAsMap(description) 206 | if (logEnable) log.trace "${device.displayName} parse($descMap)" 207 | } 208 | 209 | def updated(option) { // called when "Save Preferences" is requested 210 | option = (option==null||option==" ")?"":option 211 | if (logEnable) log.info "${device.displayName} updated(${option})" + (traceEnable||logEnable)?" $settings":"" 212 | state.lastCommandSent = "updated(${option})" 213 | state.lastCommandTime = nowFormatted() 214 | def cmds = [] 215 | def nothingChanged = true 216 | int defaultValue 217 | int newValue 218 | 219 | if (nothingChanged && (logEnable||logEnable)) log.info "${device.displayName} No DEVICE settings were changed" 220 | log.debug "${device.displayName} Debug logging " + (logEnable?"Enabled":"Disabled") 221 | 222 | if (logEnable && disableDebugLogging) { 223 | log.debug "${device.displayName} Debug Logging will be disabled in $disableDebugLogging minutes" 224 | runIn(disableDebugLogging*60,debugLogsOff) 225 | } 226 | return cmds 227 | } 228 | 229 | List updateFirmware() { 230 | if (logEnable) log.info "${device.displayName} updateFirmware(switch's fwDate: ${state.fwDate}, switch's fwVersion: ${state.fwVersion})" 231 | state.lastCommandSent = "updateFirmware()" 232 | state.lastCommandTime = nowFormatted() 233 | def cmds = [] 234 | cmds += zigbee.updateFirmware() 235 | if (logEnable) log.debug "${device.displayName} updateFirmware $cmds" 236 | return cmds 237 | } 238 | -------------------------------------------------------------------------------- /Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Fan-Driver.groovy: -------------------------------------------------------------------------------- 1 | def getDriverDate() { return "2024-03-27" + " (Basic)" } // **** DATE OF THE DEVICE DRIVER 2 | /* 3 | * Replacement stripped down driver for the Inovelli VZM36 Zigbee Canopy Fan. Features on/off, setSpeed, cycleSpeedUp/cycleSpeedDown and Breeze. 4 | * 5 | * Remember...I am not a programmer, everything I do takes a lot of time and research! 6 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 7 | * 8 | * Paypal at: https://paypal.me/bptworld 9 | * https://github.com/bptworld/Hubitat 10 | * 11 | * ------------------------ Original Header ------------------------ 12 | * Author: Eric Maycock (erocm123) 13 | * Contributor: Mark Amber (marka75160) 14 | * Platform: Hubitat 15 | * 16 | * Copyright 2023 Eric Maycock / Inovelli 17 | 18 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 19 | * in compliance with the License. You may obtain a copy of the License at: 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 24 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 25 | * for the specific language governing permissions and limitations under the License. 26 | * 27 | * ------------------------------ 28 | * CHANGE LOG 29 | * ------------------------------ 30 | * 31 | * 2024-03-21 (BPTWorld) Stripped down BASIC Fan Driver 32 | */ 33 | 34 | import groovy.json.JsonSlurper 35 | import groovy.json.JsonOutput 36 | import groovy.transform.Field 37 | import hubitat.device.HubAction 38 | import hubitat.device.HubMultiAction 39 | import hubitat.device.Protocol 40 | import hubitat.helper.HexUtils 41 | import java.security.MessageDigest 42 | 43 | metadata { 44 | definition (name: "Inovelli VZM36 Zigbee Canopy Fan BASIC", namespace: "InovelliUSA", author: "M.Amber/E.Maycock", importUrl:"") { 45 | capability "Actuator" //device can "do" something (has commands) 46 | capability "Sensor" //device can "report" something (has attributes) 47 | 48 | capability "Configuration" 49 | capability "FanControl" 50 | capability "Initialize" 51 | capability "Switch" 52 | 53 | command "configure", [[name:"Configure"]] 54 | command "initialize", [[name:"Clear State Variables"]] 55 | command "setSpeed", [[name:"FanSpeed*", type:"ENUM", constraints:["off","low","medium-low","medium","medium-high","high","up","down"]]] 56 | command "cycleSpeed", [[name:"Same as Cycle Speed UP"]] 57 | command "cycleSpeedUp", [[name:"Cycle Speed UP"]] 58 | command "cycleSpeedDown", [[name:"Cycle Speed DOWN"]] 59 | command "breeze", [[name:"Random speeds creating a nice Breeze"]] 60 | 61 | attribute "speed", "String" 62 | attribute "switch", "String" 63 | } 64 | 65 | preferences { 66 | input name: "about", type: "paragraph", element: "paragraph", title: "A BPTWorld Driver", description: "BASIC Driver for use with the Inovelli VZM36 Zigbee Canopy Fan" 67 | input name: "logEnable", type: "bool", title: "Enable Debug Logging", defaultValue: true, description: "" 68 | input name: "disableLogLogging", type: "number", title: "Disable Debug Logging after this number of minutes", description: "(0=Do not disable)", defaultValue: 20 69 | } 70 | } 71 | 72 | def initialize() { 73 | log.info "${device.displayName} initialize()" 74 | //save the group IDs before clearing all the state variables and reset them after 75 | state.clear() 76 | state.lastCommandSent = "initialize()" 77 | state.lastCommandTime = nowFormatted() 78 | state.driverDate = getDriverDate() 79 | state.model = parent.getDataValue('model') + "-Fan" 80 | device.removeSetting("parameter23level") 81 | device.removeSetting("parameter95custom") 82 | device.removeSetting("parameter96custom") 83 | } 84 | 85 | def installed() { 86 | log.info "${device.displayName} installed()" 87 | state.lastCommandSent = "installed()" 88 | state.lastCommandTime = nowFormatted() 89 | state.driverDate = getDriverDate() 90 | state.model = parent.getDataValue('model') + "-Fan" 91 | log.info "${device.displayName} Driver Date $state.driverDate" 92 | log.info "${device.displayName} Model=$state.model" 93 | return 94 | } 95 | 96 | def updated(option) { 97 | option = (option==null||option==" ")?"":option 98 | if (logEnable) log.info "${device.displayName} updated(${option})" 99 | state.lastCommandSent = "updated(${option})" 100 | state.lastCommandTime = nowFormatted() 101 | def nothingChanged = true 102 | int defaultValue 103 | int newValue 104 | configParams.each { //loop through all parameters 105 | int i = it.value.number.toInteger() 106 | newValue = calculateParameter(i).toInteger() 107 | defaultValue=getDefaultValue(i).toInteger() 108 | if ([9,10,13,14,15,24,55,56].contains(i)) defaultValue=convertPercentToByte(defaultValue) //convert percent values back to byte values 109 | if ((i==95 && parameter95custom!=null)||(i==96 && parameter96custom!=null)) { //IF a custom hue value is set 110 | if ((Math.round(settings?."parameter${i}custom"?.toInteger()/360*255)==settings?."parameter${i}"?.toInteger())) { //AND custom setting is same as normal setting 111 | device.removeSetting("parameter${i}custom") //THEN clear custom hue and use normal color 112 | if (logEnable||logEnable) log.info "${device.displayName} Cleared Custom Hue setting since it equals standard color setting" 113 | } 114 | } 115 | } 116 | 117 | if (nothingChanged && (logEnable)) log.info "${device.displayName} No DEVICE settings were changed" 118 | log.debug "${device.displayName} Debug logging " + (logEnable?"Enabled":"Disabled") 119 | if (logEnable && disableLogLogging) { 120 | log.info "${device.displayName} Info Logging will be disabled in $disableLogLogging minutes" 121 | runIn(disableLogLogging*60,infoLogsOff) 122 | } 123 | } 124 | 125 | def debugLogsOff() { 126 | log.warn "${device.displayName} " + "Disabling Debug logging after timeout" 127 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 128 | } 129 | 130 | def configure(option) { //THIS GETS CALLED AUTOMATICALLY WHEN NEW DEVICE IS ADDED OR WHEN CONFIGURE BUTTON SELECTED ON DEVICE PAGE 131 | option = (option==null||option==" ")?"":option 132 | if (logEnable) log.info "${device.displayName} configure($option)" 133 | state.lastCommandSent = "configure($option)" 134 | state.lastCommandTime = nowFormatted() 135 | def cmds = [] 136 | if (logEnable) log.info "${device.displayName} re-establish lifeline bindings to hub" 137 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster 138 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster ep2 139 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster 140 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster ep2 141 | if (logEnable) log.debug "${device.displayName} configure $cmds" 142 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 143 | } 144 | 145 | def cycleSpeed() { 146 | cycleSpeedUp() 147 | } 148 | 149 | def cycleSpeedUp() { 150 | def cmds =[] 151 | def currentSpeed = device.currentValue("speed") 152 | if (device.currentValue("switch")=="off") currentSpeed = "off" 153 | def newLevel = 0 154 | def newSpeed = "" 155 | if (currentSpeed=="off" ) {newLevel=33; newSpeed="low" } 156 | else if (currentSpeed=="low") {newLevel=66; newSpeed="medium"} 157 | else if (currentSpeed=="medium") {newLevel=100; newSpeed="high"} 158 | else {newLevel=0; newSpeed="off"} 159 | if (logEnable) log.info "${device.displayName} cycleSpeedUp(${device.currentValue("speed")?:off}->${newSpeed})" 160 | state.lastCommandSent = "cycleSpeedUp(${device.currentValue("speed")?:off}->${newSpeed})" 161 | state.lastCommandTime = nowFormatted() 162 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(newLevel),2)} FFFF}" 163 | if (logEnable) log.debug "${device.displayName} cycleSpeedUp $cmds" 164 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 165 | sendEvent(name:"speed", value: "${newSpeed}") 166 | if(newSpeed != "off") { 167 | sendEvent(name:"switch", value: "on") 168 | } else { 169 | sendEvent(name:"switch", value: "off") 170 | } 171 | } 172 | 173 | def cycleSpeedDown() { 174 | def cmds =[] 175 | def currentSpeed = device.currentValue("speed") 176 | if (device.currentValue("switch")=="off") currentSpeed = "off" 177 | def newLevel = 0 178 | def newSpeed = "" 179 | if (currentSpeed=="off" ) {newLevel=100; newSpeed="high" } 180 | else if (currentSpeed=="high") {newLevel=66; newSpeed="medium"} 181 | else if (currentSpeed=="medium") {newLevel=33; newSpeed="low"} 182 | else {newLevel=0; newSpeed="off"} 183 | if (logEnable) log.info "${device.displayName} cycleSpeedDown(${device.currentValue("speed")?:off}->${newSpeed})" 184 | state.lastCommandSent = "cycleSpeedDown(${device.currentValue("speed")?:off}->${newSpeed})" 185 | state.lastCommandTime = nowFormatted() 186 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(newLevel),2)} FFFF}" 187 | if (logEnable) log.debug "${device.displayName} cycleSpeedDown $cmds" 188 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 189 | sendEvent(name:"speed", value: "${newSpeed}") 190 | if(newSpeed != "off") { 191 | sendEvent(name:"switch", value: "on") 192 | } else { 193 | sendEvent(name:"switch", value: "off") 194 | } 195 | } 196 | 197 | def breeze() { 198 | on() 199 | pauseExecution(500) 200 | breezy() 201 | } 202 | 203 | def breezy() { 204 | def currentSwitch = device.currentValue("switch") 205 | if(currentSwitch == "on") { 206 | currentSpeed = device.currentValue("speed") 207 | speeds = ["low","medium","high"] 208 | def randomKey = new Random().nextInt(3) 209 | rSpeed = speeds[randomKey] 210 | if(logEnable) log.debug "In breezy - currentSpeed: ${currentSpeed} -VS- rSpeed: ${rSpeed}" 211 | if(currentSpeed == rSpeed) { 212 | if(logEnable) log.debug "In breezy - currentSpeed: ${currentSpeed} == rSpeed: ${rSpeed} - Trying again!" 213 | runIn(1, breezy) 214 | } else { 215 | setSpeed(rSpeed) 216 | int randomNum = 20 + (int)(Math.random() * (60 - 20 + 1)) 217 | if(logEnable) log.debug "In breezy - Random seconds to stay at this speed - randomNum: ${randomNum}" 218 | runIn(randomNum, breezy) 219 | } 220 | } 221 | } 222 | 223 | def off() { 224 | if (logEnable) log.info "${device.displayName} off()" 225 | unschedule(breezy) 226 | state.lastCommandSent = "off()" 227 | state.lastCommandTime = nowFormatted() 228 | def cmds = [] 229 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 0 {}" 230 | if (logEnable) log.debug "${device.displayName} off $cmds" 231 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 232 | sendEvent(name:"switch", value: "off") 233 | } 234 | 235 | def on() { 236 | if (logEnable) log.info "${device.displayName} on()" 237 | state.lastCommandSent = "on()" 238 | state.lastCommandTime = nowFormatted() 239 | def cmds = [] 240 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 1 {}" 241 | if (logEnable) log.debug "${device.displayName} on $cmds" 242 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 243 | if(device.currentValue("speed")=="off") { 244 | setSpeed("low") 245 | } else { 246 | sendEvent(name:"switch", value: "on") 247 | } 248 | } 249 | 250 | def parse(String description) { 251 | // 252 | } 253 | 254 | def setSpeed(value) { 255 | if (logEnable) log.info "${device.displayName} setSpeed(${value})" 256 | state.lastCommandSent = "setSpeed(${value})" 257 | state.lastCommandTime = nowFormatted() 258 | def currentLevel = device.currentValue("level")==null?0:device.currentValue("level").toInteger() 259 | if (device.currentValue("switch")=="off") currentLevel = 0 260 | boolean smartMode = device.currentValue("smartFan")=="Enabled" 261 | def newLevel = 0 262 | def cmds = [] 263 | switch (value) { 264 | case "off": 265 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 0 {}" 266 | break 267 | case "low": 268 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(smartMode?20:33),2)} 0xFFFF}" 269 | break 270 | case "medium-low": //placeholder since Hubitat natively supports 5-speed fans 271 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(smartMode?40:33),2)} 0xFFFF}" 272 | break 273 | case "medium": 274 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(smartMode?60:66),2)} 0xFFFF}" 275 | break 276 | case "medium-high": //placeholder since Hubitat natively supports 5-speed fans 277 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(smartMode?80:66),2)} 0xFFFF}" 278 | break 279 | case "high": 280 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(100),2)} 0xFFFF}" 281 | break 282 | case "on": 283 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 1 {}" 284 | break 285 | case "up": 286 | if (currentLevel<=0 ) {newLevel=20} 287 | else if (currentLevel<=20) {newLevel=(smartMode?40:60)} 288 | else if (currentLevel<=40) {newLevel=60} 289 | else if (currentLevel<=60) {newLevel=(smartMode?80:100)} 290 | else if (currentLevel<=100) {newLevel=100} 291 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(newLevel),2)} 0xFFFF}" 292 | break 293 | case "down": 294 | if (currentLevel>80) {newLevel=(smartMode?80:60)} 295 | else if (currentLevel>60) {newLevel=60} 296 | else if (currentLevel>40) {newLevel=(smartMode?40:20)} 297 | else if (currentLevel>20) {newLevel=20} 298 | else if (currentLevel>0) {newLevel=currentLevel} 299 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(newLevel),2)} 0xFFFF}" 300 | break 301 | } 302 | if (logEnable) log.debug "${device.displayName} setSpeed $cmds" 303 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 304 | sendEvent(name:"speed", value: "${value}") 305 | if(value == "off") { 306 | sendEvent(name:"switch", value: "off") 307 | } else { 308 | sendEvent(name:"switch", value: "on") 309 | } 310 | } 311 | 312 | def toggle() { 313 | def toggleDirection = device.currentValue("switch")=="off"?"off->on":"on->off" 314 | if (logEnable) log.info "${device.displayName} toggle(${toggleDirection})" 315 | state.lastCommandSent = "toggle(${toggleDirection})" 316 | state.lastCommandTime = nowFormatted() 317 | def cmds = [] 318 | if(device.currentValue("switch")=="off") { 319 | on() 320 | } else { 321 | off() 322 | } 323 | } 324 | 325 | def getDefaultValue(paramNum=0) { 326 | paramValue=configParams["parameter${paramNum.toString()?.padLeft(3,"0")}"]?.default?.toInteger() 327 | return paramValue?:0 328 | } 329 | 330 | def nowFormatted() { 331 | if(location.timeZone) return new Date().format("yyyy-MMM-dd h:mm:ss a", location.timeZone) 332 | else return new Date().format("yyyy MMM dd EEE h:mm:ss a") 333 | } 334 | 335 | def convertPercentToByte(int value=0) { //convert a 0-100 range where 100%=254. 255 is reserved for special meaning. 336 | value = value==null?0:value //default to 0 if null 337 | value = Math.min(Math.max(value.toInteger(),0),101) //make sure input percent value is in the 0-101 range 338 | value = Math.floor(value/100*255) //convert to 0-255 where 100%=254 and 101 becomes 255 for special meaning 339 | value = value==255?254:value //this ensures that 100% rounds down to byte value 254 340 | value = value>255?255:value //this ensures that 101% rounds down to byte value 255 341 | return value 342 | } 343 | -------------------------------------------------------------------------------- /Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Light-Driver.groovy: -------------------------------------------------------------------------------- 1 | def getDriverDate() { return "2024-03-27" + " (Basic)" } // **** DATE OF THE DEVICE DRIVER 2 | /* 3 | * Replacement stripped down driver for the Inovelli VZM36 Zigbee Canopy Light. Features on/off, setLevel and toggle. 4 | * 5 | * Remember...I am not a programmer, everything I do takes a lot of time and research! 6 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 7 | * 8 | * Paypal at: https://paypal.me/bptworld 9 | * https://github.com/bptworld/Hubitat 10 | * 11 | * ------------------------ Original Header ------------------------ 12 | * Author: Eric Maycock (erocm123) 13 | * Contributor: Mark Amber (marka75160) 14 | * Platform: Hubitat 15 | * 16 | * Copyright 2023 Eric Maycock / Inovelli 17 | * 18 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 19 | * in compliance with the License. You may obtain a copy of the License at: 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 24 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 25 | * for the specific language governing permissions and limitations under the License. 26 | * 27 | * ------------------------------ 28 | * CHANGE LOG 29 | * ------------------------------ 30 | * 31 | * 2024-03-21 (BPTWorld) Stripped down BASIC Light Driver 32 | * 33 | */ 34 | 35 | import groovy.json.JsonSlurper 36 | import groovy.json.JsonOutput 37 | import groovy.transform.Field 38 | import hubitat.device.HubAction 39 | import hubitat.device.HubMultiAction 40 | import hubitat.device.Protocol 41 | import hubitat.helper.HexUtils 42 | import java.security.MessageDigest 43 | 44 | metadata { 45 | definition (name: "Inovelli VZM36 Zigbee Canopy Light BASIC", namespace: "InovelliUSA", author: "M.Amber/E.Maycock", importUrl:"") { 46 | capability "Actuator" //device can "do" something (has commands) 47 | capability "Sensor" //device can "report" something (has attributes) 48 | 49 | capability "Configuration" 50 | capability "Initialize" 51 | capability "Switch" 52 | capability "SwitchLevel" 53 | 54 | command "configure", [[name:"Configure"]] 55 | command "initialize", [[name:"Clear State Variables"]] 56 | command "toggle" 57 | 58 | attribute "level", "number" 59 | attribute "switch", "text" 60 | } 61 | 62 | preferences { 63 | input name: "about", type: "paragraph", element: "paragraph", title: "A BPTWorld Driver", description: "BASIC Driver for use with the Inovelli VZM36 Zigbee Canopy Light" 64 | input name: "logEnable", type: "bool", title: "Enable Debug Logging", defaultValue: false, description: "Detailed diagnostic data" 65 | input name: "disableDebugLogging", type: "number", title: "Disable Debug Logging after this number of minutes", description: "(0=Do not disable)", defaultValue: 5 66 | } 67 | } 68 | 69 | def userSettableParams() { //controls which options are available depending on whether the device is configured as a switch or a dimmer. 70 | if (parameter258 == "1") return [258,22,52, 3, 7, 10,11,12, 15,17,23,24,25,50,95,97] //on/off mode 71 | else return [258,22,52,1,3,5,7,9,10,11,12,14,15,17,23,24,25,50,95,97] //dimmer mode 72 | } 73 | 74 | def debugLogsOff() { 75 | log.warn "${device.displayName} " + "Disabling Debug logging after timeout" 76 | device.updateSetting("logEnable",[value:"false",type:"bool"]) 77 | } 78 | 79 | def configure(option) { //THIS GETS CALLED AUTOMATICALLY WHEN NEW DEVICE IS ADDED OR WHEN CONFIGURE BUTTON SELECTED ON DEVICE PAGE 80 | option = (option==null||option==" ")?"":option 81 | if (logEnable) log.info "${device.displayName} configure($option)" 82 | state.lastCommandSent = "configure($option)" 83 | state.lastCommandTime = nowFormatted() 84 | def cmds = [] 85 | if (logEnable) log.info "${device.displayName} re-establish lifeline bindings to hub" 86 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster 87 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0006 {${device.zigbeeId}} {}"] //On_Off Cluster ep2 88 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster 89 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0x0008 {${device.zigbeeId}} {}"] //Level Control Cluster ep2 90 | if (state.model?.substring(0,5)!="VZM35") { //Fan does not support power/energy reports 91 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0702 {${device.zigbeeId}} {}"] //Simple Metering - to get energy reports 92 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0x0B04 {${device.zigbeeId}} {}"] //Electrical Measurement - to get power reports 93 | } 94 | cmds += ["zdo bind ${device.deviceNetworkId} 0x01 0x01 0xFC31 {${device.zigbeeId}} {}"] //Private Cluster 95 | cmds += ["zdo bind ${device.deviceNetworkId} 0x02 0x01 0xFC31 {${device.zigbeeId}} {}"] //Private Cluster ep2 96 | 97 | if (logEnable) log.debug "${device.displayName} configure $cmds" 98 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 99 | } 100 | 101 | def initialize() { //CALLED DURING HUB BOOTUP IF "INITIALIZE" CAPABILITY IS DECLARED IN METADATA SECTION 102 | log.info "${device.displayName} initialize()" 103 | //save the group IDs before clearing all the state variables and reset them after 104 | state.clear() 105 | state.lastCommandSent = "initialize()" 106 | state.lastCommandTime = nowFormatted() 107 | state.driverDate = getDriverDate() 108 | state.model = parent.getDataValue('model') + "-Light" 109 | device.removeSetting("parameter23level") 110 | device.removeSetting("parameter95custom") 111 | device.removeSetting("parameter96custom") 112 | } 113 | 114 | def installed() { //THIS IS CALLED WHEN A DEVICE IS INSTALLED 115 | log.info "${device.displayName} installed()" 116 | state.lastCommandSent = "installed()" 117 | state.lastCommandTime = nowFormatted() 118 | state.driverDate = getDriverDate() 119 | state.model = parent.getDataValue('model') + "-Light" 120 | log.info "${device.displayName} Driver Date $state.driverDate" 121 | log.info "${device.displayName} Model=$state.model" 122 | return 123 | } 124 | 125 | def off() { 126 | if (logEnable) log.info "${device.displayName} off()" 127 | state.lastCommandSent = "off()" 128 | state.lastCommandTime = nowFormatted() 129 | def cmds = [] 130 | //cmds += zigbee.off() 131 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 0 {}" 132 | if (logEnable) log.debug "${device.displayName} off $cmds" 133 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 134 | sendEvent(name:"switch", value: "off") 135 | } 136 | 137 | def on() { 138 | if (logEnable) log.info "${device.displayName} on()" 139 | state.lastCommandSent = "on()" 140 | state.lastCommandTime = nowFormatted() 141 | def cmds = [] 142 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 6 1 {}" 143 | if (logEnable) log.debug "${device.displayName} on $cmds" 144 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 145 | sendEvent(name:"switch", value: "on") 146 | } 147 | 148 | def nowFormatted() { 149 | if(location.timeZone) return new Date().format("yyyy-MMM-dd h:mm:ss a", location.timeZone) 150 | else return new Date().format("yyyy MMM dd EEE h:mm:ss a") 151 | } 152 | 153 | def setLevel(level, duration=0xFFFF) { 154 | level = level?.toInteger() 155 | duration = duration?.toInteger() 156 | if (duration==null) duration=0xFFFF 157 | if (logEnable) log.info "${device.displayName} setLevel($level" + (duration==0xFFFF?")":", ${duration}s)") 158 | state.lastCommandSent = "setLevel($level" + (duration==0xFFFF?")":", ${duration}s)") 159 | state.lastCommandTime = nowFormatted() 160 | if (duration!=0xFFFF) duration = duration*10 //firmware duration in 10ths 161 | durationHex = zigbee.convertToHexString(duration,4) 162 | durationHexReverse = durationHex.substring(2,4)+durationHex.substring(0,2) 163 | def cmds = [] 164 | cmds += "he cmd 0x${parent.deviceNetworkId} 0x${device.deviceNetworkId?.substring(device.deviceNetworkId.length()-2)?:"00"} 0x0008 0x04 {${zigbee.convertToHexString(convertPercentToByte(level),2)} $durationHexReverse}" 165 | if (logEnable) log.debug "${device.displayName} setLevel $cmds" 166 | sendHubCommand(new HubMultiAction(delayBetween(cmds, shortDelay), Protocol.ZIGBEE)) 167 | sendEvent(name:"level", value: "${level}") 168 | sendEvent(name:"switch", value: "on") 169 | } 170 | 171 | def toggle() { 172 | def toggleDirection = device.currentValue("switch")=="off"?"off->on":"on->off" 173 | if (logEnable) log.info "${device.displayName} toggle(${toggleDirection})" 174 | state.lastCommandSent = "toggle(${toggleDirection})" 175 | state.lastCommandTime = nowFormatted() 176 | if(device.currentValue("switch")=="off") { 177 | on() 178 | } else { 179 | off() 180 | } 181 | } 182 | 183 | def updated(option) { // called when "Save Preferences" is requested 184 | // nothing 185 | } 186 | 187 | def parse(String description) { 188 | Map descMap = zigbee.parseDescriptionAsMap(description) 189 | if (logEnable) { 190 | log.trace "${device.displayName} parse($descMap)" 191 | try { 192 | if (zigbee.getEvent(description)!=[:]) log.debug "${device.displayName} zigbee.getEvent ${zigbee.getEvent(description)}" 193 | } catch (e) { 194 | if (logEnable) log.debug "${device.displayName} "+"There was an error while calling zigbee.getEvent: $description" 195 | } 196 | } 197 | } 198 | 199 | def convertPercentToByte(int value=0) { //convert a 0-100 range where 100%=254. 255 is reserved for special meaning. 200 | value = value==null?0:value //default to 0 if null 201 | value = Math.min(Math.max(value.toInteger(),0),101) //make sure input percent value is in the 0-101 range 202 | value = Math.floor(value/100*255) //convert to 0-255 where 100%=254 and 101 becomes 255 for special meaning 203 | value = value==255?254:value //this ensures that 100% rounds down to byte value 254 204 | value = value>255?255:value //this ensures that 101% rounds down to byte value 255 205 | return value 206 | } 207 | -------------------------------------------------------------------------------- /Drivers/Inovelli_VZM36_Zigbee_Canopy/packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "BASIC Inovelli VZM36 Driver Package", 3 | "minimumHEVersion": "1.0.0", 4 | "author": "BPTWorld", 5 | "dateReleased": "2024-03-27", 6 | "documentationLink": "https://github.com/bptworld/Hubitat", 7 | "communityLink": "", 8 | "releaseNotes": "Updated fingerprints", 9 | "apps": [ 10 | ], 11 | "drivers": [ 12 | { 13 | "id" : "a5bb0aa4-f484-4382-a2c8-e2eded421429", 14 | "name": "Basic Canopy Driver", 15 | "namespace": "BPTWorld", 16 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Canopy-Driver.groovy", 17 | "required": true, 18 | "oauth": false, 19 | "version": "1.0.1" 20 | }, 21 | { 22 | "id" : "be5e8579-48c5-4f33-9945-d1e8d1074ae7", 23 | "name": "Basic Light Driver", 24 | "namespace": "BPTWorld", 25 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Light-Driver.groovy", 26 | "required": true, 27 | "oauth": false, 28 | "version": "1.0.1" 29 | }, 30 | { 31 | "id" : "c3a04760-af26-435b-aef3-bfc12c7ab02a", 32 | "name": "Basic Fan Driver", 33 | "namespace": "BPTWorld", 34 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Inovelli_VZM36_Zigbee_Canopy/VZM36-Fan-Driver.groovy", 35 | "required": true, 36 | "oauth": false, 37 | "version": "1.0.0" 38 | } 39 | ], 40 | "libraries": [ 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Drivers/Marine Weather/README.md: -------------------------------------------------------------------------------- 1 | # Marine Weather 2 | Design Usage:
3 | This driver formats the Marine Weather - Storm Glass data to be used with Hubitat.

4 | Please vist my Docs section for Install and other information! 5 |

6 | Thanks,
7 | Bryan
8 | @BPTWorld 9 | -------------------------------------------------------------------------------- /Drivers/Pollen Forecaster/PF-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Pollen Forecaster 3 | * 4 | * Design Usage: 5 | * Retrieve data from Pollen.com. For use with Hubitat dashboards. 6 | * 7 | * Copyright 2019 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to give a shout out on the Hubitat forums to let 10 | * people know that it exists! Thanks. 11 | * 12 | * Remember...I am not a programmer, everything I do takes a lot of time and research (then MORE research)! 13 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 14 | * 15 | * Paypal at: https://paypal.me/bptworld 16 | * 17 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 18 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 19 | * the community grow. Have FUN with it! 20 | * 21 | * ------------------------------------------------------------------------------------------------------------------------------ 22 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 23 | * in compliance with the License. You may obtain a copy of the License at: 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 28 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 29 | * for the specific language governing permissions and limitations under the License. 30 | * 31 | * ------------------------------------------------------------------------------------------------------------------------------ 32 | * 33 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 34 | * 35 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 36 | * 37 | * ------------------------------------------------------------------------------------------------------------------------------ 38 | * 39 | * Changes: 40 | * 41 | * v2.0.6 - 01/03/2020 - Adjustment for AW2 42 | * v2.0.5 - 08/29/2019 - App Watchdog compatible 43 | * v2.0.4 - 05/12/2019 - Added Yesterday data by request 44 | * V1.0.3 - 04/16/2019 - Code cleanup, added importUrl 45 | * v2.0.0 - 04/10/2019 - Code cleanup. Added 'Tomorrow forecast', Updated display for Hubitat dashboard tile (@bptworld) 46 | * v1.0.0 - 12/10/2017 - Original ST 'Pollen Virtual Sensor' - Author: jschlackman (james@schlackman.org) 47 | * ST version: https://github.com/jschlackman/PollenThing 48 | * 49 | */ 50 | 51 | import groovy.json.* 52 | import groovy.transform.Field 53 | 54 | def setVersion(){ 55 | appName = "PollenForecaster" 56 | version = "v2.0.6" 57 | dwInfo = "${appName}:${version}" 58 | sendEvent(name: "dwDriverInfo", value: dwInfo, displayed: true) 59 | } 60 | 61 | @SuppressWarnings('unused') 62 | def updateVersion() { 63 | log.info "In updateVersion" 64 | setVersion() 65 | } 66 | 67 | metadata { 68 | definition (name: "Pollen Forecaster", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Pollen%20Forecaster/PF-driver.groovy") { 69 | capability "Actuator" 70 | capability "Sensor" 71 | capability "Polling" 72 | 73 | attribute "indexYesterday", "number" 74 | attribute "categoryYesterday", "string" 75 | attribute "triggersYesterday", "string" 76 | attribute "indexToday", "number" 77 | attribute "categoryToday", "string" 78 | attribute "triggersToday", "string" 79 | attribute "indexTomorrow", "number" 80 | attribute "categoryTomorrow", "string" 81 | attribute "triggersTomorrow", "string" 82 | attribute "location", "string" 83 | 84 | attribute "yesterdayTile", "string" 85 | attribute "todayTile", "string" 86 | attribute "tomorrowTile", "string" 87 | 88 | attribute "dwDriverInfo", "string" 89 | command "updateVersion" 90 | } 91 | 92 | preferences { 93 | input name: "about", type: "paragraph", title: "Pollen Forecaster", description: "Retrieve data from Pollen.com. For use with Hubitat dashboards." 94 | input name: "zipCode", type: "text", title: "Zip Code", required: true, defaultValue: "${location.zipCode}" 95 | input "fontSizeIndex", "text", title: "Font Size-Index", required: true, defaultValue: "70" 96 | input "fontSizeTriggers", "text", title: "Font Size-Triggers", required: true, defaultValue: "60" 97 | input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: false 98 | } 99 | } 100 | 101 | @SuppressWarnings('unused') 102 | def installed() { 103 | poll() 104 | } 105 | 106 | @SuppressWarnings('unused') 107 | def updated() { 108 | unschedule() 109 | updateVersion() 110 | runEvery3Hours(poll) 111 | if((Boolean)settings.logEnable) runIn(3600, "logsOff") 112 | poll() 113 | } 114 | 115 | @SuppressWarnings('unused') 116 | void logsOff() { 117 | device.updateSetting("logEnable",[value:'false',type:"bool"]) 118 | log.debug "Diabling debug logs" 119 | } 120 | 121 | @SuppressWarnings('unused') 122 | def uninstalled() { 123 | unschedule() 124 | } 125 | 126 | void poll() { 127 | if((Boolean)settings.logEnable) log.debug "In poll..." 128 | String pollenZip 129 | 130 | // Use hub zipcode if user has not defined their own 131 | if(zipCode) { 132 | pollenZip = zipCode 133 | } else { 134 | pollenZip = location.zipCode 135 | } 136 | 137 | if((Boolean)settings.logEnable) log.debug "Getting pollen data for ZIP: ${pollenZip}" 138 | 139 | def params = [ 140 | uri: 'https://www.pollen.com/api/forecast/current/pollen/', 141 | path: pollenZip, 142 | headers: [Referer:'https://www.pollen.com'], 143 | timeout: 20 144 | ] 145 | 146 | try { 147 | asynchttpGet('ahandler', params, [:]) 148 | } 149 | catch (e) { 150 | if((Boolean)settings.logEnable) log.debug "Could not retrieve pollen data: $e" 151 | sendEvent(name: "location", value: "Could not retrieve data from API", displayed: true) 152 | } 153 | } 154 | 155 | @SuppressWarnings('unused') 156 | void ahandler(resp, Map edata) { 157 | Integer responseCode = resp.status 158 | if(responseCode == 200 && resp.data) { 159 | Map rData = (Map)new JsonSlurper().parseText((String)resp.data) 160 | rData.Location.periods.each { period -> 161 | if(period.Type == 'Yesterday') { 162 | String catName 163 | Float indexNum = period.Index.toFloat() 164 | 165 | // Set the category according to index thresholds 166 | if (indexNum < 2.5) {catName = "Low"} 167 | else if (indexNum < 4.9) {catName = "Low-Medium"} 168 | else if (indexNum < 7.3) {catName = "Medium"} 169 | else if (indexNum < 9.7) {catName = "Medium-High"} 170 | else if (indexNum < 12) {catName = "High"} 171 | else {catName = "Unknown"} 172 | 173 | // Build the list of allergen triggers 174 | def triggersList = period.Triggers.inject([]) { result, entry -> 175 | result << "${entry.Name}" 176 | }.join(", ") 177 | state.indexYesterday = period.Index 178 | state.categoryYesterday = catName 179 | state.triggersYesterday = triggersList 180 | 181 | sendEvent(name: "indexYesterday", value: state.indexYesterday, displayed: true) 182 | sendEvent(name: "categoryYesterday", value: state.categoryYesterday, displayed: true) 183 | sendEvent(name: "triggersYesterday", value: state.triggersYesterday, displayed: true) 184 | } 185 | 186 | if(period.Type == 'Today') { 187 | String catName 188 | Float indexNum = period.Index.toFloat() 189 | 190 | // Set the category according to index thresholds 191 | if (indexNum < 2.5) {catName = "Low"} 192 | else if (indexNum < 4.9) {catName = "Low-Medium"} 193 | else if (indexNum < 7.3) {catName = "Medium"} 194 | else if (indexNum < 9.7) {catName = "Medium-High"} 195 | else if (indexNum < 12) {catName = "High"} 196 | else {catName = "Unknown"} 197 | 198 | // Build the list of allergen triggers 199 | def triggersList = period.Triggers.inject([]) { result, entry -> 200 | result << "${entry.Name}" 201 | }.join(", ") 202 | state.indexToday = period.Index 203 | state.categoryToday = catName 204 | state.triggersToday = triggersList 205 | 206 | sendEvent(name: "indexToday", value: state.indexToday, displayed: true) 207 | sendEvent(name: "categoryToday", value: state.categoryToday, displayed: true) 208 | sendEvent(name: "triggersToday", value: state.triggersToday, displayed: true) 209 | } 210 | 211 | if(period.Type == 'Tomorrow') { 212 | String catName 213 | Float indexNum = period.Index.toFloat() 214 | 215 | // Set the category according to index thresholds 216 | if (indexNum < 2.5) {catName = "Low"} 217 | else if (indexNum < 4.9) {catName = "Low-Medium"} 218 | else if (indexNum < 7.3) {catName = "Medium"} 219 | else if (indexNum < 9.7) {catName = "Medium-High"} 220 | else if (indexNum < 12) {catName = "High"} 221 | else {catName = "Unknown"} 222 | 223 | // Build the list of allergen triggers 224 | def triggersList = period.Triggers.inject([]) { result, entry -> 225 | result << "${entry.Name}" 226 | }.join(", ") 227 | state.indexTomorrow = period.Index 228 | state.categoryTomorrow = catName 229 | state.triggersTomorrow = triggersList 230 | 231 | sendEvent(name: "indexTomorrow", value: state.indexTomorrow, displayed: true) 232 | sendEvent(name: "categoryTomorrow", value: state.categoryTomorrow, displayed: true) 233 | sendEvent(name: "triggersTomorrow", value: state.triggersTomorrow, displayed: true) 234 | } 235 | state.location = rData.Location.DisplayLocation 236 | sendEvent(name: "location", value: state.location, displayed: true) 237 | } 238 | } else { 239 | if((Boolean)settings.logEnable) log.debug "Bad Response Could not retrieve pollen data: $e" 240 | sendEvent(name: "location", value: "Could not retrieve data from API", displayed: true) 241 | } 242 | yesterdayTileMap() 243 | todayTileMap() 244 | tomorrowTileMap() 245 | } 246 | /* } 247 | catch (SocketTimeoutException e) { 248 | if(logEnable) log.debug "Connection to Pollen.com API timed out." 249 | sendEvent(name: "location", value: "Connection timed out while retrieving data from API", displayed: true) 250 | } 251 | catch (e) { 252 | if(logEnable) log.debug "Could not retrieve pollen data: $e" 253 | sendEvent(name: "location", value: "Could not retrieve data from API", displayed: true) 254 | } 255 | yesterdayTileMap() 256 | todayTileMap() 257 | tomorrowTileMap() 258 | }*/ 259 | 260 | @SuppressWarnings('unused') 261 | def configure() { 262 | poll() 263 | } 264 | 265 | void yesterdayTileMap() { 266 | if((Boolean)settings.logEnable) log.debug "In yesterdayTileMap..." 267 | state.appDataYesterday = "" 268 | state.appDataYesterday+= "" 269 | state.appDataYesterday+= "" 270 | state.appDataYesterday+= "" 271 | state.appDataYesterday+= "
Pollen Forecast Today
${state.location}
${state.indexYesterday} - ${state.categoryYesterday}
${state.triggersYesterday}
" 272 | sendEvent(name: "yesterdayTile", value: state.appDataYesterday, displayed: true) 273 | } 274 | 275 | void todayTileMap() { 276 | if((Boolean)settings.logEnable) log.debug "In todayTileMap..." 277 | state.appDataToday = "" 278 | state.appDataToday+= "" 279 | state.appDataToday+= "" 280 | state.appDataToday+= "" 281 | state.appDataToday+= "
Pollen Forecast Today
${state.location}
${state.indexToday} - ${state.categoryToday}
${state.triggersToday}
" 282 | sendEvent(name: "todayTile", value: state.appDataToday, displayed: true) 283 | } 284 | 285 | void tomorrowTileMap() { 286 | if((Boolean)settings.logEnable) log.debug "In tomorrowTileMap..." 287 | state.appDataTomorrow = "" 288 | state.appDataTomorrow+= "" 289 | state.appDataTomorrow+= "" 290 | state.appDataTomorrow+= "" 291 | state.appDataTomorrow+= "
Pollen Forecast Tomorrow
${state.location}
${state.indexTomorrow} - ${state.categoryTomorrow}
${state.triggersTomorrow}
" 292 | sendEvent(name: "tomorrowTile", value: state.appDataTomorrow, displayed: true) 293 | } 294 | -------------------------------------------------------------------------------- /Drivers/Pollen Forecaster/README.md: -------------------------------------------------------------------------------- 1 | # Pollen Forecaster 2 | Design Usage:
3 | Retrieve data from pollen.com. For use with Hubitat dashboards.

4 | Please vist my Docs section for Install and other information! 5 |

6 | Thanks,
7 | Bryan
8 | @BPTWorld 9 | -------------------------------------------------------------------------------- /Drivers/Send to Hub with CATT/README.md: -------------------------------------------------------------------------------- 1 | # Send to Hub with CATT Driver 2 | Design Usage:
3 | This driver is designed to send the HE dashboard (and MORE) to a Nest Hub using CATT.

4 | Please vist my Docs section for Install and other information! 5 |

6 | Thanks,
7 | Bryan
8 | @BPTWorld 9 | -------------------------------------------------------------------------------- /Drivers/Send to Hub with CATT/STHWC-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** Send to Hub with CATT Driver **************** 3 | * 4 | * Design Usage: 5 | * This driver is designed to send the HE dashboard (and MORE) to a Nest Hub using CATT. 6 | * 7 | * Copyright 2019 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to give a shout out on the Hubitat forums to let 10 | * people know that it exists! Thanks. 11 | * 12 | * Remember...I am not a programmer, everything I do takes a lot of time and research (then MORE research)! 13 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 14 | * 15 | * Paypal at: https://paypal.me/bptworld 16 | * 17 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 18 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 19 | * the community grow. Have FUN with it! 20 | * 21 | ------------------------------------------------------------------------------------------------------------------------------ 22 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 23 | * in compliance with the License. You may obtain a copy of the License at: 24 | * 25 | * http://www.apache.org/licenses/LICENSE-2.0 26 | * 27 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 28 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 29 | * for the specific language governing permissions and limitations under the License. 30 | * 31 | * ------------------------------------------------------------------------------------------------------------------------------ 32 | * 33 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 34 | * 35 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 36 | * 37 | * ------------------------------------------------------------------------------------------------------------------------------ 38 | * 39 | * Changes: 40 | * 41 | * v1.0.4 - 01/03/20 - Adjustment for AW2 42 | * V1.0.3 - 08/29/19 - App Watchdog compatible 43 | * V1.0.2 - 08/17/19 - Added more commands, fixed typo in volume 44 | * V1.0.1 - 08/16/19 - Name changed to 'Send to Hub with CATT', added a ton more commands, added some suggestions from @Ryan780, Thank you! 45 | * V1.0.0 - 08/15/19 - Initial release 46 | */ 47 | 48 | def setVersion(){ 49 | appName = "SendtoHubwithCATTDriver" 50 | version = "v1.0.4" 51 | dwInfo = "${appName}:${version}" 52 | sendEvent(name: "dwDriverInfo", value: dwInfo, displayed: true) 53 | } 54 | 55 | def updateVersion() { 56 | log.info "In updateVersion" 57 | setVersion() 58 | } 59 | 60 | metadata { 61 | definition (name: "Send to Hub with CATT Driver", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/Send%20to%20Hub%20with%20CATT/STHWC-driver.groovy") { 62 | capability "Actuator" 63 | capability "Initialize" 64 | capability "Telnet" 65 | capability "Switch" 66 | capability "Speech Synthesis" 67 | 68 | attribute "telnet", "string" 69 | attribute "switch", "string" 70 | 71 | command "add", ["URI"] 72 | command "cast", ["URI"] 73 | command "castDashboard", ["URI"] 74 | command "clear" 75 | command "ffwd", ["Number"] 76 | command "info" 77 | command "pause" 78 | command "play" 79 | command "remove", ["URI"] 80 | command "restore" 81 | command "rewind", ["Number"] 82 | command "save" 83 | command "skip" 84 | command "status" 85 | command "stop" 86 | command "volume", ["Number"] 87 | command "volumedown" 88 | command "volumeup" 89 | command "write_config", ["Text"] 90 | 91 | attribute "dwDriverInfo", "string" 92 | command "updateVersion" 93 | } 94 | 95 | preferences() { 96 | section(){ 97 | input "ipaddress", "text", required: true, title: "Catt Server IP Address", defaultValue: "0.0.0.0" 98 | input "userName", "text", required: true, title: "Catt Server Username" 99 | input "userPass", "password", required: true, title: "Catt Server Password" 100 | input "gDevice", "text", required: true, title: "Exact name of the Nest Hub to use" 101 | input "castWebsite", "text", required: true, title: "Default Website - Enter the exact webite URL including http://" 102 | input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: false 103 | } 104 | } 105 | } 106 | 107 | def sendCommand(theCommand){ 108 | state.lastmsg = theCommand 109 | if(logEnable) log.debug "Sending msg: ${theCommand}" 110 | return new hubitat.device.HubAction("${theCommand}\n", hubitat.device.Protocol.TELNET) 111 | } 112 | 113 | def resend(){ 114 | if(logEnable) log.debug "RESEND!" 115 | sendCommand(state.lastmsg) 116 | } 117 | 118 | def initialize(){ 119 | try { 120 | if(logEnable) log.debug "Opening telnet connection" 121 | sendEvent([name: "telnet", value: "Opening"]) 122 | telnetConnect([terminalType: 'VT100'], "${ipaddress}", 23, "${userName}", "${userPass}") 123 | //give it a chance to start 124 | pauseExecution(1000) 125 | if(logEnable) log.debug "Telnet connection established" 126 | } catch(e) { 127 | if(logEnable) log.debug "Initialize Error: ${e.message}" 128 | } 129 | } 130 | 131 | def installed() { 132 | initialize() 133 | } 134 | 135 | def updated() { 136 | initialize() 137 | } 138 | 139 | def parse(String msg) { 140 | if(logEnable) log.debug "parse ${msg}" 141 | sendEvent([name: "telnet", value: "Connected"]) 142 | if (msg == "busyIR,1:1,1"){ 143 | return new hubitat.device.HubAction("${state.lastmsg}\n", hubitat.device.Protocol.TELNET) 144 | } 145 | } 146 | 147 | def telnetStatus(String status) { 148 | if(logEnable) log.debug "telnetStatus: ${status}" 149 | if (status == "receive error: Stream is closed" || status == "send error: Broken pipe (Write failed)") { 150 | log.error("Telnet connection dropped...PLEASE OPEN THIS DEVICE IN HE AND PRESS THE 'INITIALIZE' BUTTON") 151 | sendEvent([name: "telnet", value: "Disconnected"]) 152 | telnetClose() 153 | runIn(60, initialize) 154 | } 155 | } 156 | 157 | def on(msg) { 158 | if(msg){ 159 | theMsg = msg 160 | } else { 161 | theMsg = castWebsite 162 | } 163 | def msgAction = "catt -d '${gDevice}' cast_site '${theMsg}'" 164 | sendEvent(name: "switch", value: "on") 165 | sendCommand(msgAction) 166 | } 167 | 168 | def castDashboard(dashBoard){ 169 | def msgAction = "catt -d '${gDevice}' cast_site '$dashBoard'" 170 | sendEvent(name: "switch", value: "on") 171 | sendCommand(msgAction) 172 | } 173 | 174 | def off() { 175 | def msgAction = "catt -d '${gDevice}' stop" 176 | sendEvent(name: "switch", value: "off") 177 | sendCommand(msgAction) 178 | } 179 | 180 | def add(msg) { 181 | def msgAction = "catt -d '${gDevice}' add '${msg}'" 182 | sendCommand(msgAction) 183 | } 184 | 185 | def cast(msg) { 186 | def msgAction = "catt -d '${gDevice}' cast '${msg}'" 187 | sendEvent(name: "switch", value: "on") 188 | sendCommand(msgAction) 189 | } 190 | 191 | def ffwd(msg) { 192 | def msgAction = "catt -d '${gDevice}' ffwd '${msg}'" 193 | sendCommand(msgAction) 194 | } 195 | 196 | def info() { 197 | def msgAction = "catt -d '${gDevice}' info" 198 | sendCommand(msgAction) 199 | } 200 | 201 | def pause() { 202 | def msgAction = "catt -d '${gDevice}' pause" 203 | sendCommand(msgAction) 204 | } 205 | 206 | def play() { 207 | def msgAction = "catt -d '${gDevice}' play" 208 | sendCommand(msgAction) 209 | } 210 | 211 | def clear() { 212 | def msgAction = "catt -d '${gDevice}' clear" 213 | sendCommand(msgAction) 214 | } 215 | 216 | def remove(msg) { 217 | def msgAction = "catt -d '${gDevice}' remove '${msg}'" 218 | sendCommand(msgAction) 219 | } 220 | 221 | def restore() { 222 | def msgAction = "catt -d '${gDevice}' restore" 223 | sendCommand(msgAction) 224 | } 225 | 226 | def rewind(msg) { 227 | def msgAction = "catt -d '${gDevice}' rewind '${msg}'" 228 | sendCommand(msgAction) 229 | } 230 | 231 | def save() { 232 | def msgAction = "catt -d '${gDevice}' save" 233 | sendCommand(msgAction) 234 | } 235 | 236 | def skip(msg) { 237 | def msgAction = "catt -d '${gDevice}' skip" 238 | sendCommand(msgAction) 239 | } 240 | 241 | def status() { 242 | def msgAction = "catt -d '${gDevice}' status" 243 | sendCommand(msgAction) 244 | } 245 | 246 | def stop() { 247 | def msgAction = "catt -d '${gDevice}' stop" 248 | sendCommand(msgAction) 249 | } 250 | 251 | def volume(msgVolume) { 252 | def msgAction = "catt -d '${gDevice}' volume ${msgVolume}" 253 | sendCommand(msgAction) 254 | } 255 | 256 | def volumeDown() { 257 | def msgAction = "catt -d '${gDevice}' volumedown 10" 258 | sendCommand(msgAction) 259 | } 260 | 261 | def volumeUp() { 262 | def msgAction = "catt -d '${gDevice}' volumeup 10" 263 | sendCommand(msgAction) 264 | } 265 | 266 | def write_config(setDevice) { 267 | def msgAction = "catt write_config ${setDevice}" 268 | sendCommand(msgAction) 269 | } 270 | -------------------------------------------------------------------------------- /Drivers/gCalendar/README.md: -------------------------------------------------------------------------------- 1 | # gCalendar Driver 2 | Design Usage:
3 | Retrieves a Google Calendar to be used with HE Dashboards.

4 | Please vist my Docs section for Install and other information! 5 |

6 | Thanks,
7 | Bryan
8 | @BPTWorld 9 | -------------------------------------------------------------------------------- /Drivers/gCalendar/gc-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** gCalendar Driver **************** 3 | * 4 | * Design Usage: 5 | * Retrieves a Google Calendar to be used with HE Dashboards. 6 | * 7 | * Copyright 2021 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to mention it on the Hubitat forums! Thanks. 10 | * 11 | * Remember...I am not a professional programmer, everything I do takes a lot of time and research (then MORE research)! 12 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 13 | * 14 | * Paypal at: https://paypal.me/bptworld 15 | * 16 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 17 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 18 | * the community grow. Have FUN with it! 19 | * 20 | * ------------------------------------------------------------------------------------------------------------------------------ 21 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 22 | * in compliance with the License. You may obtain a copy of the License at: 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 27 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 28 | * for the specific language governing permissions and limitations under the License. 29 | * 30 | * ------------------------------------------------------------------------------------------------------------------------------ 31 | * 32 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 33 | * 34 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 35 | * 36 | * ------------------------------------------------------------------------------------------------------------------------------ 37 | * 38 | * Original concept by @TechMedX 39 | * 40 | * Changes: 41 | * 42 | * 1.0.2 - 09/05/21 - Added urlSize Attribute, added additional language for 255 character limit. 43 | * 1.0.1 - 03/01/21 - Fixed a typo 44 | * 1.0.0 - 02/28/21 - Initial release 45 | */ 46 | 47 | metadata { 48 | definition (name: "gCalendar Driver", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "") { 49 | capability "Actuator" 50 | attribute "bpt-gCal", "text" 51 | attribute "lastUpdated", "text" 52 | attribute "urlSize", "text" 53 | command "refresh" 54 | } 55 | } 56 | 57 | preferences { 58 | input title:"Google Calendar Tile", description:"Note: Calendar will be updated once every hour or when 'Refresh' button is pushed.

Setup:
1) Go to your Google Calendar
2) For the calendar you want to display, click Settings
3) Scroll down until you see the Embed Code
4) Copy that code and paste it into URL field here
5) Press 'Save Preferences'", type:"paragraph", element:"paragraph" 59 | input "gCal", "text", title: "Google Calendar URL (URL must be less than 256 characters or it won't work. See urlSize in Attributes.)", required:true, submitOnChange:true 60 | if(gCal) { 61 | theCount = gCal.size() 62 | sendEvent(name: "urlSize", value: theCount) 63 | } 64 | input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: false, submitOnChange: true 65 | input "logOffTime", "enum", title: "Logs Off Time", required:false, multiple:false, options: ["1 Hour", "2 Hours", "3 Hours", "4 Hours", "5 Hours", "Keep On"], defaultValue: "1 Hour" 66 | } 67 | 68 | def refresh() { 69 | if(logEnable) log.debug "In refresh" 70 | if(gCal) { 71 | if(gCal.contains("", "") 72 | if(logEnable) log.debug "In refresh - gCal URl: ${gCal}" 73 | lu = new Date() 74 | theCal = "
" 75 | sendEvent(name: "bpt-gCal", value: theCal) 76 | sendEvent(name: "lastUpdated", value: lu) 77 | } else { 78 | log.info "gCalendar Driver - Be sure to fill in the Google Calendar URL and click Save Preferences" 79 | } 80 | } 81 | 82 | def updated() { 83 | installed() 84 | } 85 | 86 | def installed() { 87 | unschedule() 88 | if(logEnable && logOffTime == "1 Hour") runIn(3600, logsOff, [overwrite:false]) 89 | if(logEnable && logOffTime == "2 Hours") runIn(7200, logsOff, [overwrite:false]) 90 | if(logEnable && logOffTime == "3 Hours") runIn(10800, logsOff, [overwrite:false]) 91 | if(logEnable && logOffTime == "4 Hours") runIn(14400, logsOff, [overwrite:false]) 92 | if(logEnable && logOffTime == "5 Hours") runIn(18000, logsOff, [overwrite:false]) 93 | if(logEnagle && logOffTime == "Keep On") unschedule(logsOff) 94 | refresh() 95 | schedule("0 0 * ? * * *", refresh) 96 | } 97 | 98 | def logsOff() { 99 | log.info "${app.label} - Debug logging auto disabled" 100 | app.updateSetting("logEnable",[value:"false",type:"bool"]) 101 | } 102 | -------------------------------------------------------------------------------- /Drivers/gCalendar/packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "gCalendar Driver", 3 | "minimumHEVersion": "2.1.9", 4 | "author": "BPTWorld", 5 | "dateReleased": "2021-02-28", 6 | "documentationLink": "https://github.com/bptworld/Hubitat", 7 | "communityLink": "", 8 | "releaseNotes": "All release notes can be found within the Community Link and within the App/Driver code.", 9 | "apps" : [ 10 | ], 11 | "drivers" : [ 12 | { 13 | "id": "faf9cf98-d7d3-46fc-ba8a-55756ee7e1d1", 14 | "name": "gCalendar Driver", 15 | "namespace": "BPTWorld", 16 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/gCalendar/gc-driver.groovy", 17 | "required": true, 18 | "version": "1.0.2" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *** BPTWorld apps are no longer being developed or maintained. Thanks *** 2 | 3 | # Apps/Drivers for Hubitat Elevation 4 | Apps for use with the Hubitat Elevation Hub.
5 | Offiical Website: www.hubitat.com
6 | Official Forum: https://community.hubitat.com/
7 |
8 | Donations to support development efforts are accepted via:
9 | Paypal: https://paypal.me/bptworld
Venmo: Bryan-Turcotte-MA (8477)

Remember...I am not a professional programmer, everything I do takes a lot of time and research! Donations for this time and effort are always greatly appreciated.
I 'Thank You' for your support.
Be sure to include your forum username if you would like a personal thank you, which is always nice! 10 |
11 | * Note: Acceptance of donation in no way guarentees support or updates. All apps/drivers are provided 'as is'. While I do my best to maintain/update each app/driver, priorities do change and any app/driver could be depreciated at any time. Thank you for your understanding. 12 |
13 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /apps/FlowEngine/drawflow-css.min.css: -------------------------------------------------------------------------------- 1 | .drawflow,.drawflow .parent-node{position:relative}.parent-drawflow{display:flex;overflow:hidden;touch-action:none;outline:0}.drawflow{width:100%;height:100%;user-select:none}.drawflow .drawflow-node{display:flex;align-items:center;position:absolute;background:#0ff;width:160px;min-height:40px;border-radius:4px;border:2px solid #000;color:#000;z-index:2;padding:15px}.drawflow .drawflow-node.selected{background:red}.drawflow .drawflow-node:hover{cursor:move}.drawflow .drawflow-node .inputs,.drawflow .drawflow-node .outputs{width:0}.drawflow .drawflow-node .drawflow_content_node{width:100%;display:block}.drawflow .drawflow-node .input,.drawflow .drawflow-node .output{position:relative;width:20px;height:20px;background:#fff;border-radius:50%;border:2px solid #000;cursor:crosshair;z-index:1;margin-bottom:5px}.drawflow .drawflow-node .input{left:-27px;top:2px;background:#ff0}.drawflow .drawflow-node .output{right:-3px;top:2px}.drawflow svg{z-index:0;position:absolute;overflow:visible!important}.drawflow .connection{position:absolute;pointer-events:none}.drawflow .connection .main-path{fill:none;stroke-width:5px;stroke:#4682b4;pointer-events:all}.drawflow .connection .main-path:hover{stroke:#1266ab;cursor:pointer}.drawflow .connection .main-path.selected{stroke:#43b993}.drawflow .connection .point{cursor:move;stroke:#000;stroke-width:2;fill:#fff;pointer-events:all}.drawflow .connection .point.selected,.drawflow .connection .point:hover{fill:#1266ab}.drawflow .main-path{fill:none;stroke-width:5px;stroke:#4682b4}.drawflow-delete{position:absolute;display:block;width:30px;height:30px;background:#000;color:#fff;z-index:4;border:2px solid #fff;line-height:30px;font-weight:700;text-align:center;border-radius:50%;font-family:monospace;cursor:pointer}.drawflow>.drawflow-delete{margin-left:-15px;margin-top:15px}.parent-node .drawflow-delete{right:-15px;top:-15px} -------------------------------------------------------------------------------- /apps/FlowEngine/drawflow-extra.css: -------------------------------------------------------------------------------- 1 | button { background: #b3e6b3; border: 1px solid #5cb85c; margin:4px; } 2 | button:active { background: #2d862d; color: white; } 3 | html, body { margin:0; padding:0; height:100%; font-family:sans-serif; } 4 | #controls { padding:10px; background:#eee; display:flex; flex-wrap:wrap; gap:10px; } 5 | #main { display:flex; height:calc(100vh - 100px); } 6 | #drawflow { 7 | width:70%; 8 | background-color: #f4f4f4; 9 | background-image: 10 | linear-gradient(to right, #ddd 1px, transparent 1px), 11 | linear-gradient(to bottom, #ddd 1px, transparent 1px); 12 | background-size: 24px 24px; 13 | } 14 | #editor { width:30%; padding:10px; background:#fff; overflow:auto; border-left:1px solid #ccc; } 15 | #nodeEditor { display:flex; flex-direction:column; gap:10px; } 16 | input, button, select { padding:6px; font-size:14px; border-radius:4px; border:1px solid #ccc; } 17 | .log-info { color: #2d862d; } 18 | .log-error { color: #c0392b; } 19 | .drawflow-node .outputs .output { width: 14px; height: 14px; } 20 | .drawflow-node .outputs .output[data-name="false"] { background: #e67e22; } 21 | .drawflow-node .outputs .output[data-name="true"] { background: #27ae60; } 22 | #wsLogBox { 23 | resize: vertical; 24 | min-height: 50px; 25 | max-height: 300px; 26 | height: 120px; 27 | } 28 | -------------------------------------------------------------------------------- /apps/FlowEngine/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/apps/FlowEngine/favicon.ico -------------------------------------------------------------------------------- /apps/FlowEngine/flowengine-parent.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** Flow Engine Parent **************** 3 | * Design Usage: 4 | * Feel the Flow 5 | * 6 | * Copyright 2025 Bryan Turcotte (@bptworld) 7 | * 8 | * This App is free. If you like and use this app, please be sure to mention it on the Hubitat forums! Thanks. 9 | * 10 | * Remember...I am not a professional programmer, everything I do takes a lot of time and research! 11 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 12 | * 13 | * Paypal at: https://paypal.me/bptworld 14 | *------------------------------------------------------------------------------------------------------------------- 15 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 16 | * in compliance with the License. You may obtain a copy of the License at: 17 | * 18 | * http://www.apache.org/licenses/LICENSE-2.0 19 | * 20 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 21 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 22 | * for the specific language governing permissions and limitations under the License. 23 | * ------------------------------------------------------------------------------------------------------------------------------ 24 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 25 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 26 | * ------------------------------------------------------------------------------------------------------------------------------ 27 | * Changes are listed in child app 28 | */ 29 | 30 | import groovy.json.JsonOutput 31 | import groovy.json.JsonSlurper 32 | import groovy.transform.Field 33 | 34 | definition( 35 | name:"Flow Engine", 36 | namespace: "BPTWorld", 37 | author: "Bryan Turcotte", 38 | description: "Feel the Flow", 39 | category: "Convenience", 40 | iconUrl: "", 41 | iconX2Url: "", 42 | iconX3Url: "", 43 | importUrl: "", 44 | oauth: true 45 | ) 46 | 47 | preferences { 48 | page name: "mainPage", title: "", install: true, uninstall: true 49 | } 50 | 51 | def mainPage() { 52 | dynamicPage(name: "mainPage") { 53 | installCheck() 54 | if(state.appInstalled == 'COMPLETE'){ 55 | display() 56 | 57 | section(getFormat("header-green", " Child Apps")) { 58 | app(name: "anyOpenApp", appName: "Flow Engine Child", namespace: "BPTWorld", title: "Add a new 'Flow Engine' child", multiple: true) 59 | } 60 | 61 | section(getFormat("header-green", " Flow Engine Editor Infomation")) { 62 | paragraph "This app is used to receive flow data from your Flow Engine Editor." 63 | paragraph "Copy and paste this info into the Flow Engine Editor - appId: ${state.appId} - token: ${state.token}" 64 | paragraph "" 65 | paragraph "
Flow Engine Editor
Click to create Flows!
" 66 | } 67 | 68 | section(getFormat("header-green", " Device Master List:")) {} 69 | section(" Master List", hideable: true, hidden: true) { 70 | input "masterDeviceList", "capability.*", title: "Master List of Devices Used in this App - INFO -", required:false, multiple:true, submitOnChange:true 71 | } 72 | 73 | section(getFormat("header-green", " Flow Engine Editor Infomation")) { 74 | input "logEnable", "bool", title: "Enable Debug Options", description: "Log Options", defaultValue:false, submitOnChange:true 75 | if(logEnable) { 76 | input "logOffTime", "enum", title: "Logs Off Time", required:false, multiple:false, options: ["1 Hour", "2 Hours", "3 Hours", "4 Hours", "5 Hours", "Keep On"] 77 | } 78 | } 79 | display2() 80 | } 81 | } 82 | } 83 | 84 | def installed() { 85 | if(logEnable) log.debug "Installed with settings: ${settings}" 86 | initialize() 87 | } 88 | 89 | def updated() { 90 | //if(logEnable) log.debug "Updated with settings: ${settings}" 91 | unschedule() 92 | unsubscribe() 93 | if(logEnable && logOffTime == "1 Hour") runIn(3600, logsOff, [overwrite:false]) 94 | if(logEnable && logOffTime == "2 Hours") runIn(7200, logsOff, [overwrite:false]) 95 | if(logEnable && logOffTime == "3 Hours") runIn(10800, logsOff, [overwrite:false]) 96 | if(logEnable && logOffTime == "4 Hours") runIn(14400, logsOff, [overwrite:false]) 97 | if(logEnable && logOffTime == "5 Hours") runIn(18000, logsOff, [overwrite:false]) 98 | if(logEnable && logOffTime == "Keep On") unschedule(logsOff) 99 | initialize() 100 | } 101 | 102 | def initialize() { 103 | if (!state.accessToken) { 104 | createAccessToken() 105 | state.lastFullUrl = "http://${location.hub.localIP}:80/apps/api/${app.id}/flow?access_token=${state.accessToken}" 106 | state.appId = app.id 107 | state.token = state.accessToken 108 | } 109 | } 110 | 111 | def installCheck(){ 112 | display() 113 | state.appInstalled = app.getInstallationState() 114 | if(state.appInstalled != 'COMPLETE'){ 115 | section{paragraph "Please hit 'Done' to install '${app.label}' parent app "} 116 | } 117 | else{ 118 | if(logEnable) log.info "Parent Installed OK" 119 | } 120 | } 121 | 122 | mappings { 123 | path("/runFlow") { action: [POST: "handleFlow" ] } 124 | path("/listFiles") { action: [GET: "apiListFiles"] } 125 | path("/getFile") { action: [GET: "apiGetFile"] } 126 | path("/testTile") { action: [POST: "testTileHandler"] } 127 | path("/devices") { action: [GET: "apiGetDevices"] } 128 | path("/uploadFile") { action: [POST: "apiUploadFile"] } 129 | } 130 | 131 | def handleFlow() { 132 | try { 133 | def json = request.JSON 134 | flowName = "${json?.flowName}" 135 | if(logEnable) log.debug "Received Flow: ${flowName} - payload: ${json?.drawflow?.Home?.data?.size()} nodes" 136 | 137 | state.lastFlow = json 138 | render contentType: "application/json", data: [status: "ok", nodes: json?.drawflow?.Home?.data?.size()] 139 | saveFlow(flowName, state.lastFlow) 140 | } catch (e) { 141 | log.error "Flow handler error: ${e.message}" 142 | render status: 500, data: [error: "Invalid JSON or server error"] 143 | } 144 | } 145 | 146 | def apiGetDevices() { 147 | // Only allow with token 148 | if (!state.accessToken || params.access_token != state.accessToken) { 149 | render status: 401, text: "Unauthorized" 150 | return 151 | } 152 | def output = [] 153 | masterDeviceList?.each { dev -> 154 | output << [ 155 | id: dev.id, 156 | label: dev.displayName ?: dev.label ?: dev.name, 157 | name: dev.name, 158 | attributes: dev.supportedAttributes?.collect { attr -> 159 | [ 160 | name: attr.name, 161 | currentValue: dev.currentValue(attr.name) 162 | ] 163 | } ?: [], 164 | commands: dev.supportedCommands?.collect { it.name } ?: [] 165 | ] 166 | } 167 | render contentType: "application/json", data: groovy.json.JsonOutput.toJson(output) 168 | } 169 | 170 | void saveFlow(fName, fData) { 171 | if(logEnable) log.debug "Saving to file - ${fName}" 172 | String listJson = JsonOutput.toJson(fData) as String 173 | uploadHubFile("${fName}.json",listJson.getBytes()) 174 | } 175 | 176 | def apiGetFile() { 177 | log.debug "In apiGetFile" 178 | def name = params.name 179 | if (!name) { 180 | log.debug "In apiGetFile - Missing file name" 181 | render status: 400, text: "Missing file name" 182 | return 183 | } 184 | def fileData = null 185 | try { 186 | def url = "http://${location.hub.localIP}:8080/local/${name}" 187 | log.debug "Fetching file via httpGet: ${url}" 188 | httpGet([uri: url, contentType: 'text/plain']) { resp -> 189 | fileData = resp.data?.text 190 | } 191 | if (!fileData) { 192 | log.debug "In apiGetFile - File not found or empty" 193 | render status: 404, text: "File not found or empty" 194 | return 195 | } 196 | try { 197 | def obj = new groovy.json.JsonSlurper().parseText(fileData) 198 | render contentType: "application/json", data: groovy.json.JsonOutput.toJson(obj) 199 | } catch (ex) { 200 | log.debug "In apiGetFile - returning as text" 201 | render contentType: "text/plain", text: fileData 202 | } 203 | } catch (e) { 204 | log.debug "In apiGetFile - 500 error: ${e}" 205 | render status: 500, text: "Error: ${e}" 206 | } 207 | } 208 | 209 | 210 | 211 | def apiListFiles() { 212 | if(logEnable) log.debug "Getting list of files" 213 | uri = "http://${location.hub.localIP}:8080/hub/fileManager/json"; 214 | def params = [ 215 | uri: uri, 216 | headers: [ 217 | "Cookie": cookie 218 | ] 219 | ] 220 | try { 221 | fileList = [] 222 | httpGet(params) { resp -> 223 | if (resp != null){ 224 | log.debug "Found the files" 225 | def json = resp.data 226 | for (rec in json.files) { 227 | if (rec.name?.toLowerCase()?.endsWith(".json")) { 228 | fileList << rec.name 229 | } 230 | } 231 | } else { 232 | // 233 | } 234 | } 235 | if(logEnable) log.debug fileList.sort() 236 | } catch (e) { 237 | log.error e 238 | } 239 | render contentType: "application/json", data: groovy.json.JsonOutput.toJson([files: fileList]) 240 | } 241 | 242 | def getDeviceById(id) { 243 | if(logDebug) log.debug "In getDeviceById - ${id}" 244 | theDevice = settings.masterDeviceList?.find { it.id.toString() == id?.toString() } 245 | if(logDebug) log.debug "In getDeviceById - Returning: ${theDevice.deviceLabel}" 246 | return theDevice 247 | } 248 | 249 | def testTileHandler() { 250 | try { 251 | def slurper = new groovy.json.JsonSlurper() 252 | def req = request.JSON ?: slurper.parseText(request?.body ?: "{}") 253 | def node = req.node 254 | if (logEnable) log.info "In testTileHandler: Received node: ${node?.name}, id=${node?.id}" 255 | // Minimal emulation: process node as if running in a flow 256 | switch (node?.name) { 257 | case "device": 258 | def devIds = [] 259 | if (node.data.deviceIds instanceof List) { 260 | devIds = node.data.deviceIds 261 | } else if (node.data.deviceIds) { 262 | devIds = [node.data.deviceIds] 263 | } else if (node.data.deviceId) { 264 | devIds = [node.data.deviceId] 265 | } 266 | def cmd = node.data.command 267 | def val = node.data.value 268 | def output = [] 269 | devIds.each { devId -> 270 | def device = getDeviceById(devId) 271 | if (device && cmd) { 272 | if (cmd == "setColor" && node.data.color) { 273 | // Convert hex to HSV or at least something the driver can use 274 | def color = node.data.color 275 | def rgb = color?.startsWith("#") ? color.substring(1) : color 276 | if (rgb.size() == 6) { 277 | def r = Integer.parseInt(rgb.substring(0,2),16) / 255.0 278 | def g = Integer.parseInt(rgb.substring(2,4),16) / 255.0 279 | def b = Integer.parseInt(rgb.substring(4,6),16) / 255.0 280 | 281 | def max = [r, g, b].max() 282 | def min = [r, g, b].min() 283 | def h, s, v 284 | v = max 285 | def d = max - min 286 | s = max == 0 ? 0 : d / max 287 | if (max == min) { 288 | h = 0 // achromatic 289 | } else if (max == r) { 290 | h = (g - b) / d + (g < b ? 6 : 0) 291 | } else if (max == g) { 292 | h = (b - r) / d + 2 293 | } else if (max == b) { 294 | h = (r - g) / d + 4 295 | } 296 | h = h / 6 297 | 298 | def hue = (h * 100).toInteger() 299 | def sat = (s * 100).toInteger() 300 | def lev = (v * 100).toInteger() 301 | def colorMap = [hue: hue, saturation: sat, level: lev] 302 | device.setColor(colorMap) 303 | output << "Executed setColor on ${device.displayName} with ${colorMap}" 304 | } else { 305 | output << "Invalid color format: ${color}" 306 | } 307 | } else if (val != null && val != "") { 308 | def arg = val 309 | if (val.isInteger()) arg = val.toInteger() 310 | else if (val.isDouble()) arg = val.toDouble() 311 | device."${cmd}"(arg) 312 | } else { 313 | device."${cmd}"() 314 | } 315 | output << "Executed ${cmd} on ${device.displayName} ${val ? "with value $val" : ""}" 316 | } 317 | } 318 | render contentType: "text/plain", data: output ? output.join("; ") : "No device command executed." 319 | return 320 | case "condition": 321 | def device = getDeviceById(node.data.deviceId) 322 | if (!device) { 323 | render contentType: "text/plain", data: "Device not found" 324 | return 325 | } 326 | def attrVal = device.currentValue(node.data.attribute) 327 | def passes = evaluateComparator(attrVal, node.data.value, node.data.comparator) 328 | render contentType: "text/plain", data: "Condition result: ${passes} (current ${node.data.attribute}: ${attrVal})" 329 | return 330 | case "eventTrigger": 331 | render contentType: "text/plain", data: "Test not implemented for eventTrigger (needs event context)." 332 | return 333 | case "notification": 334 | def ids = node.data.targetDeviceId instanceof List ? node.data.targetDeviceId : [node.data.targetDeviceId] 335 | def msg = node.data.message ?: "Test Notification" 336 | ids.each { devId -> 337 | def dev = masterDeviceList?.find { it.id == devId } 338 | if (dev && node.data.notificationType == "push" && dev.hasCommand("deviceNotification")) { 339 | dev.deviceNotification(msg) 340 | } 341 | if (dev && node.data.notificationType == "speech" && dev.hasCommand("speak")) { 342 | dev.speak(msg) 343 | } 344 | } 345 | render contentType: "text/plain", data: "Notification sent to device(s)" 346 | return 347 | 348 | // Add other node types if you want 349 | default: 350 | render contentType: "text/plain", data: "Node type ${node?.name} not supported for test." 351 | } 352 | } catch (ex) { 353 | log.error "TestTileHandler error: $ex" 354 | render contentType: "text/plain", data: "TestTileHandler error: $ex" 355 | } 356 | } 357 | 358 | def apiUploadFile() { 359 | // Require token 360 | if (!state.accessToken || params.access_token != state.accessToken) { 361 | render status: 401, text: "Unauthorized" 362 | return 363 | } 364 | def name = params.name 365 | if (!name) { 366 | render status: 400, text: "Missing file name" 367 | return 368 | } 369 | def body = request?.body ?: request?.JSON 370 | if (!body) { 371 | render status: 400, text: "Missing file data" 372 | return 373 | } 374 | try { 375 | // Handle both raw body and JSON (most browsers send JSON) 376 | def fileText = (body instanceof String) ? body : groovy.json.JsonOutput.toJson(body) 377 | uploadHubFile(name, fileText.getBytes("UTF-8")) 378 | render contentType: "application/json", data: groovy.json.JsonOutput.toJson([ok: true]) 379 | } catch (e) { 380 | log.error "apiUploadFile error: $e" 381 | render status: 500, text: "Error: $e" 382 | } 383 | } 384 | 385 | def getFormat(type, myText=null, page=null) { 386 | if(type == "header-green") return "
${myText}
" 387 | if(type == "line") return "
" 388 | } 389 | 390 | def display() { 391 | section() { 392 | paragraph getFormat("line") 393 | label title: "Enter a name for this automation", required:true, submitOnChange:true 394 | } 395 | } 396 | 397 | def display2() { 398 | section() { 399 | paragraph getFormat("line") 400 | paragraph "
BPTWorld
Donations are never necessary but always appreciated!
" 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /apps/FlowEngine/flowvars.js: -------------------------------------------------------------------------------- 1 | // flowvars.js - Full Advanced Variable/Expression Engine for FlowEngine 2 | 3 | // --- Hubitat File Manager helpers for variables --- 4 | async function uploadVarsToHubitat(varsArr, fileName) { 5 | const appId = document.getElementById("hubitatAppId")?.value.trim(); 6 | const token = document.getElementById("hubitatToken")?.value.trim(); 7 | if (!appId || !token) { alert("Missing Hubitat appId/token"); return false; } 8 | const url = `/apps/api/${appId}/uploadFile?access_token=${token}&name=${encodeURIComponent(fileName)}`; 9 | const res = await fetch(url, { 10 | method: "POST", 11 | body: JSON.stringify(varsArr), 12 | headers: { "Content-Type": "application/json" } 13 | }); 14 | if (res.ok) return true; 15 | alert("Failed to upload vars to Hubitat: " + (await res.text())); 16 | return false; 17 | } 18 | async function fetchHubitatVarFiles() { 19 | const appId = document.getElementById("hubitatAppId")?.value.trim(); 20 | const token = document.getElementById("hubitatToken")?.value.trim(); 21 | if (!appId || !token) { alert("Missing Hubitat appId/token"); return []; } 22 | const url = `/apps/api/${appId}/listFiles?access_token=${token}`; 23 | const res = await fetch(url); 24 | if (!res.ok) { alert("Failed to list files on Hubitat: " + (await res.text())); return []; } 25 | const arr = await res.json(); 26 | // PATCH: handle both plain array and {files:[...]} 27 | const fileArr = Array.isArray(arr) ? arr : arr.files; 28 | if (!fileArr) return []; 29 | return fileArr.filter(x => x.endsWith('.json')); 30 | } 31 | 32 | async function fetchHubitatVarFileContent(fileName) { 33 | const appId = document.getElementById("hubitatAppId")?.value.trim(); 34 | const token = document.getElementById("hubitatToken")?.value.trim(); 35 | if (!appId || !token) { alert("Missing Hubitat appId/token"); return null; } 36 | const url = `/apps/api/${appId}/getFile?access_token=${token}&name=${encodeURIComponent(fileName)}`; 37 | const res = await fetch(url); 38 | if (!res.ok) { alert("Failed to get file: " + (await res.text())); return null; } 39 | return await res.text(); 40 | } 41 | 42 | (function() { 43 | // Expose to global scope 44 | const root = window.flowVars = {}; 45 | 46 | // --- STATE --- 47 | let flowVars = []; 48 | let globalVars = []; 49 | let managerEl = null; // Root UI element 50 | let ctx = {}; // Current eval context (all vars) 51 | let listeners = []; // For external change notifications 52 | 53 | // --- TYPE & UTILS --- 54 | function parseType(val) { 55 | if (typeof val === 'number') return "Number"; 56 | if (typeof val === 'boolean') return "Boolean"; 57 | if (typeof val === 'object') return "Object"; 58 | if (typeof val !== 'string') return "String"; 59 | if (/^\d+(\.\d+)?$/.test(val)) return "Number"; 60 | if (/^(true|false)$/i.test(val)) return "Boolean"; 61 | if (/^\$\(.+\)$/.test(val) || /[><=!&|+\-*\/]/.test(val)) return "Expression"; 62 | return "String"; 63 | } 64 | 65 | // Utility: escape HTML (for safe tooltips) 66 | function htmlEscape(str) { 67 | return String(str).replace(/[&<>"']/g, function(m) { 68 | return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]); 69 | }); 70 | } 71 | 72 | // Utility: colors for types 73 | function typeColor(type) { 74 | return ({ 75 | "Expression": "#e8b84a", 76 | "Number": "#5cf", 77 | "Boolean": "#7f7", 78 | "String": "#baf", 79 | "Object": "#fa8" 80 | })[type] || "#fff"; 81 | } 82 | 83 | // Utility: highlight errors 84 | function errorColor(err) { return err ? "#f33" : "#b7ffac"; } 85 | 86 | // --- SAFE EVALUATOR --- 87 | // Built-ins for expressions 88 | const safeFns = { 89 | now: () => Date.now(), 90 | dayOfWeek: () => (new Date()).getDay(), 91 | min: Math.min, 92 | max: Math.max, 93 | abs: Math.abs, 94 | round: Math.round, 95 | floor: Math.floor, 96 | ceil: Math.ceil, 97 | clamp: (v, mn, mx) => Math.max(mn, Math.min(mx, v)), 98 | // Add your own as needed! 99 | }; 100 | function safeEval(expr, context = {}) { 101 | try { 102 | // Disallow unsafe words! 103 | if (/window|document|Function|eval|require|process|global/.test(expr)) throw new Error("Unsafe!"); 104 | // Replace $(foo) with context.foo 105 | expr = expr.replace(/\$\((\w+)\)/g, (_, n) => 106 | (n in context) ? JSON.stringify(context[n]) : 'undefined' 107 | ); 108 | // Bind safe functions and context vars 109 | let sandbox = Object.assign({}, safeFns, context); 110 | let f = new Function(...Object.keys(sandbox), `return (${expr})`); 111 | return f(...Object.values(sandbox)); 112 | } catch(e) { 113 | return `ERR: ${e.message}`; 114 | } 115 | } 116 | // --- DEPENDENCY GRAPH --- 117 | // Extract $(foo) variable references from a string/expression 118 | function extractDeps(str) { 119 | let out = []; 120 | String(str).replace(/\$\((\w+)\)/g, (_, n) => { out.push(n); return n; }); 121 | return out; 122 | } 123 | 124 | // Check for circular dependencies (DFS) 125 | function hasCircular(varName, getVars, seen = {}) { 126 | if (seen[varName]) return true; 127 | seen[varName] = true; 128 | const v = getVars().find(vv => vv.name === varName); 129 | if (!v) return false; 130 | const deps = extractDeps(v.value); 131 | return deps.some(dep => hasCircular(dep, getVars, {...seen})); 132 | } 133 | 134 | // --- VALUE RESOLUTION (with dependency/expr) --- 135 | function getVarResolved(v, visited = {}) { 136 | if (!v || !v.name) return ''; 137 | // Circular reference check 138 | if (visited[v.name]) return 'ERR: Circular!'; 139 | visited[v.name] = true; 140 | // Expressions 141 | if (parseType(v.value) === "Expression") { 142 | try { 143 | updateCtx(); 144 | return safeEval(v.value, ctx); 145 | } catch (e) { 146 | return `ERR: ${e.message}`; 147 | } 148 | } 149 | // Numbers/booleans 150 | if (parseType(v.value) === "Number") return parseFloat(v.value); 151 | if (parseType(v.value) === "Boolean") return /^true$/i.test(v.value); 152 | // Plain string 153 | return v.value; 154 | } 155 | 156 | // --- CONTEXT EVALUATION --- 157 | function updateCtx() { 158 | ctx = {}; 159 | (globalVars || []).forEach(v => ctx[v.name] = getVarResolved(v)); 160 | (flowVars || []).forEach(v => ctx[v.name] = getVarResolved(v)); 161 | } 162 | 163 | // --- EVENT LISTENERS FOR VAR CHANGES (external modules can subscribe) --- 164 | function onVarsChange(fn) { listeners.push(fn); } 165 | function notifyVarsChange() { listeners.forEach(fn => fn(flowVars, globalVars)); } 166 | 167 | // --- AUTOCOMPLETE SUGGESTIONS (for UI fields) --- 168 | function suggestVars(fragment, scope = "all") { 169 | // scope: all, flow, global 170 | let arr = (scope === "global") ? globalVars : 171 | (scope === "flow") ? flowVars : 172 | [...globalVars, ...flowVars]; 173 | return arr.map(v => v.name).filter(n => n.startsWith(fragment)); 174 | } 175 | 176 | // --- TOOLTIP for variable reference (can wire up to any field) --- 177 | function makeVarTooltip(varName) { 178 | let v = flowVars.find(vv => vv.name === varName) || 179 | globalVars.find(vv => vv.name === varName); 180 | if (!v) return `[not found]`; 181 | let type = parseType(v.value); 182 | let val = getVarResolved(v); 183 | let circ = hasCircular(varName, () => [...flowVars, ...globalVars]); 184 | return ` 185 | ${htmlEscape(varName)}
186 | ${htmlEscape(v.value)}
187 | 188 | ${circ ? 'Circular ref!' : '='+htmlEscape(val)} 189 | 190 | `; 191 | } 192 | // --- VARIABLE MANAGER SIDEBAR UI --- 193 | function renderManager(el, opts = {}) { 194 | managerEl = el; 195 | let html = `Variables 196 |
197 | 198 | 199 | 200 | 201 |
202 |
203 |
204 | Test Expression: 205 | 206 | 207 |
`; 208 | el.innerHTML = html; 209 | renderVarsList(opts.globalVars ? "global" : "flow"); 210 | document.getElementById('addVarBtn').onclick = () => { 211 | let arr = opts.globalVars ? globalVars : flowVars; 212 | arr.push({ name:"", value:"", type:"String" }); 213 | renderVarsList(opts.globalVars ? "global" : "flow"); 214 | notifyVarsChange(); 215 | }; 216 | // Export variables to Hubitat 217 | document.getElementById('exportVarsBtn').onclick = async () => { 218 | let arr = opts.globalVars ? globalVars : flowVars; 219 | let fileName = prompt("Export file name?", opts.globalVars ? "global_vars.json" : "vars.json"); 220 | if (!fileName) return; 221 | let ok = await uploadVarsToHubitat(arr, fileName); 222 | if (ok) alert("Variables exported to Hubitat as " + fileName); 223 | }; 224 | // Import variables from Hubitat 225 | document.getElementById('importVarsBtn').onclick = async () => { 226 | let files = await fetchHubitatVarFiles(); 227 | if (!files.length) { alert("No .json files on Hubitat"); return; } 228 | // Create and show a real select dropdown dialog 229 | let modal = document.createElement("div"); 230 | modal.style.position = "fixed"; 231 | modal.style.top = "0"; 232 | modal.style.left = "0"; 233 | modal.style.width = "100vw"; 234 | modal.style.height = "100vh"; 235 | modal.style.background = "rgba(0,0,0,0.32)"; 236 | modal.style.display = "flex"; 237 | modal.style.alignItems = "center"; 238 | modal.style.justifyContent = "center"; 239 | modal.style.zIndex = "99999"; 240 | modal.innerHTML = ` 241 |
242 |
Import variables from:
243 | 246 |
247 | 248 | 249 |
250 |
251 | `; 252 | document.body.appendChild(modal); 253 | document.getElementById("varsImportOK").onclick = async () => { 254 | let pick = document.getElementById("varsFileDropdown").value; 255 | document.body.removeChild(modal); 256 | if (!pick) return; 257 | let txt = await fetchHubitatVarFileContent(pick); 258 | try { 259 | let arr = JSON.parse(txt); 260 | if (Array.isArray(arr)) { 261 | if (opts.globalVars) globalVars = arr; 262 | else flowVars = arr; 263 | renderVarsList(opts.globalVars ? "global" : "flow"); 264 | notifyVarsChange(); 265 | alert("Imported " + arr.length + " vars from " + pick); 266 | } else alert("File does not contain an array."); 267 | } catch(e) { alert("Failed to parse file: " + e.message); } 268 | }; 269 | document.getElementById("varsImportCancel").onclick = () => { 270 | document.body.removeChild(modal); 271 | }; 272 | }; 273 | 274 | document.getElementById('switchScopeBtn').onclick = () => { 275 | opts.globalVars = !opts.globalVars; 276 | renderManager(managerEl, opts); 277 | }; 278 | document.getElementById('exprInput').oninput = function() { 279 | updateCtx(); 280 | let val = safeEval(this.value, ctx); 281 | document.getElementById('exprResult').innerText = `= ${val}`; 282 | }; 283 | } 284 | 285 | // --- RENDER VARIABLES LIST --- 286 | function renderVarsList(scope = "flow") { 287 | updateCtx(); 288 | let arr = scope === "global" ? globalVars : flowVars; 289 | let vlist = managerEl.querySelector('#varsList'); 290 | vlist.innerHTML = ''; 291 | arr.forEach((v,i) => { 292 | let type = parseType(v.value); 293 | let valDisplay = getVarResolved(v); 294 | let circ = hasCircular(v.name, () => arr); 295 | let used = isVarUsed(v.name, arr) || false; 296 | vlist.innerHTML += `
297 | 299 | 301 | ${type} 302 | ${circ ? "ERR: Circular" : (valDisplay!==undefined?" = "+htmlEscape(valDisplay):"")} 303 | 304 |
`; 305 | }); 306 | } 307 | 308 | // --- VARIABLE USAGE DETECTION (across all scopes) --- 309 | function isVarUsed(name, arr) { 310 | let regex = new RegExp("\\$\\(" + name + "\\)", "g"); 311 | let others = (arr === flowVars) ? [...flowVars] : [...globalVars]; 312 | for (let v of others) { 313 | if (v.value && regex.test(v.value)) return true; 314 | } 315 | // Could check node fields too (user responsibility to call this) 316 | return false; 317 | } 318 | 319 | // --- INLINE UI EVENTS FOR VARS --- 320 | root.setVarName = function(scope, i, name) { 321 | let arr = scope === "global" ? globalVars : flowVars; 322 | arr[i].name = name; 323 | notifyVarsChange(); 324 | }; 325 | root.setVarVal = function(scope, i, val) { 326 | let arr = scope === "global" ? globalVars : flowVars; 327 | arr[i].value = val; 328 | notifyVarsChange(); 329 | }; 330 | root.delVar = function(scope, i) { 331 | let arr = scope === "global" ? globalVars : flowVars; 332 | arr.splice(i,1); 333 | renderVarsList(scope); 334 | notifyVarsChange(); 335 | }; 336 | // --- ADVANCED: VAR HOVER TOOLTIP --- 337 | // Call this in your value field's mouseover to get a tooltip with variable info 338 | root.varTooltip = function(varName) { 339 | return makeVarTooltip(varName); 340 | }; 341 | 342 | // --- AUTOCOMPLETE: For UI input fields --- 343 | // Suggests variable names for a given fragment (call in keyup/input) 344 | root.suggest = function(fragment, scope = "all") { 345 | return suggestVars(fragment, scope); 346 | }; 347 | 348 | // --- PUBLIC API --- 349 | root.flowVars = flowVars; 350 | root.globalVars = globalVars; 351 | root.renderManager = renderManager; 352 | root.evaluate = function(expr) { updateCtx(); return safeEval(expr, ctx); }; 353 | root.add = function(name, value, type, scope="flow") { 354 | let arr = scope === "global" ? globalVars : flowVars; 355 | arr.push({ name, value, type }); 356 | renderVarsList(scope); 357 | notifyVarsChange(); 358 | }; 359 | root.getResolved = getVarResolved; 360 | root.isVarUsed = isVarUsed; 361 | root.onVarsChange = onVarsChange; 362 | root.suggestVars = suggestVars; 363 | 364 | // --- OPTIONAL: Hook for custom field autocomplete/tooltip integration --- 365 | // Example: in your input field's oninput: 366 | // let suggestions = flowVars.suggest(this.value); 367 | // Example: onmouseover for $(foo): show flowVars.varTooltip("foo") 368 | 369 | // --- FINAL CLEANUP --- 370 | 371 | root.updateCtx = updateCtx; 372 | root.getVars = function(scope="all") { 373 | if (scope === "global") return globalVars; 374 | if (scope === "flow") return flowVars; 375 | return [...globalVars, ...flowVars]; 376 | }; 377 | 378 | // Set the globalVars array safely (for when loading from file!) 379 | root.setGlobalVars = function(arr) { 380 | globalVars.length = 0; 381 | if (Array.isArray(arr)) for (const v of arr) globalVars.push(v); 382 | updateCtx(); 383 | notifyVarsChange(); 384 | }; 385 | 386 | updateCtx(); 387 | })(); 388 | -------------------------------------------------------------------------------- /apps/FlowEngine/packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Flow Engine", 3 | "version": "1.0.0", 4 | "releaseNotes": "Initial Beta Release", 5 | "minimumHEVersion": "2.3.8.117", 6 | "author": "Bryan Turcotte (@bptworld)", 7 | "dateReleased": "2025-05-28", 8 | "documentationLink": "https://github.com/bptworld/Hubitat", 9 | "communityLink": "", 10 | "licenseFile": "", 11 | "payPalUrl": "https://paypal.me/bptworld", 12 | "category": "Utility", 13 | "tags": ["Flow", "Tools & Utilities"], 14 | "files": [ 15 | { 16 | "id": "f069a335-86f1-43c8-b2d9-7bd086517134", 17 | "name": "pickr.es5.min.js", 18 | "namespace": "BPTWorld", 19 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/pickr.es5.min.js", 20 | "description": "Color Picker File", 21 | "required": true 22 | }, 23 | { 24 | "id": "f188ce7b-bd8d-4791-a9d3-dc84a7ccf7cb", 25 | "name": "pickr.min.css", 26 | "namespace": "BPTWorld", 27 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/pickr.min.css", 28 | "description": "Color Picker File", 29 | "required": true 30 | }, 31 | { 32 | "id": "c3726581-e8fa-42ba-9164-52b45dcbe42d", 33 | "name": "drawflow-js.min.js", 34 | "namespace": "BPTWorld", 35 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/drawflow-js.min.js", 36 | "description": "UI File", 37 | "required": true 38 | }, 39 | { 40 | "id": "d8eead19-7efd-467e-9004-4f9ec66cb7ba", 41 | "name": "drawflow-extra.css", 42 | "namespace": "BPTWorld", 43 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/drawflow-extra.css", 44 | "description": "UI File", 45 | "required": true 46 | }, 47 | { 48 | "id": "0a04c142-7b90-45d5-b8fd-9073ec8bec99", 49 | "name": "drawflow-css.min.css", 50 | "namespace": "BPTWorld", 51 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/drawflow-css.min.css", 52 | "description": "UI File", 53 | "required": true 54 | }, 55 | { 56 | "id": "d9974ef6-a02e-41c2-9984-add8fef04a29", 57 | "name": "flowvars.js", 58 | "namespace": "BPTWorld", 59 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/flowvars.js", 60 | "description": "UI File", 61 | "required": true 62 | }, 63 | { 64 | "id": "e00bed86-4f80-4149-99c4-344e92a525e8", 65 | "name": "flowengineeditor.html", 66 | "namespace": "BPTWorld", 67 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/flowengineeditor.html", 68 | "description": "Flow Eninge Editor UI", 69 | "required": true 70 | } 71 | ], 72 | "apps": [ 73 | { 74 | "id": "7f9f927d-bc2a-4fe6-b12a-fd7d8bee3e46", 75 | "name": "Flow Engine Parent", 76 | "namespace": "BPTWorld", 77 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/flowengine-parent.groovy", 78 | "description": "Parent File", 79 | "oauth": true, 80 | "required": true 81 | }, 82 | { 83 | "id": "414e5892-81f2-4f76-a0c7-914163276fc9", 84 | "name": "Flow Engine Child", 85 | "namespace": "BPTWorld", 86 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/flowengine-child.groovy", 87 | "description": "Parent File", 88 | "oauth": true, 89 | "required": true 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /apps/FlowEngine/pickr.min.css: -------------------------------------------------------------------------------- 1 | /*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ 2 | .pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;border-radius:.15em;background:url("data:image/svg+xml;utf8, ") no-repeat center;background-size:0;transition:all .3s}.pickr .pcr-button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pickr .pcr-button::before{z-index:initial}.pickr .pcr-button::after{position:absolute;content:"";top:0;left:0;height:100%;width:100%;transition:background .3s;background:var(--pcr-color);border-radius:.15em}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear::before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-button.disabled{cursor:not-allowed}.pickr *,.pcr-app *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr input:focus,.pickr input.pcr-active,.pickr button:focus,.pickr button.pcr-active,.pcr-app input:focus,.pcr-app input.pcr-active,.pcr-app button:focus,.pcr-app button.pcr-active{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-palette,.pickr .pcr-slider,.pcr-app .pcr-palette,.pcr-app .pcr-slider{transition:box-shadow .3s}.pickr .pcr-palette:focus,.pickr .pcr-slider:focus,.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(0,0,0,.25)}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);left:0;top:0}.pcr-app.visible{transition:opacity .3s;visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports(display: grid){.pcr-app .pcr-swatches{display:grid;align-items:center;grid-template-columns:repeat(auto-fit, 1.75em)}}.pcr-app .pcr-swatches>button{font-size:1em;position:relative;width:calc(1.75em - 5px);height:calc(1.75em - 5px);border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:rgba(0,0,0,0);z-index:1}.pcr-app .pcr-swatches>button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button::after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:var(--pcr-color);border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{filter:brightness(1.05)}.pcr-app .pcr-swatches>button:not(.pcr-active){box-shadow:none}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -0.2em 0 -0.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{filter:brightness(0.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::-moz-selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff;width:auto}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff}.pcr-app .pcr-interaction .pcr-save:hover,.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover{filter:brightness(0.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-cancel{background:#f44250}.pcr-app .pcr-interaction .pcr-clear:focus,.pcr-app .pcr-interaction .pcr-cancel:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pcr-app .pcr-selection .pcr-color-palette,.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity{position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-palette:active,.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active{cursor:grabbing;cursor:-webkit-grabbing}.pcr-app[data-theme=classic]{width:28.5em;max-width:95vw;padding:.8em}.pcr-app[data-theme=classic] .pcr-selection{display:flex;justify-content:space-between;flex-grow:1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview{position:relative;z-index:1;width:2em;display:flex;flex-direction:column;justify-content:space-between;margin-right:.75em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-last-color{cursor:pointer;border-radius:.15em .15em 0 0;z-index:2}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-current-color{border-radius:0 0 .15em .15em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-last-color,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-preview .pcr-current-color{background:var(--pcr-color);width:100%;height:50%}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette{width:100%;height:8em;z-index:1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette .pcr-palette{flex-grow:1;border-radius:.15em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-palette .pcr-palette::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity{margin-left:.75em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-picker{left:50%;transform:translateX(-50%)}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-slider{width:8px;flex-grow:1;border-radius:50em}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(to bottom, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%))}.pcr-app[data-theme=classic] .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(to bottom, transparent, black),url("data:image/svg+xml;utf8, ");background-size:100%,50%} -------------------------------------------------------------------------------- /apps/TheFlasher/packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "The Flasher", 3 | "minimumHEVersion": "1.0.0", 4 | "author": "BPTWorld", 5 | "dateReleased": "2024-03-16", 6 | "documentationLink": "https://github.com/bptworld/Hubitat", 7 | "communityLink": "https://community.hubitat.com/t/placeholder-bundle-manager-find-install-and-update-bundles-quickly-and-easily/94567", 8 | "releaseNotes": "Flip between two different colors when using built in Flash option", 9 | "apps": [ 10 | { 11 | "id" : "44131a76-320b-4b13-baea-9d2cc161ceb7", 12 | "name": "The Flasher Parent", 13 | "namespace": "BPTWorld", 14 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/TheFlasher/theflasher-parent.groovy", 15 | "required": true, 16 | "oauth": false, 17 | "version": "1.3.6", 18 | "primary": true 19 | }, 20 | { 21 | "id" : "b06563f2-984f-49ec-a2de-22135006f848", 22 | "name": "The Flasher Child", 23 | "namespace": "BPTWorld", 24 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/TheFlasher/theflasher-child.groovy", 25 | "required": true, 26 | "oauth": false, 27 | "version": "1.3.6", 28 | "primary": true 29 | } 30 | ], 31 | "drivers": [ 32 | ], 33 | "libraries": [ 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /apps/devicewatchdog/dw-tiledriver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** Device Watchdog Tile Driver **************** 3 | * 4 | * Design Usage: 5 | * This driver formats the Device Watchdog data to be used with Hubitat's Dashboards. 6 | * 7 | * Copyright 2019-2024 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to mention it on the Hubitat forums! Thanks. 10 | * 11 | * Remember...I am not a professional programmer, everything I do takes a lot of time and research (then MORE research)! 12 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 13 | * 14 | * Paypal at: https://paypal.me/bptworld 15 | * 16 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 17 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 18 | * the community grow. Have FUN with it! 19 | * 20 | * ------------------------------------------------------------------------------------------------------------------------------ 21 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 22 | * in compliance with the License. You may obtain a copy of the License at: 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 27 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 28 | * for the specific language governing permissions and limitations under the License. 29 | * 30 | * ------------------------------------------------------------------------------------------------------------------------------ 31 | * 32 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 33 | * 34 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 35 | * 36 | * ------------------------------------------------------------------------------------------------------------------------------ 37 | * 38 | * Changes: 39 | * 40 | * All changes are listed in the child app 41 | */ 42 | 43 | metadata { 44 | definition (name: "Device Watchdog Tile", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "") { 45 | capability "Actuator" 46 | 47 | command "clearStates" 48 | command "sendWatchdogActivityMap", ["string"] 49 | command "sendWatchdogActivityAttMap", ["string"] 50 | command "sendWatchdogBatteryMap", ["string"] 51 | command "sendWatchdogStatusMap", ["string"] 52 | command "sendWatchdogComboActBatMap", ["string"] 53 | command "sendWatchdogSpecialMap", ["string"] 54 | 55 | attribute "bpt-watchdogActivity1", "string" 56 | attribute "bpt-watchdogActivity2", "string" 57 | attribute "bpt-watchdogActivity3", "string" 58 | attribute "bpt-ActivityNumOfDevices", "string" 59 | 60 | attribute "bpt-watchdogActivityAtt1", "string" 61 | attribute "bpt-watchdogActivityAtt2", "string" 62 | attribute "bpt-watchdogActivityAtt3", "string" 63 | attribute "bpt-ActivityAttNumOfDevices", "string" 64 | 65 | attribute "bpt-watchdogBattery1", "string" 66 | attribute "bpt-watchdogBattery2", "string" 67 | attribute "bpt-watchdogBattery3", "string" 68 | attribute "bpt-BatteryNumOfDevices", "string" 69 | 70 | attribute "bpt-watchdogStatus1", "string" 71 | attribute "bpt-watchdogStatus2", "string" 72 | attribute "bpt-watchdogStatus3", "string" 73 | attribute "bpt-StatusNumOfDevices", "string" 74 | 75 | attribute "bpt-watchdogComboActBat", "string" 76 | 77 | attribute "bpt-watchdogSpecial1", "string" 78 | attribute "bpt-watchdogSpecial2", "string" 79 | attribute "bpt-watchdogSpecial3", "string" 80 | attribute "bpt-SpecialNumOfDevices", "string" 81 | 82 | attribute "watchdogActivityCount1", "string" 83 | attribute "watchdogActivityCount2", "string" 84 | attribute "watchdogActivityCount3", "string" 85 | 86 | attribute "watchdogActivityAttCount1", "string" 87 | attribute "watchdogActivityAttCount2", "string" 88 | attribute "watchdogActivityAttCount3", "string" 89 | 90 | attribute "watchdogBatteryCount1", "string" 91 | attribute "watchdogBatteryCount2", "string" 92 | attribute "watchdogBatteryCount3", "string" 93 | 94 | attribute "watchdogStatusCount1", "string" 95 | attribute "watchdogStatusCount2", "string" 96 | attribute "watchdogStatusCount3", "string" 97 | 98 | attribute "watchdogComboActBatCount", "string" 99 | 100 | attribute "watchdogSpecialCount1", "string" 101 | attribute "watchdogSpecialCount2", "string" 102 | attribute "watchdogSpecialCount3", "string" 103 | } 104 | preferences() { 105 | section(""){ 106 | input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: true 107 | } 108 | } 109 | } 110 | 111 | def clearStates() { 112 | state.clear() 113 | } 114 | 115 | def sendWatchdogActivityMap(activityMap) { 116 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Activity data!" 117 | def (whichMap, theData) = activityMap.split('::') 118 | activityDeviceCount = activityMap.length() 119 | if(activityDeviceCount <= 1024) { 120 | if(logEnable) log.debug "activityDevice - has ${activityDeviceCount} Characters" 121 | } else { 122 | theData = "Too many characters to display on Dashboard (${activityDeviceCount})" 123 | } 124 | if(whichMap == "1") { 125 | sendEvent(name: "bpt-watchdogActivity1", value: theData, displayed: true) 126 | sendEvent(name: "watchdogActivityCount1", value: activityDeviceCount, displayed: true) 127 | } 128 | if(whichMap == "2") { 129 | sendEvent(name: "bpt-watchdogActivity2", value: theData, displayed: true) 130 | sendEvent(name: "watchdogActivityCount2", value: activityDeviceCount, displayed: true) 131 | } 132 | if(whichMap == "3") { 133 | sendEvent(name: "bpt-watchdogActivity3", value: theData, displayed: true) 134 | sendEvent(name: "watchdogActivityCount3", value: activityDeviceCount, displayed: true) 135 | } 136 | } 137 | 138 | def sendWatchdogActivityAttMap(activityAttMap) { 139 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Activity with Attributes data!" 140 | def (whichMap, theData) = activityAttMap.split('::') 141 | activityAttDeviceCount = activityAttMap.length() 142 | if(activityAttDeviceCount <= 1024) { 143 | if(logEnable) log.debug "activityAttDevice - has ${activityAttDeviceCount} Characters" 144 | } else { 145 | theData = "Too many characters to display on Dashboard (${activityAttDeviceCount})" 146 | } 147 | if(whichMap == "1") { 148 | sendEvent(name: "bpt-watchdogActivityAtt1", value: theData, displayed: true) 149 | sendEvent(name: "watchdogActivityAttCount1", value: activityAttDeviceCount, displayed: true) 150 | } 151 | if(whichMap == "2") { 152 | sendEvent(name: "bpt-watchdogActivityAtt2", value: theData, displayed: true) 153 | sendEvent(name: "watchdogActivityAttCount2", value: activityAttDeviceCount, displayed: true) 154 | } 155 | if(whichMap == "3") { 156 | sendEvent(name: "bpt-watchdogActivityAtt3", value: theData, displayed: true) 157 | sendEvent(name: "watchdogActivityAttCount3", value: activityAttDeviceCount, displayed: true) 158 | } 159 | } 160 | 161 | def sendWatchdogBatteryMap(batteryMap) { 162 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Battery data!" 163 | def (whichMap, theData) = batteryMap.split('::') 164 | batteryDeviceCount = theData.length() 165 | if(batteryDeviceCount <= 1024) { 166 | if(logEnable) log.debug "batteryDevice - has ${batteryDeviceCount} Characters" 167 | } else { 168 | theData = "Too many characters to display on Dashboard (${batteryDeviceCount})" 169 | } 170 | if(whichMap == "1") { 171 | sendEvent(name: "bpt-watchdogBattery1", value: theData, displayed: true) 172 | sendEvent(name: "watchdogBatteryCount1", value: batteryDeviceCount, displayed: true) 173 | } 174 | if(whichMap == "2") { 175 | sendEvent(name: "bpt-watchdogBattery2", value: theData, displayed: true) 176 | sendEvent(name: "watchdogBatteryCount2", value: batteryDeviceCount, displayed: true) 177 | } 178 | if(whichMap == "3") { 179 | sendEvent(name: "bpt-watchdogBattery3", value: theData, displayed: true) 180 | sendEvent(name: "watchdogBatteryCount3", value: batteryDeviceCount, displayed: true) 181 | } 182 | } 183 | 184 | def sendWatchdogStatusMap(statusMap) { 185 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Status data!" 186 | def (whichMap, theData) = statusMap.split('::') 187 | statusDeviceCount = theData.length() 188 | if(statusDeviceCount <= 1024) { 189 | if(logEnable) log.debug "statusDevice - has ${statusDeviceCount} Characters" 190 | } else { 191 | theData = "Too many characters to display on Dashboard (${statusDeviceCount})" 192 | } 193 | if(whichMap == "1") { 194 | sendEvent(name: "bpt-watchdogStatus1", value: theData, displayed: true) 195 | sendEvent(name: "watchdogStatusCount1", value: statusDeviceCount, displayed: true) 196 | } 197 | if(whichMap == "2") { 198 | sendEvent(name: "bpt-watchdogStatus2", value: theData, displayed: true) 199 | sendEvent(name: "watchdogStatusCount2", value: statusDeviceCount, displayed: true) 200 | } 201 | if(whichMap == "3") { 202 | sendEvent(name: "bpt-watchdogStatus3", value: theData, displayed: true) 203 | sendEvent(name: "watchdogStatusCount3", value: statusDeviceCount, displayed: true) 204 | } 205 | } 206 | 207 | def sendWatchdogComboActBatMap(comboMap) { 208 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Combo data!" 209 | def (whichMap, theData) = comboMap.split('::') 210 | comboDeviceCount = theData.length() 211 | if(comboDeviceCount <= 1024) { 212 | if(logEnable) log.debug "comboDevice - has ${comboDeviceCount} Characters" 213 | } else { 214 | theData = "Too many characters to display on Dashboard (${comboDeviceCount})" 215 | } 216 | if(whichMap == "1") { 217 | sendEvent(name: "bpt-watchdogComboActBat", value: theData, displayed: true) 218 | sendEvent(name: "watchdogComboActBatCount", value: comboDeviceCount, displayed: true) 219 | } 220 | } 221 | 222 | def sendWatchdogSpecialMap(specialMap) { 223 | if(logEnable) log.debug "In Device Watchdog Tile - Received new Special data!" 224 | def (whichMap, theData) = specialMap.split('::') 225 | specialDeviceCount = specialMap.length() 226 | if(specialDeviceCount <= 1024) { 227 | if(logEnable) log.debug "specialDevice - has ${specialDeviceCount} Characters" 228 | } else { 229 | theData = "Too many characters to display on Dashboard (${specialDeviceCount})" 230 | } 231 | if(whichMap == "1") { 232 | sendEvent(name: "bpt-watchdogSpecial1", value: theData, displayed: true) 233 | sendEvent(name: "watchdogSpecialCount1", value: specialDeviceCount, displayed: true) 234 | } 235 | if(whichMap == "2") { 236 | sendEvent(name: "bpt-watchdogSpecial2", value: theData, displayed: true) 237 | sendEvent(name: "watchdogSpecialCount2", value: specialDeviceCount, displayed: true) 238 | } 239 | if(whichMap == "3") { 240 | sendEvent(name: "bpt-watchdogSpecial3", value: theData, displayed: true) 241 | sendEvent(name: "watchdogSpecialCount3", value: specialDeviceCount, displayed: true) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /apps/devicewatchdog/packageManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageName": "Device Watchdog", 3 | "minimumHEVersion": "1.0.0", 4 | "author": "BPTWorld", 5 | "dateReleased": "2024-04-13", 6 | "documentationLink": "https://github.com/bptworld/Hubitat", 7 | "communityLink": "", 8 | "releaseNotes": "More color", 9 | "apps": [ 10 | { 11 | "id" : "8ac392c0-e2e1-4dd9-88e6-97253787ac72", 12 | "name": "Device Watchdog Parent", 13 | "namespace": "BPTWorld", 14 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/devicewatchdog/dw-parent.groovy", 15 | "required": true, 16 | "oauth": false, 17 | "version": "2.5.0", 18 | "primary": true 19 | }, 20 | { 21 | "id" : "55afbc78-d4d0-4477-9768-4dc8567b6e37", 22 | "name": "Device Watchdog Child", 23 | "namespace": "BPTWorld", 24 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/devicewatchdog/dw-child.groovy", 25 | "required": true, 26 | "oauth": false, 27 | "version": "2.5.0", 28 | "primary": true 29 | } 30 | ], 31 | "drivers": [ 32 | { 33 | "id" : "4e84796d-be50-4871-a810-b2e947991df4", 34 | "name": "Device Watchdog Tile Driver", 35 | "namespace": "BPTWorld", 36 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/devicewatchdog/dw-tiledriver.groovy", 37 | "required": true, 38 | "oauth": false, 39 | "version": "2.5.0", 40 | "primary": true 41 | } 42 | ], 43 | "libraries": [ 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /apps/simplepush/simplepush-Barebones.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** Simplepush App **************** 3 | * Barebones app to test the Simplepush API 4 | * 5 | * As always, You are free to change, ripout, copy, modify or 6 | * otherwise use the code in anyway you want. Have FUN! 7 | * 8 | * Be sure to enable OAuth 9 | * 10 | * Thanks to the great work/additions from @TMLeafs 11 | */ 12 | 13 | definition( 14 | name: "Simplepush Barebones", 15 | namespace: "BPTWorld", 16 | author: "Bryan Turcotte", 17 | description: "Barebones app to test the Simplepush API", 18 | category: "Convenience", 19 | iconUrl: "", 20 | iconX2Url: "", 21 | iconX3Url: "", 22 | importUrl: "", 23 | ) 24 | 25 | preferences { 26 | page name: "pageConfig" 27 | } 28 | 29 | def pageConfig() { 30 | // Do not share your accessToken or OAuth Url when posting screenshots on Hubitat forums or anywhere else 31 | if(logEnable) log.debug "The accessToken is: {$state.accessToken}" 32 | def extUri = fullApiServerUrl().replaceAll("null","webhook?access_token=${state.accessToken}") 33 | if(logEnable) log.debug "The OAUTH Url is {$extUri}" 34 | 35 | 36 | dynamicPage(name: "", title: "", install: true, uninstall: true) { 37 | section() { 38 | paragraph "

Simplepush Test

" 39 | } 40 | 41 | section(){ 42 | label title: "Enter a name for this automation", required:true, submitOnChange:true 43 | } 44 | 45 | section(" Options") { 46 | input "simpleKey", "text", title: "Simplepush Key", required:true, submitOnChange:true 47 | input "simpleMsg", "text", title: "Message", required:true, submitOnChange:true 48 | input "eventType", "text", title: "Event (Optional) - Set up in the phone app first.", sumbitOnChange:true 49 | input "sendMsg", "button", title: "Send Test Msg", width: 3 50 | } 51 | 52 | section(" General") { 53 | input "logEnable", "bool", title: "Enable Debug Options", description: "Log Options", defaultValue:false, submitOnChange:true 54 | if(logEnable) { 55 | input "logOffTime", "enum", title: "Logs Off Time", required:false, multiple:false, options: ["1 Hour", "2 Hours", "3 Hours", "4 Hours", "5 Hours", "Keep On"] 56 | } 57 | } 58 | } 59 | } 60 | 61 | def installed() { 62 | log.debug "Installed with settings: ${settings}" 63 | updated() 64 | } 65 | 66 | def updated() { 67 | if(logEnable) log.debug "Updated with settings: ${settings}" 68 | unschedule() 69 | unsubscribe() 70 | if(logEnable && logOffTime == "1 Hour") runIn(3600, logsOff, [overwrite:false]) 71 | if(logEnable && logOffTime == "2 Hours") runIn(7200, logsOff, [overwrite:false]) 72 | if(logEnable && logOffTime == "3 Hours") runIn(10800, logsOff, [overwrite:false]) 73 | if(logEnable && logOffTime == "4 Hours") runIn(14400, logsOff, [overwrite:false]) 74 | if(logEnable && logOffTime == "5 Hours") runIn(18000, logsOff, [overwrite:false]) 75 | if(logEnagle && logOffTime == "Keep On") unschedule(logsOff) 76 | initialize() 77 | } 78 | 79 | def initialize() { 80 | def oauthStatus = "" 81 | //enable OAuth in the app settings or this call will fail 82 | try{ 83 | if (!state.accessToken) { 84 | createAccessToken() 85 | } 86 | } 87 | catch (e) { 88 | oauthStatus = "Edit Apps Code -> Simplepush. Select 'oAUTH' in the top right and use defaults to enable oAUTH to continue." 89 | logError(oauthStatus) 90 | } 91 | 92 | if(pauseApp || state.eSwitch) { 93 | log.info "${app.label} is Paused or Disabled" 94 | } else { 95 | // Nothing 96 | } 97 | } 98 | 99 | mappings { 100 | path("/webhook") { action: [ GET: "webhook"] } 101 | } 102 | 103 | def webhook() { 104 | if(logEnable) log.info "${app.getLabel()} executing 'webhook()'" 105 | if(logEnable) log.info "params: $params" 106 | return render(contentType: "text/html", data: "webhook params:
$params

webhook request:
$request", status: 200) 107 | } 108 | 109 | def sendAsynchttpPost() { 110 | def extUri = fullApiServerUrl().replaceAll("null","webhook?access_token=${state.accessToken}") 111 | def postParams = [ 112 | uri: "https://simplepu.sh", 113 | requestContentType: 'application/json', 114 | contentType: 'application/json', 115 | body : ["key": simpleKey, "title": "Hubitat Notification", "msg": simpleMsg, "event": eventType, "attachments": attach, "actions": [["name": "yes", "url": "${extUri}&action=yes"],["name": "no", "url": "${extUri}&action=no"]]] 116 | ] 117 | if(logEnable) log.debug "In sendAsynchttpPost - ${postParams}" 118 | asynchttpPost('myCallbackMethod', postParams, [dataitem1: "datavalue1"]) 119 | } 120 | 121 | def myCallbackMethod(response, data) { 122 | if(data["dataitem1"] == "datavalue1") 123 | if(logEnable) log.debug "Data was passed successfully" 124 | if(logEnable) log.debug "status of post call is: ${response.status}" 125 | } 126 | 127 | def appButtonHandler(buttonPressed) { 128 | if(logEnable) log.debug "In appButtonHandler (${state.version}) - Button Pressed: ${buttonPressed}" 129 | if(buttonPressed == "sendMsg") { 130 | if(logEnable) log.debug "In appButtonHandler - Working on: ${buttonPressed}" 131 | sendAsynchttpPost()} 132 | } 133 | -------------------------------------------------------------------------------- /apps/simplepush/simplepushNotifications-driver.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * **************** Simplepush Notification Driver **************** 3 | * 4 | * Design Usage: 5 | * This driver works with the Simplepush Notification app. 6 | * 7 | * Copyright 2024 Bryan Turcotte (@bptworld) 8 | * 9 | * This App is free. If you like and use this app, please be sure to mention it on the Hubitat forums! Thanks. 10 | * 11 | * Remember...I am not a programmer, everything I do takes a lot of time and research (then MORE research)! 12 | * Donations are never necessary but always appreciated. Donations to support development efforts are accepted via: 13 | * 14 | * Paypal at: https://paypal.me/bptworld 15 | * 16 | * Unless noted in the code, ALL code contained within this app is mine. You are free to change, ripout, copy, modify or 17 | * otherwise use the code in anyway you want. This is a hobby, I'm more than happy to share what I have learned and help 18 | * the community grow. Have FUN with it! 19 | * 20 | * ------------------------------------------------------------------------------------------------------------------------------ 21 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 22 | * in compliance with the License. You may obtain a copy of the License at: 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 27 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 28 | * for the specific language governing permissions and limitations under the License. 29 | * 30 | * ------------------------------------------------------------------------------------------------------------------------------ 31 | * 32 | * If modifying this project, please keep the above header intact and add your comments/credits below - Thank you! - @BPTWorld 33 | * App and Driver updates can be found at https://github.com/bptworld/Hubitat 34 | * 35 | * ------------------------------------------------------------------------------------------------------------------------------ 36 | * 37 | * Changes: 38 | * Changes are listed in the app. 39 | */ 40 | 41 | metadata { 42 | definition (name: "Simplepush Notification Driver", namespace: "BPTWorld", author: "Bryan Turcotte", importUrl: "") { 43 | capability "Notification" 44 | capability "Actuator" 45 | capability "Switch" 46 | 47 | command "sendSimplepush", [[name:"title", type:"STRING", description: "Optional Title"], 48 | [name:"theMessage", type:"STRING", description:"Message to send"], 49 | [name:"actions", type:"STRING", description: "Optional - 'option1-option2' - Seperated by a -"], 50 | [name:"theEvent", type:"STRING", description: "Optional Event"]] 51 | 52 | command "sendSimplepushwebCore",[[name:"title", type:"STRING", description: "Optional Title"], 53 | [name:"theMessage", type:"STRING", description:"Message to send"], 54 | [name:"WebCoreURL", type:"STRING", description:"WebCoRE External URL"], 55 | [name:"theEvent", type:"STRING", description: "Optional Event"]] 56 | 57 | attribute "lastMessage", "string" 58 | attribute "lastAction", "string" 59 | attribute "sentAt", "string" 60 | attribute "switch", "string" 61 | } 62 | preferences() { 63 | section(){ 64 | input name: "about", type: "paragraph", element: "paragraph", title: "Simplepush Notification", description: "This device was created by Simplepush Notification

Actions: option1-option2
ie. on-off, off-on, yes-no, light-dark, whatever you want!

Selecting Option1 will turn this device ON, Option2 with turn this device OFF." 65 | 66 | input name: "about", type: "paragraph", element: "paragraph", title: "Use with RM", description: "In the Message box use syntax 'Title:Message:Actions:Event'.

ie. 'My Title:This is a Test:yes-no:silent'.

If a field isn't needed use 'na', do not leave a field blank or missing." 67 | 68 | input name: "simpleKey", type: "text", title: "Simplepush Key", description: "Each 'User/Phone' will have one Key that can be used across as many virtual devices as you need." 69 | input name: "simpleTitle", type: "text", title: "Push Title", description: "Default Title to be used if no custom title is specified." 70 | 71 | input("logEnable", "bool", title: "Enable logging", required: false, defaultValue: false) 72 | } 73 | } 74 | } 75 | 76 | def sendSimplepush(title=null, theMessage, actions=null, theEvent=null) { 77 | if(simpleKey) { 78 | if(logEnable) log.info "In sendSimplepush - ${title} - ${theMessage} - Actions: ${actions} - Event: ${theEvent}" 79 | def data = new Date() 80 | sendEvent(name: "sentAt", value: data, displayed: true) 81 | sendEvent(name: "lastMessage", value: theMessage, displayed: true) 82 | if(title == "na" || title == null) title = simpleTitle ?: "" 83 | theDevice = device.id 84 | parent.sendAsynchttpPost(theDevice, simpleKey, title, theMessage, actions, theEvent) 85 | } else { 86 | log.warn "Simplepush Driver - Be sure to enter your Simplepush Key in to the driver." 87 | } 88 | } 89 | 90 | def sendSimplepushwebCore(title=null, theMessage, webCoreURL=null,theEvent=null) { 91 | if(simpleKey) { 92 | if(webCoreURL){ 93 | if(logEnable) log.info "In sendSimplepushwebCore - ${title} - ${theMessage} - Event: ${theEvent} - WebCoRE ${webCoreURL}" 94 | def data = new Date() 95 | sendEvent(name: "sentAt", value: data, displayed: true) 96 | sendEvent(name: "lastMessage", value: theMessage, displayed: true) 97 | if(title == "na" || title == null) title = simpleTitle ?: "" 98 | theDevice = device.id 99 | parent.sendAsynchttpPostwebCore(theDevice, simpleKey, title, theMessage, webCoreURL, theEvent) 100 | } 101 | else{ 102 | log.warn "Simplepush Driver - Be sure to enter your WebCoRE External Piston URL when using the Send SimplePush WebCore Command." 103 | } 104 | } 105 | else { 106 | log.warn "Simplepush Driver - Be sure to enter your Simplepush Key in to the driver." 107 | } 108 | } 109 | 110 | def actionHandler(theAction) { 111 | if(logEnable) log.info "In actionHandler - ${theAction}" 112 | if(theAction == "act0") { on() } 113 | if(theAction == "act1") { off() } 114 | if(theAction == "act2") { } 115 | if(theAction == "act3") { } 116 | if(theAction == "act4") { } 117 | 118 | sendEvent(name: "lastAction", value: theAction, displayed: true) 119 | } 120 | 121 | def on() { 122 | sendEvent(name: "switch", value: "on", displayed: true) 123 | } 124 | 125 | def off() { 126 | sendEvent(name: "switch", value: "off", displayed: true) 127 | } 128 | 129 | def deviceNotification(data) { 130 | try{ 131 | if(logEnable) log.info "In deviceNotification - ${data}" 132 | theData = data.split(":") 133 | sendSimplepush(theData[0], theData[1], theData[2], theData[3]) 134 | } catch(e) { 135 | log.info "Simplepush - Please check your notification syntax. Must be 'Title:Your Message:Actions:Event'" 136 | log.error(getExceptionMessageWithLine(e)) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerMessage": "
BPTWorld Apps and Drivers - BIG update to Bundle Manager and how apps report their versions.
Please see the Bundle Manager thread for more.
", 3 | "footerMessage": "
BPTWorld Apps and Drivers
No walled gardens, No crazy requirements!
Donations are never necessary but always appreciated!
", 4 | } 5 | -------------------------------------------------------------------------------- /old-repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Bryan Turcotte (BPTWorld)", 3 | "gitHubUrl": "https://github.com/bptworld/Hubitat", 4 | "payPalUrl": "https://paypal.me/bptworld", 5 | "packages": [ 6 | { 7 | "name": "GCalendar Driver", 8 | "category": "Integrations", 9 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Drivers/gCalendar/packageManifest.json", 10 | "description": "Retrieves a Google Calendar to be used with HE Dashboards.", 11 | "tags": [ "Dashboards", "Misc. Devices", "Tools & Utilities" ] 12 | }, 13 | { 14 | "name": "One at a Time", 15 | "category": "Control", 16 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/One%20at%20a%20Time/packageManifest.json", 17 | "description": "Designed to allow only one switch, in a group of switches, to be on at a time.", 18 | "tags": [ "Automations & Groups", "Tools & Utilities" ] 19 | }, 20 | { 21 | "name": "Remote Wellness Check", 22 | "category": "Security", 23 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/Remote%20Wellness%20Check/packageManifest.json", 24 | "description": "Stay connected to your loved ones. Get notified if they haven't triggered a device in a specified time.", 25 | "tags": [ "Monitoring", "Safety & Security", "Tools & Utilities" ] 26 | }, 27 | { 28 | "name": "Ring Keypad Companion", 29 | "category": "Security", 30 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/Ring%20Keypad%20Companion/packageManifest.json", 31 | "description": "Make the Ring Keypad Gen2 do more! For use with the Ring Alarm Gen 2 Keypad using the Ring Alarm Keypad G2 Community Driver.", 32 | "tags": [ "Monitoring", "Safety & Security", "Tools & Utilities" ] 33 | }, 34 | { 35 | "name": "Simple Dates", 36 | "category": "Utility", 37 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/Simple%20Dates/packageManifest.json", 38 | "description": "Create a simple coutdown to your most important dates.", 39 | "tags": [ "Dashboards", "Monitoring", "Tools & Utilities" ] 40 | }, 41 | { 42 | "name": "Simple Device Timer", 43 | "category": "Control", 44 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/Simple%20Device%20Timer/packageManifest.json", 45 | "description": "Simple Device Timer with safety checks, reminders, multiple timers and restrictions.", 46 | "tags": [ "Dashboards", "Monitoring", "Timers", "Tools & Utilities" ] 47 | }, 48 | { 49 | "name": "Simple Groups", 50 | "category": "Utility", 51 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/Apps/Simple%20Groups/packageManifest.json", 52 | "description": "Group just about anything. Motion, Contact, Water Sensors and other devices. Even group the groups!", 53 | "tags": [ "Automations & Groups", "Tools & Utilities" ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /repositories.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Bryan Turcotte (BPTWorld)", 3 | "gitHubUrl": "https://github.com/bptworld/Hubitat", 4 | "payPalUrl": "https://paypal.me/bptworld", 5 | "packages": [ 6 | { 7 | "name": "Device Watchdog", 8 | "category": "Utilities", 9 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/devicewatchdog/packageManifest.json", 10 | "description": "Keep an eye on your devices and see how long it's been since they checked in.", 11 | "tags": [ "Tools & Utilities" ] 12 | }, 13 | { 14 | "name": "Flow Engine", 15 | "category": "Utilities", 16 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/FlowEngine/packageManifest.json", 17 | "description": "Visual drag and drop rules engine.", 18 | "tags": [ "Tools & Utilities" ] 19 | }, 20 | { 21 | "name": "The Flasher", 22 | "category": "Utilities", 23 | "location": "https://raw.githubusercontent.com/bptworld/Hubitat/master/apps/TheFlasher/packageManifest.json", 24 | "description": "Flash your lights based on several triggers!", 25 | "tags": [ "Tools & Utilities" ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | Stuff used with BPTWorld Apps and Drivers

3 | Please vist my Docs section for Install and other information! 4 |

5 | Thanks,
6 | Bryan
7 | @BPTWorld 8 | -------------------------------------------------------------------------------- /resources/images/L360-URL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/L360-URL.png -------------------------------------------------------------------------------- /resources/images/L360-URL2a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/L360-URL2a.png -------------------------------------------------------------------------------- /resources/images/L360-XMLError2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/L360-XMLError2.png -------------------------------------------------------------------------------- /resources/images/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/blank.png -------------------------------------------------------------------------------- /resources/images/button-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/button-green.png -------------------------------------------------------------------------------- /resources/images/button-power-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/button-power-green.png -------------------------------------------------------------------------------- /resources/images/button-power-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/button-power-red.png -------------------------------------------------------------------------------- /resources/images/button-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/button-red.png -------------------------------------------------------------------------------- /resources/images/checkMarkGreen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/checkMarkGreen2.png -------------------------------------------------------------------------------- /resources/images/cogWithWrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/cogWithWrench.png -------------------------------------------------------------------------------- /resources/images/door-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/door-closed.png -------------------------------------------------------------------------------- /resources/images/door-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/door-open.png -------------------------------------------------------------------------------- /resources/images/image-Bryan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/image-Bryan.png -------------------------------------------------------------------------------- /resources/images/instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/instructions.png -------------------------------------------------------------------------------- /resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/logo.png -------------------------------------------------------------------------------- /resources/images/options-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/options-green.png -------------------------------------------------------------------------------- /resources/images/options-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/options-red.png -------------------------------------------------------------------------------- /resources/images/pp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/pp.png -------------------------------------------------------------------------------- /resources/images/pp2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/pp2.jpg -------------------------------------------------------------------------------- /resources/images/question-mark-icon-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/question-mark-icon-2.jpg -------------------------------------------------------------------------------- /resources/images/question-mark-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/question-mark-icon.png -------------------------------------------------------------------------------- /resources/images/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/images/reports.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/reports.jpg -------------------------------------------------------------------------------- /resources/images/shield-checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/shield-checkmark.png -------------------------------------------------------------------------------- /resources/images/shield-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/images/shield-lock.png -------------------------------------------------------------------------------- /resources/media/doorclose1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/media/doorclose1.mp3 -------------------------------------------------------------------------------- /resources/media/dooropen1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/media/dooropen1.mp3 -------------------------------------------------------------------------------- /resources/media/fastpops1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/media/fastpops1.mp3 -------------------------------------------------------------------------------- /resources/media/pup1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptworld/Hubitat/0ef6af971b7260ea936eb4dca21ac131efc82e46/resources/media/pup1.mp3 -------------------------------------------------------------------------------- /resources/media/readme.txt: -------------------------------------------------------------------------------- 1 | . 2 | --------------------------------------------------------------------------------