├── .gitignore ├── README.md ├── apps └── home_presence_app │ ├── home_presence_app.example.yaml │ ├── home_presence_app.py │ └── requirements.txt ├── hacs.json ├── installer ├── README.md ├── appdaemon.yaml ├── appdaemon@appdaemon.service ├── apps.yaml ├── install.sh ├── install_ad.sh ├── install_ad_part2.sh ├── install_ma_only.sh ├── screenshot_installer.JPG ├── update_ad_ma.sh ├── update_ad_ma_part2.sh └── update_ma.sh └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monitor-Appdaemon-App 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) 4 | 5 | Buy Me A Coffee 6 | 7 | Appdaemon App for [Andrew's Monitor Presence Detection System](https://github.com/andrewjfreyer/monitor). 8 | 9 | The Monitor Presence Detection system, is a Bash script `monitor.sh` created by [Andrew Freyer](https://github.com/andrewjfreyer), which is designed to run on multiple Linux systems like the Raspberry Pi around the home, to detect if persons are near or not. It is designed to work with 1 or more scripts installed on 1 or more computers (like Raspberry Pi) referred to here as nodes, to detect presence. The node uses the onboard Bluetooth adapter to detect Bluetooth devices (phone/watch/beacon/etc) is near and then reports the state from the device on a person (near or not) to a MQTT Broker. More details about the script, how it functions and setup can be found by following this [link](https://github.com/andrewjfreyer/monitor). 10 | 11 | This App is designed to maximise the use of the detection system, so that the user can easily have it integrated into their automation system comprising of Home Assistant (HA) and AppDaemon (AD), with as less effort as possible no matter the number of users or nodes in place. This app when added to an Appdaemon instance, will help to auto generate entities for presence detection based on the data reported by each node in HA, no matter the number of nodes running in that location or Bluetooth devices to be detected. Added to this, for those that have no Appdaemon running to use this app, this repository also includes a script to easily install both AppDaemona and the Monitor-App in a Linux computer. - contributed by [TheStigh](https://github.com/TheStigh) 12 | 13 | ## Features 14 | - Generates sensors in Home Assistant (HA) and AppDaemon (AD) for the following 15 | - Sensors of the Confidence levels for each Bluetooth device like phone/watch/beacon etc based on each node in each location. So if you have 3 presence nodes, each known device will have 3 confidence sensors with the names sensor._location_conf. in AD it is ._location in the `mqtt` namespace 16 | - Binary Sensors for each device. So no matter the number of location sensors you have, only one is generated and this is a presence sensor. The sensor entity_id will be binary_sensor.. So if one has an entry in the known_static_address as xx:xx:xx:xx:xx:xx odianosen's iphone it will generate `binary_sensor.monitor_odianosens_iphone_s` 17 | - If wanting to use `device_trackers`, it is possible to config the app to use `device_tracker` instead of `binary_sensors` for each device. The app will update the state as required; that is use `home`/`not_home` instead of `on`/`off`. - contributed by [shbatm](https://github.com/shbatm) 18 | - Binary sensors for when everyone is in `binary_sensor.everyone_home`, when everyone is out `binary_sensor.everyone_not_home`. These sensors are set to ON or OFF depending on declared users in the apps.yaml file users_sensors are in or out. If some are in and some out, both will be OFF, but another sensor `binary_sensor.somebody_is_home` can be used. This is handy for other automation rules. 19 | - The name of the sensors for `everyone_home`, `everyone_not_home` and `somebody_is_home` can be modified to use other names as required. - contributed by [shbatm](https://github.com/shbatm) 20 | - If a device is seen to be below the configured minimum confidence minimum_confidence level across all locations which defaults to 50, a configurable not_home_timeout is ran before declaring the user device is not home in HA using the binary sensor generated for that device. 21 | - When one of the declared gateway_sensors in the apps.yaml is opened, based on who is in the house it will send a scan instruction to the monitor system. 22 | - When a gateway is opened for a long time, it is possible to set a time interval that instructs the app to carryout scans over a set interval. Useful if living within a space that has one of the gateways opened for a long time 23 | - Before sending the scan instruction, it first checks for if the system is busy scanning. With the new upgrade to monitor by Andrew, this is not really needed. But (though preferred) if the user was to activate `PREF_MQTT_REPORT_SCAN_MESSAGES` to `true` in preferences, it can still use it 24 | - If no gateway sensors are specified, it will send scan instructions every 1 minute. This negates the experience for quick detection, so it is highly recommended to make use of at least a single gateway sensor. 25 | - Ability to define the `known_devices` in a single place within AD, which is then loaded to all monitor nodes on the network. This can be useful, if having multiple nodes, and need to manage all `known_devices` from a single place, instead of having to change it in all nodes individually. 26 | - Cleans out old ``known_devices`` from the nodes, when they have been deleted from the ``known_devices`` setting. Do note this takes about 2 minutes after app initialialies to complete 27 | - Generates entities within AD, which has all the data published by the node per device, and can be listened to in other Apps for other automation reasons. For example `rssi` readings based on devices. 28 | - Constantly checks for all installed monitor nodes on the network, to ensure which is online. If any location doesn't respond after a set time `system_timeout`, it sets all entities generated from that location to `0`. This is very useful if for example, a node reported a device confidence of `100`, then it went down. The device will stay at `100` even if the user had left the house, which will lead to wrong state. 29 | - Reporting of the state of the entire monitor system, including all nodes state to a MQTT topic. The topic is `monitor/state` 30 | - Reporting of the state of each node's state to a MQTT topic. The topic is `monitor//state` 31 | - Requests all devices update from the nodes on the network on a system restart 32 | - Determines the closest monitor node in an area with more than one, and adds that to the generated user binary sensor. - contributed by [shbatm](https://github.com/shbatm) 33 | - Supports the use of external MQTT command to instruct the app to executes some tasks like `arrive` scan or hardware reboot. - contributed by [shbatm](https://github.com/shbatm) 34 | - Supports the use of multi-level topics for the monitor topic like `hass/monitor` instead of just `monitor`. - contributed by [shbatm](https://github.com/shbatm) 35 | - Has the ability to hardware reboot remote monitor nodes, as its known that after a while the Pi script is running (node) on can get locked and the script doesn't work as efficiently anymore. So instead of simply restarting the script, the app can be set to reboot the hardware itself. This can also be done via mqtt by sending an empty payload to `monitor//reboot`. More explanation below 36 | - Has service calls within AD only, that allows a user to execute its functions from other AD apps 37 | - Use motion sensors to update Received Signal Strength Indication (RSSI) values in the home, so when users move the `nearest_monitor` can be updated 38 | - Can schedule a restart of the entire Monitor system at a scheduled time during certain days in the week via the `scheduled_restart` configuration 39 | - Supports the ability to have the node restarted, if the node is reported to be offline. This will only take place if `auto_reboot_when_offline` is `True` 40 | 41 | 42 | Requirements 43 | -------------------------------------------------------------------------- 44 | - [Home Assistant](https://www.home-assistant.io/getting-started/) 45 | - [MQTT Broker](https://www.home-assistant.io/docs/mqtt/broker/) Mosquitto MQTT broker add-on from Add-on-Store works out of the box 46 | - [Appdaemon](https://appdaemon.readthedocs.io/en/latest/INSTALL.html) >= 4.0 running (of course :roll_eyes:). You can install AppDaemon addon from the Add-on-store. Make sure to also [enable MQTT plugin in Appdaemon](https://appdaemon.readthedocs.io/en/latest/CONFIGURE.html#configuration-of-the-mqtt-plugin). A simple AppDaemon plugin configuration sufficient for this app, in the `appdaemon.yaml` file is seen below. It is important set the `client_topics` to ``NONE``, if not using the plugin for other app in AppDaemon 47 | ```yaml 48 | plugins: 49 | HASS: 50 | type: hass 51 | 52 | MQTT: 53 | type: mqtt 54 | namespace: mqtt 55 | client_host: Broker IP Address or DNS 56 | client_user: username 57 | client_password: password 58 | client_topics: 59 | - NONE 60 | ``` 61 | - [Andrew's Monitor](https://github.com/andrewjfreyer/monitor) running on the network. 62 | - Have at least a single main node, which runs as `monitor.sh -tdr -a -b` in a location that users stay more often in line with @andrewjfreyer example setup. If having more than 1 monitor, have the rest run as `monitor.sh -tad -a -b` so they only scan on trigger for both arrival and departure. 63 | - Don't worry about adding known_add `known_static_addresses` or `known_beacon_addresses` as Monitor-App will handle all that for you 64 | - In the main node, have good spacing between scans, not only to avoid unnecessarily flooding your environment with scans but also allowing the app to take over scans intermittently. I have mine set at 120 secs throughout for now 65 | - Recommended: Have sensors at the entrances into the home which I termed `gateways`, whether it be doors or garages. Windows also for those that use it :wink: 66 | 67 | Installation 68 | -------------------------------------------------------------------------- 69 | - **Install using HACS**: (Easiest Way) by first enabling "Enable AppDaemon apps discovery & tracking" in the HACS options under integration. Then go into HACS > Automation and search for "Monitor-App" 70 | - **Configure Monitor-App**: HACS will install Monitor-App in /config/Appdaemon/apps/Monitor-App. Rename the `home_presence_app_example.yaml` to `home_presence_app.yaml` (or it will be overwritten during next update). Make your configuration changes. At the very minimum you will need to update the following: 71 | - known_devices (these will be synced with all your nodes) 72 | - remote_monitors (add your Monitor's address) 73 | - **Restart AppDaemon to activate Monitor-App**. You can see AppDaemon's logs to see the startup process. NOTE: not all of created Monitor-App sensors (like monitor.xxx) are used in HA so some warnings are ok here. 74 | - If everything is working properly you should now see new `binary_sensors` (binary_sensor.monitor_xxxx) show up for each `known_device` that you created. 75 | 76 | 77 | ## Alternative Installation Methods (Without HACS): 78 | - **Download Repository**: You can simply download the repository and copy the `home_presence_app` folder, and place it into your AD's `apps` folder. Make the required changes in the `home_presence_app.yaml` file, and AD will automatically pickup the app for instanciation. 79 | - **Use an installation script**: 80 | - If AppDaemon is not installed in the PC to run this app, execute in a commandline 81 | ``` 82 | bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/install_ad.sh)" 83 | ``` 84 | The script will install AppDaemon and this App alongside. Then make the required changes, as required. Please read more about the [AD install script here](https://github.com/Odianosen25/Monitor-App/blob/master/installer/README.md). - contributed by [TheStigh](https://github.com/TheStigh) 85 | 86 | 87 | 88 | 89 | When developing this app, 4 main things were my target: 90 | ------------------------------------------------------- 91 | 92 | - Ease of use: The user should only setup the monitor system (collection of nodes), and no matter the number of nodes involved, it should be up and running without virtually any or minimal extra work needed. The idea of editing the configuration.yaml file for sensors, automation and input_boolean as in the example to use this great system was almost a put off for me. And once one’s system grows, it exponentially takes more work to setup and debug :persevere:. 93 | - Scalability: No matter the number of users or gateways or nodes in place, whether its small like mine which is 3, 1 & 2 respectively or you have 30, 10 and 20 respectively (if possible), it should take virtually the same amount of work to be up and running when using this app :smirk: 94 | - Speed: To improve in speed, the app makes use of an internal feature, whereby the app instructs the system to carry out an arrival or departure scans based on if someone enters or leaves the house and if everyone home or not. This made possible without need of forcing the monitor system to scan more frequently and thereby reducing impact on WiFi and other wireless equipment :relieved: 95 | - Lastly and most especially Reliability: It was important false positives/negatives are eliminated in the way the system runs. So the app tries to build in some little time based buffers here and there :grimacing: 96 | 97 | ### Example Simple Configuration 98 | ```yaml 99 | home_presence_app: 100 | module: home_presence_app 101 | class: HomePresenceApp 102 | home_gateway_sensors: 103 | - binary_sensor.main_door_contact 104 | 105 | known_devices: 106 | - xx:xx:xx:xx:xx:xx Odianosen's iPhone 107 | - xx:xx:xx:xx:xx:xx Nkiruka's iPad 108 | ``` 109 | 110 | ### Example Advanced Configuration 111 | ```yaml 112 | home_presence_app: 113 | module: home_presence_app 114 | class: HomePresenceApp 115 | plugin: 116 | - HASS 117 | - MQTT 118 | #monitor_topic: presence 119 | #mqtt_event: MQTT 120 | #user_device_domain: device_tracker 121 | #everyone_not_home: everyone_not_home 122 | #everyone_home: everyone_home 123 | #somebody_is_home: somebody_is_home 124 | depart_check_time: 30 125 | depart_scans: 3 126 | minimum_confidence: 60 127 | not_home_timeout: 15 128 | system_check: 30 129 | system_timeout: 60 130 | home_gateway_sensors: 131 | - binary_sensor.main_door_contact 132 | 133 | gateway_scan_interval_delay: 180 # wait for 3 minutes 134 | gateway_scan_interval: 60 # if after 3 minutes gateway still opended, scan every 1 minute 135 | 136 | # reboot the all nodes at 12 midnight on Mondays and Thursdays 137 | scheduled_restart: 138 | time: 00:00:01 139 | days: 140 | - mon 141 | - thu 142 | location: all 143 | 144 | # other location configuration options 145 | #location: living_room, kitchen 146 | 147 | #location: 148 | # - living_room 149 | # - kitchen 150 | 151 | home_motion_sensors: 152 | - binary_sensor.living_room_motion_sensor_occupancy 153 | - binary_sensor.kitchen_motion_sensor_occupancy 154 | - binary_sensor.hallway_motion_sensor_occupancy 155 | 156 | #log_level: DEBUG 157 | known_devices: 158 | - xx:xx:xx:xx:xx:xx Odianosen's iPhone 159 | - xx:xx:xx:xx:xx:xx Nkiruka's iPad 160 | 161 | known_beacons: 162 | - xx:xx:xx:xx:xx:xx Odianosen's Car Keys 163 | 164 | remote_monitors: 165 | disable: False 166 | kitchen: 167 | auto_reboot_when_offline: True 168 | host: !secret kitchen_monitor_host 169 | username: !secret kitchen_monitor_username 170 | password: !secret kitchen_monitor_password 171 | 172 | living_room: 173 | host: 192.168.1.xxx 174 | username: !secret living_room_monitor_username 175 | password: !secret living_room_monitor_password 176 | reboot_command: sudo /sbin/reboot now 177 | auto_reboot_when_offline: True 178 | time: 02:00:01 179 | ``` 180 | 181 | ### App Configuration 182 | key | optional | type | default | description 183 | -- | -- | -- | -- | -- 184 | `module` | False | string | home_presence_app | The module name of the app. 185 | `class` | False | string | HomePresenceApp | The name of the Class. 186 | `plugin` | True | list | | The plugins at if restarted, the app should restart. 187 | `monitor_topic` | True | string | `monitor` | The top topic level used by the monitor system. This is also used as the domain for service call 188 | `mqtt_event` | True | string | `MQTT_MESSAGE` | The event name, used by the MQTT plugin to send data to the app. 189 | `user_device_domain` | True | string | `binary_sensor` | The domain to be used for the sensors generated by the app for each device. 190 | `everyone_home` | True | string | `everyone_home` | Binary sensor name to be used, to indicate everyone is home. 191 | `everyone_not_home` | True | string | `everyone_not_home` | Binary sensor name to be used, to indicate everyone is not home. 192 | `somebody_is_home` | True | string | `somebody_is_home` | Binary sensor name to be used, to indicate someone is home. 193 | `depart_check_time` | True | int | 30 | Delay in seconds, before depart scan is ran. This depends on how long it takes the user to leave the door and not being picked up by a monitor node. 194 | `depart_scans` | True | int | 3 | The number of times the depart scans should be ran. This useful for those that spend some time within areas the system can still pick them up, even though they have left the house. 195 | `minimum_confidence` | True | int | 50 | Minimum confidence required across all nodes, for a device to be considered departed. 196 | `not_home_timeout` | True | int | 15 | Time in seconds a device has to be considered away, before registering it deaprted by the app. 197 | `system_check`| True | int | 30 | Time in seconds, for the app to check the availability of each monitor node. 198 | `system_timeout`| True | int | 60 | Time in seconds, for a monitor node not to respond to system check for it to be considered offline. 199 | `scheduled_restart`| True | dict | | A dictionary specifing the `time` as `str` in `HH:MM:SS` format, first 3 letters of the `days` as a `list` and locations as `list` or `str` the app should restart the nodes on the network. If `remote_monitors` specified and `disabled` is not `True`, it will lead to a reboot of the node's hardware as specified in location. If no location is specified, it will only restart the script. 200 | `remote_monitors`| True | dict | | The names (locations), login details (`host`, `username` and `password`) optional `reboot_command` which defaults to `sudo reboot now` of the nodes to be rebooted. Also a parameter `auto_reboot_when_offline` can be added, which instructs the app if to reboot the node when offline, and what `time` to be auto rebooted. If `disable` is `True`, the app will not be able to reboot any nodes defined. 201 | `home_gateway_sensors`| True | list | | List of gateway sensors, which can be used by the app to instruct the nodes based on their state if to run a arrive/depart scan. If all home, only depart scan is ran. If all away, arrive scan is ran, and if neither both scans are ran. This accepts any kind of entity, and not limited to `binary_sensors` 202 | `gateway_scan_interval_delay`| None | int | | If the app is set to scan continously over a given time if any of the gateways are opened, this is used to set the time in seconds for it to wait, before carrying out the scans 203 | `gateway_scan_interval`| None | int | | This is used to instruct the app to keep running scans, while a gateway is opened. This can be useful if living in a space that keeps the door or something opened for a long time 204 | `home_motion_sensors`| True | list | | List of motion sensors, which can be used by the app to instruct the nodes based on their state if to run rssi scan. 205 | `known_devices`| True | list | | List of known devices that are to be loaded into all the nodes on the network 206 | `known_beacons`| True | list | | List of known beacons that data received from them by the app from the nodes, are to be processed by the app 207 | `log_level` | True | `'INFO'` | `'DEBUG'` | `'INFO'` | Switches log level. 208 | 209 | Service Calls: 210 | -------------- 211 | This app supports the use of some service calls, which can be useful if wanting to use execute some commands in the app from other AD apps. The domain of the service calls, depends on what is specified as the `monitor_topic`. An example service call is 212 | 213 | ```python 214 | self.call_service("monitor/remove_known_device", device="xx:xx:xx:xx:xx:xx", namespace=mqtt) 215 | ``` 216 | The domain is determined by the specified `monitor_topic`. Below is listed the supported service calls 217 | 218 | ### remove_known_device 219 | Used to remove a known device from all the nodes. The device's MAC address should be supplied in the service call 220 | 221 | ```python 222 | self.call_service("monitor/remove_known_device", device="xx:xx:xx:xx:xx:xx", namespace=mqtt) 223 | ``` 224 | 225 | ### run_arrive_scan 226 | Used to instruct the app to execute an arrival scan on all nodes 227 | 228 | ```python 229 | self.call_service("presence/run_arrive_scan", namespace=mqtt) 230 | ``` 231 | 232 | ### run_depart_scan 233 | Used to instruct the app to execute a depart scan on all nodes. If wanting to execute it immediately, pass a parameter `scan_delay=0` in the call. If not, the defined `depart_check_time` will be used as the delay before running the scan 234 | 235 | ```python 236 | # run depart scan in 10 seconds time 237 | self.call_service("presence/run_depart_scan", scan_delay=10, namespace=mqtt) 238 | ``` 239 | 240 | ### run_rssi_scan 241 | Used to instruct the app to execute an rssi scan on all nodes 242 | 243 | ```python 244 | self.call_service("monitor/run_rssi_scan", namespace=mqtt) 245 | ``` 246 | 247 | ### restart_device 248 | Used to instruct the app to execute a restart of the monitor script on all nodes. If a node has its login detail in `remote_monitors` it will attempt to reboot the hardware itself. To reboot a particular node in a location, specify the `location` parameter. This same location, should be used in defining the node's login details in `remote_monitors` 249 | 250 | ```python 251 | # restart the monitor scripts in all nodes 252 | self.call_service("monitor/restart_device", namespace=mqtt) 253 | 254 | # reboot the node in the living room's hardware 255 | self.call_service("monitor/restart_device", location="living_room", namespace=mqtt) 256 | ``` 257 | 258 | ### reload_device_state 259 | Used to instruct the app to have the nodes report the state of their devices 260 | 261 | ```python 262 | self.call_service("presence/reload_device_state", namespace=mqtt) 263 | ``` 264 | 265 | ### load_known_devices 266 | Used to instruct the app to have the nodes setup the known devices as specified in the app's configuration 267 | 268 | ```python 269 | self.call_service("presence/load_known_devices", namespace=mqtt) 270 | ``` 271 | 272 | ### clear_location_entities 273 | Used to instruct the app to set all entities in a predefined location to 0, indicating that no device is seen by that node. The `location` parameter must be specified 274 | 275 | ```python 276 | self.call_service("monitor/clear_location_entities", location="hallway", namespace=mqtt) 277 | ``` 278 | 279 | ### clean_devices 280 | Used to instruct the app to clean up old known devices. This is always ran at start-up, so technically should not be a need to be manually ran 281 | 282 | ```python 283 | self.call_service("monitor/clean_devices", namespace=mqtt) 284 | ``` 285 | 286 | MQTT Commands: 287 | -------------- 288 | This app supports the ability to send commands to it over MQTT. This can be very useful, if wanting to execute specific functions from an external system like HA or any hub that supports MQTT. Outline below are the supported MQTT topics and the payload commands: 289 | 290 | ### monitor/run_scan 291 | This topic is listened to by the app, and when a message is received it will execute the required command. Supported commands on this topic are as follows 292 | - `arrive`: This will run the arrive scan immediately 293 | - `depart`: This will run the depart scan immediaiely 294 | - `rssi`: This will run the rssi scan immediately 295 | 296 | ### monitor/location/reboot 297 | This topic is used by the app to reboot a remote monitor node. The `location` parmeter can be a any of the declared nodes in `remote_monitors`. So if wanting to say reboot only the living room's node, simply send an empty payload to `monitor/living_room/reboot`. if the location is `all`, that is an empty payload is sent to `monitor/all/reboot`, this will reboot all the declared remote_monitor nodes. 298 | 299 | 300 | RSSI Tracking: 301 | -------------- 302 | 303 | Within this app, RSSI tracking is also updated regularly on the AppDaemon based entities. I personally use this, for rudimentary home area tracking, aided with the use of motion sensors within the home. To use this feature, it is advised that all monitor systems are setup as `monitor.sh -tad -a -b` and the `PREF_MQTT_REPORT_SCAN_MESSAGES` should be set to `true` in preferences. I also found using this `rssi` scans only based on motion sensors, does help in keeping my systems reported state updated, with as minimal scans as possible; for example no need scanning at night, when all are sleeping. I am not advising the get motion sensors for this, but in my home I already had motion sensors for lights. So felt I may as well integrate it to improve on reliability. 304 | 305 | Hardware Rebooting (WARNING): 306 | ----------------------------- 307 | 308 | This is a feature which allows the app to remotely reboot a node's hardware, and not just the script it is running. It must be noted to make use of this, an external python package in the `requirements.txt` file most be installed. If using `Hass.io`, do add it to your `python_pakages` list in the config. If running on a standalone Linux system and not using the supplied script above, simply run `pip3 install -r requirements.txt` should install it; depending on which user is running AD. Care should be taken when using this feature, as any device with its details specified within the `remote_monitors` can be rebooted by the app. The hardware within which this app is running, should never be added to the list. Below is listed the conditions that can lead to a hardware reboot: 309 | - When a `restart_device` service call is made with the location, the app will also attempt to reboot the hardware 310 | - When a MQTT message is sent, to the reboot topic 311 | - When using `scheduled_restart`, it is advisable not to also use `auto_reboot_when_offline` at the same time. Or vis-a-Vis 312 | - If wanting to use both, it is advsiable to use a larger `system_check_timeout`, to ensure the node doesn't get rebooted twice at the same time. 313 | - When `auto_reboot_when_offline` is set to `True`, and the node is reported to be `offline`. If having network issues, its advisable to give a larger `system_check_timeout` to ensure its not rebooting too often. 314 | 315 | It is advisable not to use 316 | 317 | Buy Me A Coffee 318 | -------------------------------------------------------------------------------- /apps/home_presence_app/home_presence_app.example.yaml: -------------------------------------------------------------------------------- 1 | home_presence_app: 2 | module: home_presence_app 3 | class: HomePresenceApp 4 | plugin: 5 | - HASS 6 | - MQTT 7 | #monitor_topic: presence 8 | #mqtt_event: MQTT 9 | #user_device_domain: device_tracker 10 | #everyone_not_home: everyone_not_home 11 | #everyone_home: everyone_home 12 | #somebody_is_home: somebody_is_home 13 | depart_check_time: 30 14 | depart_scans: 3 15 | minimum_confidence: 60 16 | not_home_timeout: 15 17 | system_check: 30 18 | system_timeout: 60 19 | home_gateway_sensors: 20 | - binary_sensor.main_door_contact 21 | - cover.garage 22 | - zigbee2mqtt.contact.kithen_window 23 | 24 | # reboot the all nodes at 12 midnight on Mondays and Thursdays 25 | scheduled_restart: 26 | time: 00:00:01 27 | days: 28 | - mon 29 | - thu 30 | location: all 31 | 32 | # other location configuration options 33 | #location: living_room, kitchen 34 | 35 | #location: 36 | # - living_room 37 | # - kitchen 38 | 39 | home_motion_sensors: 40 | - zigbee2mqtt.occupancy.living_room_motion_sensor_occupancy 41 | - binary_sensor.kitchen_motion_sensor_occupancy 42 | - binary_sensor.hallway_motion_sensor_occupancy 43 | 44 | #log_level: DEBUG 45 | known_devices: 46 | - xx:xx:xx:xx:xx:xx Odianosen's iPhone 47 | - xx:xx:xx:xx:xx:xx Nkiruka's iPad 48 | 49 | known_beacons: 50 | - xx:xx:xx:xx:xx:xx Odianosen's Car Keys 51 | 52 | remote_monitors: 53 | disable: False 54 | kitchen: 55 | auto_reboot_when_offline: True 56 | host: !secret kitchen_monitor_host 57 | username: !secret kitchen_monitor_username 58 | password: !secret kitchen_monitor_password 59 | 60 | living_room: 61 | host: 192.168.1.xxx 62 | username: !secret living_room_monitor_username 63 | password: !secret living_room_monitor_password 64 | reboot_command: sudo /sbin/reboot now 65 | auto_reboot_when_offline: True 66 | time: 02:00:01 67 | -------------------------------------------------------------------------------- /apps/home_presence_app/home_presence_app.py: -------------------------------------------------------------------------------- 1 | """AppDaemon App For use with Monitor Bluetooth Presence Detection Script. 2 | 3 | apps.yaml parameters: 4 | | - monitor_topic (default 'monitor'): MQTT Topic monitor.sh script publishes to 5 | | - mqtt_event (default 'MQTT_MESSAGE'): MQTT event name as specified in the plugin setting 6 | | - not_home_timeout (default 30s): Time interval before declaring not home 7 | | - minimum_confidence (default 50): Minimum Confidence Level to consider home 8 | | - depart_check_time (default 30s): Time to wait before running depart scan 9 | | - system_timeout (default 90s): Time for system to report back from echo 10 | | - system_check (default 30s): Time interval for checking if system is online 11 | | - everyone_not_home: Name to use for the "Everyone Not Home" Sensor 12 | | - everyone_home: Name to use for the "Everyone Home" Sensor 13 | | - somebody_is_home: Name to use for the "Somebody Is Home" Sensor 14 | | - user_device_domain: Use "binary_sensor" or "device_tracker" domains. 15 | | - known_devices: Known devices to be added to each monitor. 16 | | - known_beacons: Known Beacons to monitor. 17 | | - remote_monitors: login details of remote monitors that can be hardware rebooted 18 | """ 19 | import json 20 | import adbase as ad 21 | import copy 22 | from datetime import datetime, timedelta 23 | import traceback 24 | import re 25 | 26 | 27 | __VERSION__ = "2.4.2" 28 | IGNORED_ACTIONS = [ 29 | "depart", 30 | "arrive", 31 | "state", 32 | "known device states", 33 | "add static device", 34 | "delete static device", 35 | ] 36 | 37 | # pylint: disable=attribute-defined-outside-init,unused-argument 38 | class HomePresenceApp(ad.ADBase): 39 | """Home Precence App Main Class.""" 40 | 41 | def initialize(self): 42 | """Initialize AppDaemon App.""" 43 | self.adapi = self.get_ad_api() 44 | self.hass = self.get_plugin_api("HASS") 45 | self.mqtt = self.get_plugin_api("MQTT") 46 | 47 | self.monitor_topic = self.args.get("monitor_topic", "monitor") 48 | self.user_device_domain = self.args.get("user_device_domain", "binary_sensor") 49 | 50 | # State string to use depends on which domain is in use. 51 | self.state_true = "on" if self.user_device_domain == "binary_sensor" else "home" 52 | self.state_false = ( 53 | "off" if self.user_device_domain == "binary_sensor" else "not_home" 54 | ) 55 | 56 | # Setup dictionary of known beacons in the format { mac_id: name }. 57 | self.known_beacons = { 58 | p[0]: p[1].lower() 59 | for p in (b.split(" ", 1) for b in self.args.get("known_beacons", [])) 60 | } 61 | 62 | # Setup dictionary of known devices in the format { mac_id: name }. 63 | self.known_devices = { 64 | p[0]: p[1].lower() 65 | for p in (b.split(" ", 1) for b in self.args.get("known_devices", [])) 66 | } 67 | 68 | # Support nested presence topics (e.g. "hass/monitor") 69 | self.topic_level = len(self.monitor_topic.split("/")) 70 | self.monitor_name = self.monitor_topic.split("/")[-1] 71 | 72 | self.timeout = self.args.get("not_home_timeout", 30) 73 | self.minimum_conf = self.args.get("minimum_confidence", 50) 74 | self.depart_check_time = self.args.get("depart_check_time", 30) 75 | self.system_timeout = self.args.get("system_timeout", 60) 76 | system_check = self.args.get("system_check", 30) 77 | 78 | self.all_users_sensors = list() 79 | self.not_home_timers = dict() 80 | self.location_timers = dict() 81 | self.confidence_handlers = dict() 82 | self.home_state_entities = dict() 83 | self.system_handle = dict() 84 | self.node_scheduled_reboot = dict() 85 | self.node_executing = dict() 86 | self.locations = set() 87 | 88 | # Create a sensor to keep track of if the monitor is busy or not. 89 | self.monitor_entity = f"{self.monitor_name}.monitor_state" 90 | 91 | self.mqtt.set_state( 92 | self.monitor_entity, 93 | state="idle", 94 | attributes={ 95 | "locations": [], 96 | "version": __VERSION__, 97 | "nodes": 0, 98 | "online_nodes": [], 99 | "offline_nodes": [], 100 | "friendly_name": "Monitor System State", 101 | }, 102 | replace=True, 103 | ) 104 | 105 | # Listen for requests to scan immediately. 106 | self.mqtt.listen_state(self.monitor_scan_now, self.monitor_entity, new="scan") 107 | 108 | # Listen for all changes to the monitor entity for MQTT forwarding 109 | self.mqtt.listen_state( 110 | self.forward_monitor_state, 111 | self.monitor_entity, 112 | attribute="all", 113 | immediate=True, 114 | ) 115 | 116 | self.monitor_handlers = {self.monitor_entity: None} 117 | 118 | # Setup the Everybody Home/Not Home Group Sensors 119 | self.setup_global_sensors() 120 | 121 | # Initialize our timer variables 122 | self.gateway_timer = None 123 | self.motion_timer = None 124 | self.check_home_timer = None 125 | 126 | # Setup home gateway sensors 127 | if self.args.get("home_gateway_sensors") is not None: 128 | 129 | for gateway_sensor in self.args["home_gateway_sensors"]: 130 | (namespace, sensor) = self.parse_sensor(gateway_sensor) 131 | self.adapi.listen_state( 132 | self.gateway_opened, sensor, namespace=namespace 133 | ) 134 | else: 135 | # no gateway sensors, do app has to run arrive and depart scans every 2 minutes 136 | self.adapi.log( 137 | "No Gateway Sensors specified, Monitor-APP will run Arrive and Depart Scan every 2 minutes. Please specify Gateway Sensors for a better experience", 138 | level="WARNING", 139 | ) 140 | self.adapi.run_every( 141 | self.run_arrive_scan, self.adapi.datetime() + timedelta(seconds=1), 60 142 | ) 143 | self.adapi.run_every( 144 | self.run_depart_scan, self.adapi.datetime() + timedelta(seconds=2), 60 145 | ) 146 | 147 | # Setup home motion sensors, used for RSSI tracking 148 | for motion_sensor in self.args.get("home_motion_sensors", []): 149 | (namespace, sensor) = self.parse_sensor(motion_sensor) 150 | self.adapi.listen_state(self.motion_detected, sensor, namespace=namespace) 151 | 152 | if self.args.get("scheduled_restart") is not None: 153 | kwargs = {} 154 | if "time" in self.args["scheduled_restart"]: 155 | time = self.args["scheduled_restart"]["time"] 156 | 157 | if "days" in self.args["scheduled_restart"]: 158 | kwargs["constrain_days"] = ",".join( 159 | self.args["scheduled_restart"]["days"] 160 | ) 161 | 162 | if "location" in self.args["scheduled_restart"]: 163 | kwargs["location"] = self.args["scheduled_restart"]["location"] 164 | 165 | self.adapi.log("Setting up Monitor auto reboot") 166 | self.adapi.run_daily(self.restart_device, time, **kwargs) 167 | 168 | else: 169 | self.adapi.log( 170 | "Will not be setting up auto reboot, as no time specified", 171 | level="WARNING", 172 | ) 173 | 174 | # Setup the system checks. 175 | if self.system_timeout > system_check: 176 | topic = f"{self.monitor_topic}/echo" 177 | self.adapi.run_every( 178 | self.send_mqtt_message, 179 | self.adapi.datetime() + timedelta(seconds=1), 180 | system_check, 181 | topic=topic, 182 | payload="", 183 | scan_type="System", 184 | ) 185 | else: 186 | self.adapi.log( 187 | "Cannot setup System Check due to System Timeout" 188 | " being Lower than System Check in Seconds", 189 | level="WARNING", 190 | ) 191 | 192 | # subscribe to the mqtt topic 193 | self.mqtt.mqtt_subscribe(f"{self.monitor_topic}/#") 194 | 195 | # Setup primary MQTT Listener for all presence messages. 196 | self.mqtt.listen_event( 197 | self.presence_message, 198 | self.args.get("mqtt_event", "MQTT_MESSAGE"), 199 | wildcard=f"{self.monitor_topic}/#", 200 | ) 201 | self.adapi.log(f"Listening on MQTT Topic {self.monitor_topic}", level="DEBUG") 202 | 203 | # Listen for any HASS restarts 204 | self.hass.listen_event(self.hass_restarted, "plugin_restarted") 205 | 206 | # Load the devices from the config. 207 | self.adapi.run_in(self.clean_devices, 0) # clean old devices first 208 | self.setup_service() # setup service 209 | 210 | # now this is to be ran, every hour to clean strayed location data 211 | self.adapi.run_every( 212 | self.run_location_clean, f"now+{self.system_timeout + 30}", 3600 213 | ) 214 | 215 | def setup_global_sensors(self): 216 | """Add all global home/not_home sensors.""" 217 | everyone_not_home = self.args.get("everyone_not_home", "everyone_not_home") 218 | self.everyone_not_home = f"binary_sensor.{everyone_not_home}" 219 | 220 | everyone_home = self.args.get("everyone_home", "everyone_home") 221 | self.everyone_home = f"binary_sensor.{everyone_home}" 222 | 223 | somebody_is_home = self.args.get("somebody_is_home", "somebody_is_home") 224 | self.somebody_is_home = f"binary_sensor.{somebody_is_home}" 225 | 226 | self.create_global_sensor(everyone_not_home) 227 | self.create_global_sensor(everyone_home) 228 | self.create_global_sensor(somebody_is_home) 229 | 230 | def create_global_sensor(self, sensor): 231 | """Create a global sensor in HASS if it does not exist.""" 232 | if self.hass.entity_exists(f"binary_sensor.{sensor}"): 233 | return 234 | 235 | self.adapi.log(f"Creating Binary Sensor for {sensor}", level="DEBUG") 236 | attributes = { 237 | "friendly_name": sensor.replace("_", " ").title(), 238 | "device_class": "presence", 239 | } 240 | 241 | self.hass.set_state( 242 | f"binary_sensor.{sensor}", state="off", attributes=attributes 243 | ) 244 | 245 | def presence_message(self, event_name, data, kwargs): 246 | """Process a message sent on the MQTT Topic.""" 247 | topic = data.get("topic") 248 | payload = data.get("payload") 249 | self.adapi.log(f"{topic} payload: {payload}", level="DEBUG") 250 | 251 | topic_path = topic.split("/") 252 | action = topic_path[-1].lower() 253 | 254 | # Process the payload as JSON if it is JSON 255 | payload_json = {} 256 | try: 257 | payload_json = json.loads(payload) 258 | except ValueError: 259 | pass 260 | 261 | # Handle request for immediate scan via MQTT 262 | # can be arrive/depart/rssi 263 | if action == "run_scan": 264 | # add scan_delay=0 to ensure its done immediately 265 | self.mqtt.call_service( 266 | f"{self.monitor_topic}/run_{payload.lower()}_scan", scan_delay=0 267 | ) 268 | return 269 | 270 | # Determine which scanner initiated the message 271 | location = None 272 | if isinstance(payload_json, dict) and "identity" in payload_json: 273 | location = payload_json.get("identity") 274 | 275 | elif len(topic_path) > self.topic_level + 1: 276 | location = topic_path[self.topic_level] 277 | 278 | if location in (None, "None", ""): 279 | # got an invalid location 280 | 281 | if action in IGNORED_ACTIONS + [ 282 | "echo" 283 | ]: # its echo, so recieved possibly from himself 284 | pass 285 | 286 | else: 287 | self.adapi.log( 288 | f"Got an invalid location {location}, from topic {topic}", 289 | level="WARNING", 290 | ) 291 | return 292 | 293 | location = location.replace(" ", "_").lower() 294 | location_friendly = location.replace("_", " ").title() 295 | 296 | # Presence System is Restarting 297 | if action == "restart": 298 | self.adapi.log("The Entire Presence System is Restarting") 299 | return 300 | 301 | # Miscellaneous Actions, Discard 302 | if action in IGNORED_ACTIONS: 303 | return 304 | 305 | # Status Message from the Presence System 306 | if action == "status": 307 | self.handle_status(location=location, payload=payload.lower()) 308 | return 309 | 310 | if action in ["start", "end"]: 311 | self.handle_scanning( 312 | action=action, 313 | location=location, 314 | scan_type=topic_path[self.topic_level + 1], 315 | ) 316 | return 317 | 318 | # Response to Echo Check of Scanner 319 | if action == "echo": 320 | self.handle_echo(location=location, payload=payload) 321 | return 322 | 323 | # Handle request for reboot of hardware 324 | if action == "reboot": 325 | self.adapi.run_in(self.restart_device, 1, location=location) 326 | return 327 | 328 | device_name = topic_path[self.topic_level + 1] 329 | # Handle Beacon Topics in MAC or iBeacon ID formats and make friendly. 330 | if device_name in list(self.known_beacons.keys()): 331 | device_name = self.known_beacons[device_name] 332 | else: 333 | device_name = device_name.replace(":", "_").replace("-", "_") 334 | 335 | device_entity_id = f"{self.monitor_name}_{device_name}" 336 | device_state_sensor = f"{self.user_device_domain}.{device_entity_id}" 337 | device_entity_prefix = f"{device_entity_id}_{location}" 338 | device_conf_sensor = f"sensor.{device_entity_prefix}_conf" 339 | device_local = f"{device_name}_{location}" 340 | appdaemon_entity = f"{self.monitor_name}.{device_local}" 341 | friendly_name = device_name.strip().replace("_", " ").title() 342 | 343 | # store the location 344 | self.locations.add(location) 345 | 346 | # RSSI Value for a Known Device: 347 | if action == "rssi": 348 | if topic == f"{self.monitor_topic}/scan/rssi" or payload == "": 349 | return 350 | 351 | attributes = { 352 | "rssi": payload, 353 | "last_reported_by": location.replace("_", " ").title(), 354 | } 355 | self.adapi.log( 356 | f"Recieved an RSSI of {payload} for {device_name} from {location_friendly}", 357 | level="DEBUG", 358 | ) 359 | 360 | if ( 361 | self.hass.entity_exists(device_conf_sensor) 362 | and self.hass.get_state(device_state_sensor, copy=False) 363 | == self.state_true 364 | ): 365 | # unless it exists, and the device is home don't update RSSI 366 | self.mqtt.set_state(appdaemon_entity, attributes=attributes) 367 | self.update_hass_sensor(device_conf_sensor, new_attr={"rssi": payload}) 368 | self.update_nearest_monitor(device_name) 369 | return 370 | 371 | # Ignore invalid JSON responses 372 | if not payload_json: 373 | return 374 | 375 | # Ignore unknown/bad types and unknown beacons 376 | if payload_json.get("type") not in [ 377 | "KNOWN_MAC", 378 | "GENERIC_BEACON", 379 | ] and payload_json.get("id") not in list(self.known_beacons.keys()): 380 | self.adapi.log( 381 | f"Ignoring Beacon {payload_json.get('id')} because it is not in the known_beacons list.", 382 | level="DEBUG", 383 | ) 384 | return 385 | 386 | # Clean-up names now that we have proper JSON payload available. 387 | payload_json["friendly_name"] = f"{friendly_name} {location_friendly}" 388 | if "name" in payload_json: 389 | payload_json["name"] = payload_json["name"].strip().title() 390 | 391 | # Get the confidence value from the payload 392 | confidence = int(float(payload_json.get("confidence", "0"))) 393 | del payload_json["confidence"] 394 | 395 | state = self.state_true if confidence >= self.minimum_conf else self.state_false 396 | 397 | if not self.hass.entity_exists(device_conf_sensor): 398 | # Entity does not exist in HASS yet. 399 | self.adapi.log( 400 | "Creating sensor {!r} for Confidence".format(device_conf_sensor) 401 | ) 402 | self.hass.set_state( 403 | device_conf_sensor, 404 | state=confidence, 405 | attributes={ 406 | "friendly_name": f"{friendly_name} {location_friendly} Confidence", 407 | "unit_of_measurement": "%", 408 | }, 409 | ) 410 | 411 | if not self.hass.entity_exists(device_state_sensor): 412 | # Device Home Presence Sensor Doesn't Exist Yet in Hass so create it 413 | self.adapi.log( 414 | "Creating sensor {!r} for Home State".format(device_state_sensor), 415 | level="DEBUG", 416 | ) 417 | self.hass.set_state( 418 | device_state_sensor, 419 | state=state, 420 | attributes={ 421 | "friendly_name": f"{friendly_name} Home", 422 | "type": payload_json.get("type", "UNKNOWN_TYPE"), 423 | "device_class": "presence", 424 | }, 425 | ) 426 | 427 | if not self.mqtt.entity_exists(device_state_sensor): 428 | # Device Home Presence Sensor Doesn't Exist Yet in default so create it 429 | self.adapi.log( 430 | "Creating sensor {!r} for Home State".format(device_state_sensor), 431 | level="DEBUG", 432 | ) 433 | self.mqtt.set_state( 434 | device_state_sensor, 435 | state=state, 436 | attributes={ 437 | "friendly_name": f"{friendly_name} Home", 438 | "type": payload_json.get("type", "UNKNOWN_TYPE"), 439 | "device_class": "presence", 440 | }, 441 | ) 442 | 443 | if device_entity_id not in self.home_state_entities: 444 | self.home_state_entities[device_entity_id] = list() 445 | 446 | # Add listeners to the conf sensors to update the main state sensor on change. 447 | if device_conf_sensor not in self.home_state_entities[device_entity_id]: 448 | self.home_state_entities[device_entity_id].append(device_conf_sensor) 449 | self.confidence_handlers[device_conf_sensor] = self.hass.listen_state( 450 | self.confidence_updated, 451 | device_conf_sensor, 452 | device_entity_id=device_entity_id, 453 | immediate=True, 454 | ) 455 | 456 | # Actually update the confidence sensor. 457 | payload_json["location"] = location 458 | self.update_hass_sensor(device_conf_sensor, confidence, new_attr=payload_json) 459 | self.mqtt.set_state(appdaemon_entity, state=confidence, attributes=payload_json) 460 | 461 | # Set the nearest monitor property if we have a new RSSI. 462 | if "rssi" in payload_json: 463 | self.update_nearest_monitor(device_name) 464 | 465 | if device_state_sensor not in self.all_users_sensors: 466 | self.all_users_sensors.append(device_state_sensor) 467 | 468 | # now listen to this sensor's state changes 469 | # used to check if the user was not home before, and if home run rssi immediately to determine closest monitor 470 | self.mqtt.listen_state( 471 | self.device_state_changed, 472 | device_state_sensor, 473 | device_name=device_name, 474 | immediate=True, 475 | ) 476 | 477 | if device_entity_id not in self.not_home_timers: 478 | self.not_home_timers[device_entity_id] = None 479 | 480 | def handle_status(self, location, payload): 481 | """Handle a status message from the presence system.""" 482 | location_friendly = location.replace("_", " ").title() 483 | self.adapi.log( 484 | f"The {location_friendly} Presence System is {payload.title()}.", 485 | level="DEBUG", 486 | ) 487 | 488 | if payload == "offline": 489 | # Location Offline, Run Timer to Clear All Entities 490 | if location in self.location_timers and self.adapi.timer_running( 491 | self.location_timers[location] 492 | ): 493 | self.adapi.cancel_timer(self.location_timers[location]) 494 | 495 | self.location_timers[location] = self.adapi.run_in( 496 | self.clear_location_entities, self.system_timeout, location=location 497 | ) 498 | 499 | elif ( 500 | payload == "online" 501 | and location in self.location_timers 502 | and self.adapi.timer_running(self.location_timers[location]) 503 | ): 504 | # Location back online. Cancel any timers. 505 | self.adapi.cancel_timer(self.location_timers[location]) 506 | 507 | self.handle_nodes_state(location, payload) 508 | 509 | entity_id = f"{self.monitor_name}.{location}_state" 510 | attributes = {} 511 | 512 | if ( 513 | not self.mqtt.entity_exists(entity_id) 514 | or self.mqtt.get_state(entity_id, attribute="friendly_name", copy=False) 515 | is None 516 | ): 517 | attributes.update( 518 | { 519 | "friendly_name": f"{location_friendly} State", 520 | "last_rebooted": "", 521 | "location": location_friendly, 522 | } 523 | ) 524 | # Load devices for all locations: 525 | self.adapi.run_in(self.load_known_devices, 30) 526 | 527 | self.mqtt.set_state(entity_id, state=payload, attributes=attributes) 528 | 529 | if self.system_handle.get(entity_id) is None: 530 | self.system_handle[entity_id] = self.mqtt.listen_state( 531 | self.node_state_changed, entity_id 532 | ) 533 | 534 | # Listen for all changes to the node's entity for MQTT forwarding 535 | self.mqtt.listen_state( 536 | self.forward_monitor_state, entity_id, attribute="all", immediate=True, 537 | ) 538 | 539 | def handle_scanning(self, action, location, scan_type): 540 | """Handle a Monitor location starting or stopping a scan.""" 541 | old_state = self.mqtt.get_state(self.monitor_entity, copy=False) 542 | locations_attr = self.mqtt.get_state(self.monitor_entity, attribute="locations") 543 | new_state = "scanning" if action == "start" else "idle" 544 | attributes = { 545 | "scan_type": scan_type, 546 | "locations": locations_attr, 547 | location: new_state, 548 | } 549 | 550 | if action == "start": 551 | self.adapi.log( 552 | f"The {location} presence system is scanning...", level="DEBUG" 553 | ) 554 | if old_state != "scanning": 555 | # Scanner was IDLE. Set it to SCANNING. 556 | attributes["locations"] = [location] 557 | elif location not in locations_attr: 558 | attributes["locations"].append(location) 559 | # Scan has just finished. 560 | elif action == "end" and location in locations_attr: 561 | attributes["locations"].remove(location) 562 | 563 | last_one = old_state != new_state and not attributes.get("locations") 564 | 565 | self.mqtt.set_state( 566 | self.monitor_entity, 567 | state="scanning" if not last_one else "idle", 568 | attributes=attributes, 569 | ) 570 | 571 | def handle_echo(self, location, payload): 572 | """Handle an echo response from a scanner.""" 573 | self.adapi.log(f"Echo received from {location}: {payload}", level="DEBUG") 574 | if payload != "ok": 575 | return 576 | 577 | entity_id = f"{self.monitor_name}.{location}_state" 578 | if location in self.location_timers and self.adapi.timer_running( 579 | self.location_timers[location] 580 | ): 581 | self.adapi.cancel_timer(self.location_timers[location]) 582 | 583 | self.location_timers[location] = self.adapi.run_in( 584 | self.clear_location_entities, self.system_timeout, location=location 585 | ) 586 | 587 | if self.mqtt.get_state(entity_id, copy=False) != "online": 588 | self.mqtt.set_state(entity_id, state="online") 589 | 590 | self.handle_nodes_state(location, "online") 591 | 592 | def handle_nodes_state(self, location, state): 593 | """Used to handle the state of the nodes for reporting """ 594 | location_friendly = location.replace("_", " ").title() 595 | state = state.lower() 596 | 597 | attributes = self.mqtt.get_state(self.monitor_entity, attribute="all")[ 598 | "attributes" 599 | ] 600 | 601 | if state == "online": 602 | 603 | # update the online/offline nodes as needed 604 | if location_friendly not in attributes["online_nodes"]: 605 | attributes["online_nodes"].append(location_friendly) 606 | 607 | if location_friendly in attributes["offline_nodes"]: 608 | attributes["offline_nodes"].remove(location_friendly) 609 | 610 | elif state == "offline": 611 | 612 | # update the online/offline nodes as needed 613 | if location_friendly not in attributes["offline_nodes"]: 614 | attributes["offline_nodes"].append(location_friendly) 615 | 616 | if location_friendly in attributes["online_nodes"]: 617 | attributes["online_nodes"].remove(location_friendly) 618 | 619 | attributes["nodes"] = len(attributes["online_nodes"]) + len( 620 | attributes["offline_nodes"] 621 | ) 622 | 623 | self.mqtt.set_state(self.monitor_entity, attributes=attributes) 624 | 625 | def update_nearest_monitor(self, device_name): 626 | """Determine which monitor the device is closest to based on RSSI value.""" 627 | device_entity_id = f"{self.monitor_name}_{device_name}" 628 | device_conf_sensors = self.home_state_entities.get(device_entity_id) 629 | device_state_sensor = f"{self.user_device_domain}.{device_entity_id}" 630 | 631 | if device_conf_sensors is None: 632 | self.adapi.log( 633 | f"Got Confidence Value for {device_entity_id} but device" 634 | " is not set up (no sensors found).", 635 | level="WARNING", 636 | ) 637 | self.adapi.run_in(self.run_arrive_scan, 0) 638 | return 639 | 640 | rssi_values = { 641 | loc.replace(f"sensor.{device_entity_id}_", "").replace( 642 | "_conf", "" 643 | ): self.hass.get_state(loc, attribute="rssi") 644 | for loc in device_conf_sensors 645 | } 646 | 647 | rssi_values = { 648 | loc: int(rssi) 649 | for loc, rssi in rssi_values.items() 650 | if rssi is not None and rssi != "unknown" 651 | } 652 | 653 | nearest_monitor = "unknown" 654 | if rssi_values: 655 | nearest_monitor = max(rssi_values, key=rssi_values.get) 656 | self.adapi.log( 657 | f"{device_entity_id} is closest to {nearest_monitor} based on last reported RSSI values", 658 | level="DEBUG", 659 | ) 660 | 661 | nearest_monitor = nearest_monitor.replace("_", " ").title() 662 | self.mqtt.set_state(device_state_sensor, nearest_monitor=nearest_monitor) 663 | self.update_hass_sensor( 664 | device_state_sensor, new_attr={"nearest_monitor": nearest_monitor}, 665 | ) 666 | 667 | def confidence_updated(self, entity, attribute, old, new, kwargs): 668 | """Respond to a monitor providing a new confidence value.""" 669 | device_entity_id = kwargs["device_entity_id"] 670 | device_state_sensor = f"{self.user_device_domain}.{device_entity_id}" 671 | device_state_sensor_value = self.hass.get_state(device_state_sensor, copy=False) 672 | device_type = self.hass.get_state(entity, attribute="type", copy=False) 673 | device_conf_sensors = self.home_state_entities.get(device_entity_id) 674 | 675 | if device_conf_sensors is None: 676 | self.adapi.log( 677 | f"Got Confidence Value for {device_entity_id} but device" 678 | " is not set up (no sensors found).", 679 | level="WARNING", 680 | ) 681 | 682 | self.adapi.run_in(self.run_arrive_scan, 0) 683 | return 684 | 685 | if int(new) == 0: # the confidence is 0, so rssi should be lower 686 | # unknown used just to ensure it doesn't clash with an active node 687 | appdaemon_conf_sensor = self.hass_conf_sensor_to_appdaemon_conf(entity) 688 | self.mqtt.set_state(appdaemon_conf_sensor, rssi="unknown") 689 | self.update_hass_sensor(entity, new_attr={"rssi": "unknown"}) 690 | 691 | sensor_res = list( 692 | map(lambda x: self.hass.get_state(x, copy=False), device_conf_sensors) 693 | ) 694 | sensor_res = [i for i in sensor_res if i is not None and i != "unknown"] 695 | 696 | self.adapi.log( 697 | "Device State: {}, User Device Sensor: {}, Device Type {}, New: {}, State: {}".format( 698 | device_entity_id, 699 | device_state_sensor, 700 | device_type, 701 | new, 702 | device_state_sensor_value, 703 | ), 704 | level="DEBUG", 705 | ) 706 | 707 | if sensor_res != [] and any( 708 | list(map(lambda x: int(x) >= self.minimum_conf, sensor_res)) 709 | ): 710 | # Cancel the running timer. 711 | if self.not_home_timers.get( 712 | device_entity_id 713 | ) is not None and self.adapi.timer_running( 714 | self.not_home_timers[device_entity_id] 715 | ): 716 | self.adapi.cancel_timer(self.not_home_timers[device_entity_id]) 717 | self.not_home_timers[device_entity_id] = None 718 | 719 | # update binary sensors for user 720 | self.mqtt.set_state(device_state_sensor, state=self.state_true) 721 | self.update_hass_sensor(device_state_sensor, self.state_true) 722 | 723 | # now check how many ppl are home 724 | count = self.count_persons_in_home() 725 | self.update_hass_sensor( 726 | self.somebody_is_home, "on", new_attr={"count": count} 727 | ) 728 | 729 | if device_state_sensor in self.all_users_sensors: 730 | self.update_hass_sensor(self.everyone_not_home, "off") 731 | if self.check_home_timer is not None and self.adapi.timer_running( 732 | self.check_home_timer 733 | ): 734 | self.adapi.cancel_timer(self.check_home_timer) 735 | 736 | self.check_home_timer = self.adapi.run_in( 737 | self.check_home_state, 2, check_state="is_home" 738 | ) 739 | return 740 | 741 | if ( 742 | self.not_home_timers.get(device_entity_id) is None 743 | and device_state_sensor_value not in ["off", "not_home"] 744 | and int(new) == 0 745 | ): 746 | # if "BEACON" not in str(device_type): 747 | # Run another scan before declaring the user away as extra 748 | # check within the timeout time if this isn't a beacon 749 | self.adapi.run_in(self.run_arrive_scan, 0) 750 | 751 | self.not_home_timers[device_entity_id] = self.adapi.run_in( 752 | self.not_home_func, self.timeout, device_entity_id=device_entity_id 753 | ) 754 | self.adapi.log(f"Timer Started for {device_entity_id}", level="DEBUG") 755 | 756 | def device_state_changed(self, entity, attribute, old, new, kwargs): 757 | """Used to run RSSI scan in the event the device Left the house and re-entered""" 758 | 759 | device_name = kwargs["device_name"] 760 | device_entity_id = f"{self.monitor_name}_{device_name}" 761 | if new == self.state_true: # device now home 762 | self.adapi.run_in(self.run_rssi_scan, 0) 763 | 764 | elif new == self.state_false: # device is away 765 | device_conf_sensors = self.home_state_entities[device_entity_id] 766 | # now set all of their sensor's rssi to unknown to indicate its way 767 | for sensor in device_conf_sensors: 768 | location = self.hass.get_state(sensor, attribute="location", copy=False) 769 | device_local = f"{device_name}_{location}" 770 | appdaemon_entity = f"{self.monitor_name}.{device_local}" 771 | self.mqtt.set_state(appdaemon_entity, rssi="unknown") 772 | self.update_hass_sensor(sensor, new_attr={"rssi": "unknown"}) 773 | 774 | def not_home_func(self, kwargs): 775 | """Manage devices that are not home.""" 776 | device_entity_id = kwargs["device_entity_id"] 777 | 778 | # remove from dictionary 779 | self.not_home_timers.pop(device_entity_id, None) 780 | 781 | device_state_sensor = f"{self.user_device_domain}.{device_entity_id}" 782 | device_conf_sensors = self.home_state_entities[device_entity_id] 783 | sensor_res = list( 784 | map(lambda x: self.hass.get_state(x, copy=False), device_conf_sensors) 785 | ) 786 | 787 | # Remove unknown values from list 788 | sensor_res = [i for i in sensor_res if i is not None and i != "unknown"] 789 | 790 | self.adapi.log( 791 | f"Device Not Home: {device_entity_id}, Sensors: {sensor_res}", level="DEBUG" 792 | ) 793 | 794 | if all(list(map(lambda x: int(x) < self.minimum_conf, sensor_res))): 795 | # Confirm for the last time 796 | self.mqtt.set_state( 797 | device_state_sensor, state=self.state_false, nearest_monitor="unknown" 798 | ) 799 | self.update_hass_sensor( 800 | device_state_sensor, self.state_false, {"nearest_monitor": "unknown"} 801 | ) 802 | 803 | if device_state_sensor in self.all_users_sensors: 804 | # At least someone not home, set Everyone Home to off 805 | self.update_hass_sensor(self.everyone_home, "off") 806 | 807 | if self.check_home_timer is not None and self.adapi.timer_running( 808 | self.check_home_timer 809 | ): 810 | self.adapi.cancel_timer(self.check_home_timer) 811 | 812 | self.check_home_timer = self.adapi.run_in( 813 | self.check_home_state, 2, check_state="not_home" 814 | ) 815 | 816 | self.not_home_timers[device_entity_id] = None 817 | 818 | def send_mqtt_message(self, kwargs): 819 | """Send a MQTT Message.""" 820 | topic = kwargs.get("topic") 821 | payload = kwargs.get("payload") 822 | if kwargs["scan_type"] == "Depart": 823 | count = kwargs.get("count", 0) 824 | # Last Gateway Based Timer 825 | self.gateway_timer = None 826 | 827 | if self.mqtt.get_state(self.monitor_entity) == "idle": 828 | self.mqtt.mqtt_publish(topic, payload) 829 | # Scan for departure times. 3 as default 830 | if count <= self.args.get("depart_scans", 3): 831 | count = count + 1 832 | self.adapi.run_in(self.run_depart_scan, 0, count=count) 833 | return 834 | # Scanner busy, re-run timer for it to get idle before 835 | # sending the message to start scan 836 | self.adapi.run_in(self.run_depart_scan, 0, scan_delay=10, count=count) 837 | return 838 | 839 | # Perform Arrival Scan 840 | if kwargs["scan_type"] == "Arrive": 841 | self.mqtt.mqtt_publish(topic, payload) 842 | return 843 | 844 | # System Command, Send the raw payload 845 | if kwargs["scan_type"] == "System": 846 | self.mqtt.mqtt_publish(topic, payload) 847 | return 848 | 849 | def update_hass_sensor(self, sensor, new_state=None, new_attr=None): 850 | """Update the hass sensor if it has changed.""" 851 | if not self.hass.entity_exists(sensor): 852 | self.adapi.log( 853 | f"Entity {sensor} does not exist, running arrival scan.", level="ERROR" 854 | ) 855 | self.adapi.run_in(self.run_arrive_scan, 0) 856 | return 857 | 858 | sensor_state = self.hass.get_state(sensor, attribute="all") 859 | state = sensor_state.get("state") 860 | attributes = sensor_state.get("attributes", {}) 861 | if new_state is None: 862 | update_needed = False 863 | new_state = state 864 | else: 865 | update_needed = state != new_state 866 | 867 | if isinstance(new_attr, dict): 868 | attributes.update(new_attr) 869 | update_needed = True 870 | 871 | if update_needed: 872 | self.adapi.log( 873 | f"__function__: Entity_ID: {sensor}, new_state: {new_state}", 874 | level="DEBUG", 875 | ) 876 | self.hass.set_state(sensor, state=new_state, attributes=attributes) 877 | 878 | def motion_detected(self, entity, attribute, old, new, kwargs): 879 | """Respond to motion detected somewhere in the house. 880 | 881 | This will attempt to check for where users are located. 882 | """ 883 | self.adapi.log(f"Motion Sensor {entity} now {new}", level="DEBUG") 884 | 885 | if self.motion_timer is not None and self.adapi.timer_running( 886 | self.motion_timer 887 | ): # a timer is running already 888 | self.adapi.cancel_timer(self.motion_timer) 889 | self.motion_timer = None 890 | """ 'duration' parameter could be used in listen_state. 891 | But need to use a single timer for all motion sensors, 892 | to avoid running the scan too many times""" 893 | self.motion_timer = self.adapi.run_in( 894 | self.run_rssi_scan, self.args.get("rssi_timeout", 60) 895 | ) 896 | 897 | def check_home_state(self, kwargs): 898 | """Check if a user is home based on multiple locations.""" 899 | 900 | self.check_home_timer = None 901 | check_state = kwargs["check_state"] 902 | user_res = list( 903 | map(lambda x: self.hass.get_state(x, copy=False), self.all_users_sensors) 904 | ) 905 | user_res = [i for i in user_res if i is not None and i != "unknown"] 906 | somebody_home = "on" 907 | 908 | if check_state == "is_home" and all( 909 | list(map(lambda x: x in ["on", "home"], user_res)) 910 | ): 911 | # Someone is home, check if everyone is home. 912 | self.update_hass_sensor(self.everyone_home, "on") 913 | elif check_state == "not_home" and all( 914 | list(map(lambda x: x in ["off", "not_home"], user_res)) 915 | ): 916 | # Someone is not home, see if anyone is still home. 917 | self.update_hass_sensor(self.everyone_not_home, "on") 918 | somebody_home = "off" 919 | 920 | count = self.count_persons_in_home() 921 | new_attr = {"count": count} 922 | self.update_hass_sensor(self.somebody_is_home, somebody_home, new_attr=new_attr) 923 | 924 | def reload_device_state(self, kwargs): 925 | """Get the latest states from the scanners.""" 926 | topic = f"{self.monitor_topic}/KNOWN DEVICE STATES" 927 | self.adapi.run_in( 928 | self.send_mqtt_message, 0, topic=topic, payload="", scan_type="System" 929 | ) 930 | 931 | def monitor_changed_state(self, entity, attribute, old, new, kwargs): 932 | """Respond to a monitor location changing state.""" 933 | scan = kwargs["scan"] 934 | topic = kwargs["topic"] 935 | payload = kwargs["payload"] 936 | self.adapi.run_in( 937 | self.send_mqtt_message, 1, topic=topic, payload=payload, scan_type="Arrive" 938 | ) # Send to scan for arrival of anyone 939 | self.adapi.cancel_listen_state(self.monitor_handlers[scan]) 940 | self.monitor_handlers[scan] = None 941 | 942 | def forward_monitor_state(self, entity, attribute, old, new, kwargs): 943 | """Respond to any changes in the monitor system or each node""" 944 | new_state = copy.deepcopy(new) 945 | data = new_state["attributes"] 946 | 947 | # clean the data 948 | data.pop("friendly_name") 949 | last_changed = new_state["last_changed"] 950 | state = new_state["state"] 951 | data.update({"last_changed": last_changed, "state": state}) 952 | 953 | if "location" not in data: # it belongs to the overall monitor system 954 | topic = f"{self.monitor_topic}/state" 955 | 956 | else: # it belongs to a node 957 | location = data["location"].lower().replace(" ", "_") 958 | topic = f"{self.monitor_topic}/{location}/state" 959 | 960 | self.mqtt.mqtt_publish(topic, json.dumps(data)) 961 | 962 | def gateway_opened(self, entity, attribute, old, new, kwargs): 963 | """Respond to a gateway device opening or closing.""" 964 | self.adapi.log(f"Gateway Sensor {entity} now {new}", level="DEBUG") 965 | 966 | self.check_and_run_scans(new) 967 | 968 | def gateway_opened_timer(self, kwargs): 969 | """Ran at intervals depending on when the user has a gateway opened""" 970 | 971 | self.check_and_run_scans(**kwargs) 972 | 973 | def check_and_run_scans(self, state=None, **kwargs): 974 | """Check the state of the home and run the required scans""" 975 | 976 | true_states = ("on", "y", "yes", "true", "home", "opened", "unlocked", True) 977 | false_states = ("off", "n", "no", "false", "away", "closed", "locked", False) 978 | 979 | if state is None: 980 | # none sent, so its a timer and so need to get the data itself, what a drag 981 | 982 | states = [] 983 | for gateway_sensor in self.args.get("home_gateway_sensors", []): 984 | (namespace, sensor) = self.parse_sensor(gateway_sensor) 985 | states.append(self.adapi.get_state(x, copy=False, namespace=namespace)) 986 | 987 | # now check if any of them is opened 988 | for s in states: 989 | if s in true_states: 990 | state = s 991 | break 992 | 993 | if state not in (true_states + false_states): 994 | return 995 | 996 | if self.gateway_timer is not None and self.adapi.timer_running( 997 | self.gateway_timer 998 | ): 999 | # Cancel Existing Timer 1000 | self.adapi.cancel_timer(self.gateway_timer) 1001 | self.gateway_timer = None 1002 | 1003 | if self.hass.get_state(self.everyone_not_home, copy=False) == "on": 1004 | # No one at home 1005 | self.adapi.run_in(self.run_arrive_scan, 0) 1006 | 1007 | elif self.hass.get_state(self.everyone_home, copy=False) == "on": 1008 | # everyone at home 1009 | self.adapi.run_in(self.run_depart_scan, 0) 1010 | 1011 | else: 1012 | self.adapi.run_in(self.run_arrive_scan, 0) 1013 | self.adapi.run_in(self.run_depart_scan, 0) 1014 | 1015 | # now check if gateway opned and the user had declared a scan interval for gateway opened 1016 | if state in true_states and self.args.get("gateway_scan_interval"): 1017 | timer = int(self.args.get("gateway_scan_interval")) 1018 | first_time = kwargs.get("first_time", True) 1019 | # there is a scan interval so need to be worked on 1020 | # but first check if there is an initial one and it hasn't been ran 1021 | if first_time and self.args.get("gateway_scan_interval_delay"): 1022 | timer = int(self.args.get("gateway_scan_interval_delay")) 1023 | first_time = False 1024 | 1025 | self.adapi.run_in(self.gateway_opened_timer, timer, first_time=first_time) 1026 | 1027 | def run_arrive_scan(self, kwargs): 1028 | """Request an arrival scan. 1029 | 1030 | Will wait for the scanner to be free and then sends the message. 1031 | """ 1032 | topic = f"{self.monitor_topic}/scan/arrive" 1033 | payload = "" 1034 | if self.mqtt.get_state(self.monitor_entity, copy=False) == "idle": 1035 | self.mqtt.mqtt_publish(topic, payload) 1036 | return 1037 | 1038 | # Scanner busy. Wait for it to finish: 1039 | scan_type = self.mqtt.get_state( 1040 | self.monitor_entity, attribute="scan_type", copy=False 1041 | ) 1042 | if self.monitor_handlers.get("Arrive Scan") is None and scan_type != "arrival": 1043 | self.monitor_handlers["Arrive Scan"] = self.mqtt.listen_state( 1044 | self.monitor_changed_state, 1045 | self.monitor_entity, 1046 | new="idle", 1047 | old="scanning", 1048 | scan="Arrive Scan", 1049 | topic=topic, 1050 | payload=payload, 1051 | ) 1052 | 1053 | def run_depart_scan(self, kwargs): 1054 | """Request a departure scan. 1055 | 1056 | Will wait for the scanner to be free and then sends the message. 1057 | """ 1058 | delay = kwargs.get("scan_delay", self.depart_check_time) 1059 | count = kwargs.get("count", 1) 1060 | 1061 | topic = f"{self.monitor_topic}/scan/depart" 1062 | payload = "" 1063 | 1064 | # Cancel any timers 1065 | if self.gateway_timer is not None and self.adapi.timer_running( 1066 | self.gateway_timer 1067 | ): 1068 | self.adapi.cancel_timer(self.gateway_timer) 1069 | 1070 | # Scan for departure of anyone 1071 | self.gateway_timer = self.adapi.run_in( 1072 | self.send_mqtt_message, 1073 | delay, 1074 | topic=topic, 1075 | payload=payload, 1076 | scan_type="Depart", 1077 | count=count, 1078 | ) 1079 | 1080 | def run_rssi_scan(self, kwargs): 1081 | """Send a RSSI Scan Request.""" 1082 | topic = f"{self.monitor_topic}/scan/rssi" 1083 | payload = "" 1084 | self.mqtt.mqtt_publish(topic, payload) 1085 | self.motion_timer = None 1086 | 1087 | def restart_device(self, kwargs): 1088 | """Send a restart command to the monitor services.""" 1089 | topic = f"{self.monitor_topic}/scan/restart" 1090 | payload = "" 1091 | 1092 | location = kwargs.get("location") # meaning it needs a device to reboot 1093 | 1094 | if location is None: # no specific location specified 1095 | self.mqtt.mqtt_publish(topic, payload) 1096 | 1097 | elif ( 1098 | self.args.get("remote_monitors") is not None 1099 | and self.args["remote_monitors"].get("disable") is not True 1100 | ): 1101 | 1102 | if location == "all": # reboot everything 1103 | # get all locations 1104 | locations = list(self.args.get("remote_monitors", {}).keys()) 1105 | 1106 | elif isinstance(location, str): 1107 | locations = location.split(",") 1108 | 1109 | elif isinstance(location, list): 1110 | locations = location 1111 | 1112 | else: 1113 | self.adapi.log( 1114 | f"Location {location} not supported. So cannot run hardware reboot", 1115 | level="WARNING", 1116 | ) 1117 | 1118 | return 1119 | 1120 | for location in locations: 1121 | node = location.lower().strip().replace(" ", "_") 1122 | entity_id = f"{self.monitor_name}.{node}_state" 1123 | 1124 | if node not in self.args["remote_monitors"]: 1125 | self.adapi.log( 1126 | f"Node {node} not defined. So cannot reboot it", 1127 | level="WARNING", 1128 | ) 1129 | 1130 | continue 1131 | 1132 | if ( 1133 | self.node_scheduled_reboot.get(node) is not None 1134 | and kwargs.get("auto_rebooting") is True 1135 | ): 1136 | # it means this is from a scheduled reboot, so reset the handler 1137 | self.node_scheduled_reboot[node] = None 1138 | self.mqtt.set_state(entity_id, reboot_scheduled="off") 1139 | 1140 | try: 1141 | # use executor here, as sometimes due to being unable to process it 1142 | # as the node might be busy, could lead to AD hanging 1143 | 1144 | node_task = self.node_executing.get(node) 1145 | if node_task is None or node_task.done() or node_task.cancelled(): 1146 | # meaning its either not running, or had completed or cancelled 1147 | self.node_executing[node] = self.adapi.submit_to_executor( 1148 | self.restart_hardware, node 1149 | ) 1150 | 1151 | else: 1152 | self.adapi.log( 1153 | f"{location}'s node busy executing a command. So cannot execute this now", 1154 | level="WARNING", 1155 | ) 1156 | 1157 | except Exception as e: 1158 | self.adapi.error( 1159 | f"Could not restart {node}, due to {e}", level="ERROR" 1160 | ) 1161 | 1162 | def run_node_command(self, kwargs): 1163 | """Execute Command to be ran on the Node.""" 1164 | 1165 | location = kwargs.get("location") 1166 | cmd = kwargs.get("cmd") 1167 | 1168 | assert cmd is not None, "Command must be provided" 1169 | 1170 | # first get the required nodes 1171 | 1172 | if isinstance(location, str): 1173 | node = location.lower().replace(" ", "_") 1174 | 1175 | else: 1176 | node = location 1177 | 1178 | if node == "all": 1179 | nodes = list(self.args.get("remote_monitors", {}).keys()) 1180 | 1181 | elif isinstance(node, list): 1182 | nodes = location 1183 | 1184 | else: 1185 | nodes = [node] 1186 | 1187 | # now execute the command 1188 | for node in nodes: 1189 | if node not in self.args["remote_monitors"]: 1190 | self.adapi.log( 1191 | f"Node {node} not defined. So cannot reboot it", level="WARNING", 1192 | ) 1193 | 1194 | continue 1195 | 1196 | node_task = self.node_executing.get(node) 1197 | if node_task is None or node_task.done() or node_task.cancelled(): 1198 | # meaning its either not running, or had completed or cancelled 1199 | self.node_executing[node] = self.adapi.submit_to_executor( 1200 | self.execute_command, node, cmd 1201 | ) 1202 | 1203 | else: 1204 | self.adapi.log( 1205 | f"{location}'s node busy executing a command. So cannot execute this now", 1206 | level="WARNING", 1207 | ) 1208 | 1209 | def restart_hardware(self, node): 1210 | """Used to Restart the Hardware Monitor running in""" 1211 | 1212 | self.adapi.log(f"Restarting {node}'s Hardware") 1213 | 1214 | reboot_command = "sudo reboot now" 1215 | 1216 | if "reboot_command" in self.args["remote_monitors"][node]: 1217 | reboot_command = self.args["remote_monitors"][node]["reboot_command"] 1218 | 1219 | location = node.replace("_", " ").title() 1220 | try: 1221 | result = self.execute_command(node, reboot_command) 1222 | self.adapi.log( 1223 | f"{node}'s Hardware reset completed with result {result}", 1224 | level="DEBUG", 1225 | ) 1226 | 1227 | entity_id = f"{self.monitor_name}.{node}_state" 1228 | self.mqtt.set_state( 1229 | entity_id, 1230 | last_rebooted=self.adapi.datetime().replace(microsecond=0).isoformat(), 1231 | ) 1232 | 1233 | except Exception: 1234 | self.adapi.error(traceback.format_exc(), leve="ERROR") 1235 | self.adapi.error( 1236 | f"Could not restart {location} Monitor Hardware", level="ERROR", 1237 | ) 1238 | 1239 | def execute_command(self, node, cmd): 1240 | """Used to Run command on a Monitor Node""" 1241 | 1242 | self.adapi.log(f"Running {cmd} on {node}'s Hardware") 1243 | import paramiko 1244 | 1245 | # get the node's credentials 1246 | 1247 | if node not in self.args["remote_monitors"]: 1248 | raise ValueError(f"Given Node {node}, has no specified credentials") 1249 | 1250 | setting = self.args["remote_monitors"][node] 1251 | host = setting["host"] 1252 | username = setting["username"] 1253 | password = setting["password"] 1254 | 1255 | ssh = paramiko.SSHClient() 1256 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 1257 | ssh.connect( 1258 | host, 1259 | username=username, 1260 | password=password, 1261 | timeout=float(self.system_timeout), 1262 | ) 1263 | stdin, stdout, stderr = ssh.exec_command(cmd) 1264 | completed = stdout.readlines() 1265 | ssh.close() 1266 | 1267 | self.adapi.log(completed, level="DEBUG") 1268 | 1269 | # reset node task if completed 1270 | self.node_executing[node] = None 1271 | return completed 1272 | 1273 | def run_location_clean(self, kwargs): 1274 | """Check for if any location has data that had not been properly cleaned 1275 | and carry out some cleaning""" 1276 | 1277 | # first get all sensors, and lets start from there 1278 | monitor_sensors = list(self.mqtt.get_state(self.monitor_name).keys()) 1279 | 1280 | # next we go via the location data, and see if any location needs cleaning 1281 | for sensor in monitor_sensors: 1282 | if sensor == self.monitor_entity: 1283 | continue 1284 | 1285 | sens = list(filter(lambda l: re.search(l, sensor), self.locations)) 1286 | if len(sens) == 0: 1287 | # it means this sensor doesn't belong to a valid location 1288 | # so it needs to be removed 1289 | self.adbase.log(f"Removing sensor {sensor}", level="WARNING") 1290 | self.mqtt.remove_entity(sensor) 1291 | 1292 | def clear_location_entities(self, kwargs): 1293 | """Clear sensors from an offline location. 1294 | 1295 | This is used to retrieve the different sensors based on system 1296 | location, and set them to 0. This will ensure that if a location goes 1297 | down and the confidence is not 0, it doesn't stay that way, 1298 | and therefore lead to false info. 1299 | """ 1300 | location = kwargs["location"] 1301 | self.adapi.log( 1302 | "Processing System Unavailable for " + location.replace("_", " ").title() 1303 | ) 1304 | 1305 | # remove the handler from dict 1306 | self.location_timers.pop(location, None) 1307 | 1308 | for _, entity_list in self.home_state_entities.items(): 1309 | for sensor in entity_list: 1310 | if location in sensor: # that sensor belongs to that location 1311 | self.update_hass_sensor(sensor, 0) 1312 | appdaemon_conf_sensor = self.hass_conf_sensor_to_appdaemon_conf( 1313 | sensor 1314 | ) 1315 | # set to "unknown" since it had been cleared 1316 | self.mqtt.set_state(appdaemon_conf_sensor, state=0, rssi="unknown") 1317 | self.update_hass_sensor(sensor, new_attr={"rssi": "unknown"}) 1318 | 1319 | if location in self.location_timers: 1320 | self.location_timers.pop(location) 1321 | 1322 | entity_id = f"{self.monitor_name}.{location}_state" 1323 | self.mqtt.set_state(entity_id, state="offline") 1324 | 1325 | self.handle_nodes_state(location, "offline") 1326 | 1327 | if location in self.locations: 1328 | self.locations.remove(location) 1329 | 1330 | def hass_conf_sensor_to_appdaemon_conf(self, sensor): 1331 | """used to convert HASS confidence sensor to AD's""" 1332 | 1333 | device_entity_prefix = sensor.replace( 1334 | f"sensor.{self.monitor_name}_", "" 1335 | ).replace("_conf", "") 1336 | 1337 | appdaemon_conf_sensor = f"{self.monitor_name}.{device_entity_prefix}" 1338 | 1339 | return appdaemon_conf_sensor 1340 | 1341 | def node_state_changed(self, entity, attribute, old, new, kwargs): 1342 | """Respond to a change in the Node's state.""" 1343 | 1344 | location = self.mqtt.get_state(entity, attribute="location", copy=False) 1345 | node = location.lower().replace(" ", "_") 1346 | 1347 | if ( 1348 | new == "online" 1349 | and self.node_scheduled_reboot.get(node) 1350 | and self.adapi.timer_running(self.node_scheduled_reboot[node]) 1351 | ): 1352 | # means there was a scheduled reboot for this node, so should be cancelled 1353 | self.adapi.log( 1354 | f"Cancelling Scheduled Auto Reboot for Node at {location}, as its now back Online" 1355 | ) 1356 | 1357 | self.adapi.cancel_timer(self.node_scheduled_reboot[node]) 1358 | self.node_scheduled_reboot[node] = None 1359 | self.mqtt.set_state(entity, reboot_scheduled="off") 1360 | 1361 | if old == "offline" and new == "online": 1362 | self.adapi.run_in(self.reload_device_state, 0) 1363 | 1364 | elif new == "offline" and old == "online": 1365 | self.adapi.log( 1366 | f"Node at {location} is Offline, will need to be checked", 1367 | level="WARNING", 1368 | ) 1369 | 1370 | # now check if to auto reboot the node 1371 | if node in self.args.get("remote_monitors", {}): 1372 | if ( 1373 | self.args["remote_monitors"][node].get("auto_reboot_when_offline") 1374 | is True 1375 | ): 1376 | if self.node_scheduled_reboot.get(node) is not None: 1377 | # a reboot had been scheduled earlier, so must be cancled and started all over 1378 | # this should technically not need to run, unless there is a bug somewhere 1379 | 1380 | if self.adapi.timer_running(self.node_scheduled_reboot[node]): 1381 | self.adapi.cancel_timer(self.node_scheduled_reboot[node]) 1382 | 1383 | self.node_scheduled_reboot[node] = None 1384 | 1385 | self.adapi.log( 1386 | f"Scheduling Auto Reboot for Node at {location} as its Offline", 1387 | level="WARNING", 1388 | ) 1389 | 1390 | if self.args["remote_monitors"][node].get("time") is not None: 1391 | # there is a time it should be rebooted if need be 1392 | reboot_time = self.args["remote_monitors"][node]["time"] 1393 | now = self.adapi.datetime() 1394 | scheduled_time = datetime.combine( 1395 | self.adapi.date(), self.adapi.parse_time(reboot_time) 1396 | ) 1397 | if now > scheduled_time: # the scheduled time is in the past 1398 | # run the scheduled time the next day 1399 | scheduled_time = scheduled_time + timedelta(days=1) 1400 | 1401 | self.node_scheduled_reboot[node] = self.adapi.run_at( 1402 | self.restart_device, 1403 | scheduled_time, 1404 | location=node, 1405 | auto_rebooting=True, 1406 | ) 1407 | reboot_time = scheduled_time.isoformat() 1408 | 1409 | else: 1410 | # use the same system_check time out for auto rebooting, to give it time to 1411 | # reconnect to the network, in case of a network glich 1412 | self.node_scheduled_reboot[node] = self.adapi.run_in( 1413 | self.restart_device, 1414 | self.system_timeout, 1415 | location=node, 1416 | auto_rebooting=True, 1417 | ) 1418 | 1419 | reboot_time = ( 1420 | self.adapi.datetime() 1421 | + timedelta(seconds=self.system_timeout) 1422 | ).isoformat() 1423 | 1424 | self.mqtt.set_state( 1425 | entity, reboot_scheduled="on", reboot_time=reboot_time 1426 | ) 1427 | 1428 | else: 1429 | # send a ping to node and log the output for debugging 1430 | host = self.args["remote_monitors"][node]["host"] 1431 | 1432 | import subprocess 1433 | 1434 | status, result = subprocess.getstatusoutput(f"ping -c1 -w2 {host}") 1435 | 1436 | if status == 1: # it is offline 1437 | self.mqtt.set_state(entity, state="network disconnected") 1438 | 1439 | def monitor_scan_now(self, entity, attribute, old, new, kwargs): 1440 | """Request an immediate scan from the monitors.""" 1441 | scan_type = self.mqtt.get_state(entity, attribute="scan_type", copy=False) 1442 | locations = self.mqtt.get_state(entity, attribute="locations", copy=False) 1443 | 1444 | if scan_type == "both": 1445 | self.adapi.run_in(self.run_arrive_scan, 0, location=locations) 1446 | self.adapi.run_in(self.run_depart_scan, 0, location=locations) 1447 | 1448 | elif scan_type == "arrival": 1449 | self.adapi.run_in(self.run_arrive_scan, 0, location=locations) 1450 | 1451 | elif scan_type == "depart": 1452 | self.adapi.run_in(self.run_depart_scan, 0, location=locations) 1453 | 1454 | self.mqtt.set_state(entity, state="idle") 1455 | 1456 | def load_known_devices(self, kwargs): 1457 | """Request all known devices in config to be added to monitors.""" 1458 | timer = 0 1459 | if self.args.get("known_devices") is not None: 1460 | for device in self.args["known_devices"]: 1461 | self.adapi.run_in( 1462 | self.send_mqtt_message, 1463 | timer, 1464 | topic=f"{self.monitor_topic}/setup/ADD STATIC DEVICE", 1465 | payload=device, 1466 | scan_type="System", 1467 | ) 1468 | timer += 3 1469 | 1470 | def remove_known_device(self, kwargs): 1471 | """Request all known devices in config to be deleted from monitors.""" 1472 | 1473 | device = kwargs["device"] 1474 | 1475 | self.adapi.log(f"Removing device {device}", level="INFO") 1476 | 1477 | self.adapi.run_in( 1478 | self.send_mqtt_message, 1479 | 0, 1480 | topic=f"{self.monitor_topic}/setup/DELETE STATIC DEVICE", 1481 | payload=device, 1482 | scan_type="System", 1483 | ) 1484 | 1485 | # now remove the device from AD 1486 | entities = list( 1487 | self.mqtt.get_state(f"{self.monitor_name}", copy=False, default={}).keys() 1488 | ) 1489 | device_name = None 1490 | for entity in entities: 1491 | if device == self.mqtt.get_state(entity, attribute="id", copy=False): 1492 | location = self.mqtt.get_state(entity, attribute="location") 1493 | if location is None: 1494 | continue 1495 | 1496 | node = location.replace(" ", "_").lower() 1497 | self.mqtt.remove_entity(entity) 1498 | if device_name is None: 1499 | _, domain_device = self.mqtt.split_entity(entity) 1500 | device_name = domain_device.replace(f"_{node}", "") 1501 | 1502 | # now remove the device from HA 1503 | entities = list(self.hass.get_state("sensor", copy=False, default={}).keys()) 1504 | for entity in entities: 1505 | if device == self.hass.get_state(entity, attribute="id", copy=False): 1506 | # first cancel the handler if it exists 1507 | handler = self.confidence_handlers.get(entity) 1508 | if handler is not None: 1509 | self.hass.cancel_listen_state(handler) 1510 | 1511 | self.hass.remove_entity(entity) 1512 | 1513 | if device_name is not None: 1514 | device_entity_id = f"{self.monitor_name}_{device_name}" 1515 | device_state_sensor = f"{self.user_device_domain}.{device_entity_id}" 1516 | 1517 | if device_entity_id in self.home_state_entities: 1518 | del self.home_state_entities[device_entity_id] 1519 | 1520 | if device_state_sensor in self.all_users_sensors: 1521 | self.all_users_sensors.remove(device_state_sensor) 1522 | 1523 | # now remove for HA 1524 | self.hass.remove_entity(device_state_sensor) 1525 | 1526 | # now remove for AD 1527 | self.mqtt.remove_entity(device_state_sensor) 1528 | 1529 | def clean_devices(self, kwargs): 1530 | """Used to check for old devices, and remove them accordingly""" 1531 | 1532 | # search for them first 1533 | delay = 0 1534 | removed = [] 1535 | known_device_names = [n.lower() for n in list(self.known_devices.values())] 1536 | 1537 | for sensor in self.mqtt.get_state(self.monitor_topic, copy=False, default={}): 1538 | mac_id = self.mqtt.get_state(sensor, attribute="id", copy=False) 1539 | if mac_id is None: 1540 | continue 1541 | 1542 | sensor_name = self.mqtt.get_state( 1543 | sensor, attribute="name", copy=False, default="" 1544 | ).lower() 1545 | if mac_id not in removed and ( 1546 | mac_id not in self.known_devices 1547 | or sensor_name not in known_device_names 1548 | ): 1549 | # it should be removed 1550 | 1551 | if removed == []: # means haven't removed one yet 1552 | self.adapi.log("Cleaning out old Known Devices") 1553 | 1554 | self.adapi.run_in(self.remove_known_device, delay, device=mac_id) 1555 | removed.append(mac_id) # indicate it has been removed 1556 | delay += 3 # should process later 1557 | 1558 | if removed != []: 1559 | delay += 5 1560 | # means some where removed, so needs to re-load the scripts to clean properly 1561 | self.adapi.run_in(self.restart_device, delay) 1562 | 1563 | # now load up the known devices before state 1564 | delay += 45 1565 | self.adapi.run_in(self.load_known_devices, delay) 1566 | 1567 | if removed != []: 1568 | delay += 15 + len(known_device_names) 1569 | self.adapi.run_in(self.run_arrive_scan, delay) 1570 | self.adapi.run_in(self.load_known_devices, delay + 120) 1571 | 1572 | delay += 60 1573 | self.adapi.run_in(self.reload_device_state, delay) 1574 | 1575 | # for some strange reasons, forces the app to run load_known_devices twice 1576 | # to get updated data on the cleaned out devices 1577 | 1578 | def count_persons_in_home(self): 1579 | """Used to count the number of persons in the Home""" 1580 | 1581 | user_devices = list(self.home_state_entities.keys()) 1582 | sensors = list( 1583 | map( 1584 | lambda x: self.mqtt.get_state( 1585 | f"{self.user_device_domain}.{x}", copy=False 1586 | ), 1587 | user_devices, 1588 | ) 1589 | ) 1590 | sensors = [i for i in sensors if i == self.state_true] 1591 | 1592 | return len(sensors) 1593 | 1594 | def hass_restarted(self, event_name, data, kwargs): 1595 | """Respond to a HASS Restart.""" 1596 | self.setup_global_sensors() 1597 | # self.adapi.run_in(self.reload_device_state, 10) 1598 | self.adapi.run_in(self.restart_device, 5) 1599 | 1600 | def setup_service(self): # rgister services 1601 | """Register services for app""" 1602 | self.mqtt.register_service( 1603 | f"{self.monitor_name}/remove_known_device", self.presense_services 1604 | ) 1605 | self.mqtt.register_service( 1606 | f"{self.monitor_name}/run_arrive_scan", self.presense_services 1607 | ) 1608 | self.mqtt.register_service( 1609 | f"{self.monitor_name}/run_depart_scan", self.presense_services 1610 | ) 1611 | self.mqtt.register_service( 1612 | f"{self.monitor_name}/run_rssi_scan", self.presense_services 1613 | ) 1614 | self.mqtt.register_service( 1615 | f"{self.monitor_name}/run_node_command", self.presense_services 1616 | ) 1617 | self.mqtt.register_service( 1618 | f"{self.monitor_name}/restart_device", self.presense_services 1619 | ) 1620 | self.mqtt.register_service( 1621 | f"{self.monitor_name}/reload_device_state", self.presense_services 1622 | ) 1623 | self.mqtt.register_service( 1624 | f"{self.monitor_name}/load_known_devices", self.presense_services 1625 | ) 1626 | self.mqtt.register_service( 1627 | f"{self.monitor_name}/clear_location_entities", self.presense_services 1628 | ) 1629 | self.mqtt.register_service( 1630 | f"{self.monitor_name}/clean_devices", self.presense_services 1631 | ) 1632 | 1633 | def presense_services(self, namespace, domain, service, kwargs): 1634 | """Callback for executing service call""" 1635 | self.adapi.log( 1636 | f"presence_services() {namespace} {domain} {service} {kwargs}", 1637 | level="DEBUG", 1638 | ) 1639 | 1640 | func = getattr(self, service) # get the function first 1641 | 1642 | if func is None: 1643 | raise ValueError(f"Unsupported service call {service}") 1644 | 1645 | if service == "remove_known_device" and "device" not in kwargs: 1646 | self.adapi.log( 1647 | "Could not Remove Known Device as no Device provided", level="WARNING" 1648 | ) 1649 | return 1650 | 1651 | elif service == "clear_location_entities" and "location" not in kwargs: 1652 | self.adapi.log( 1653 | "Could not Clear Location Entities as no Location provided", 1654 | level="WARNING", 1655 | ) 1656 | return 1657 | 1658 | if "location" in kwargs: 1659 | kwargs["location"] = kwargs["location"].replace(" ", "_").lower() 1660 | 1661 | if "delay" in kwargs: 1662 | scan_delay = kwargs.pop("delay") 1663 | kwargs["scan_delay"] = scan_delay 1664 | 1665 | self.adapi.run_in(func, 0, **kwargs) 1666 | 1667 | def parse_sensor(self, sensor) -> tuple: 1668 | """Used to parse the sensor to for namespace """ 1669 | 1670 | if sensor.count(".") > 1: # means there is namespace given in the entity 1671 | (namespace, domain, device) = sensor.split(".") 1672 | sen = f"{domain}.{device}" 1673 | 1674 | else: 1675 | namespace = self.hass.get_namespace() # default is hass 1676 | sen = sensor 1677 | 1678 | return (namespace, sen) 1679 | 1680 | def terminate(self): 1681 | for node in self.node_executing: 1682 | if self.node_executing[node] is not None: 1683 | if ( 1684 | not self.node_executing[node].done() 1685 | and not self.node_executing[node].cancelled() 1686 | ): 1687 | # this means its still running, so cancel the task 1688 | self.node_executing[node].cancel() 1689 | -------------------------------------------------------------------------------- /apps/home_presence_app/requirements.txt: -------------------------------------------------------------------------------- 1 | paramiko 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monitor-App", 3 | "render_readme": "true", 4 | "domains": ["binary_sensor", "sensor", "device_tracker"] 5 | } 6 | -------------------------------------------------------------------------------- /installer/README.md: -------------------------------------------------------------------------------- 1 | # Quick installation & update script 2 | 3 | ### This script will perform different options for installation and/or updates 4 | 5 | Choose your selection of installation path below for instructions 6 | 7 | ![alt text](https://github.com/Odianosen25/Monitor-App/blob/master/installer/screenshot_installer.JPG) 8 | 9 | > The scripts are tested on Raspberry only (RPi3 & 4) but should work on most Linux distro's and usernames 10 | 11 | You will find provided templates of configuration files that will be copied to your device, you will just need to fill in your own information. Description and examples are within the configuration files themselves. To execute the full installscript, run following command from your commandline: 12 | 13 | 14 | `bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/install.sh)"` 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /installer/appdaemon.yaml: -------------------------------------------------------------------------------- 1 | logs: 2 | main_log: 3 | filename: /home/appdaemon/.appdaemon/log/appdaemon.log 4 | access_log: 5 | filename: /home/appdaemon/.appdaemon/log/access.log 6 | error_log: 7 | filename: /home/appdaemon/.appdaemon/log/error.log 8 | diag_log: 9 | filename: /home/appdaemon/.appdaemon/log/diag.log 10 | log_generations: 5 11 | log_size: 1024 12 | format: "{asctime} {levelname:<8} {appname:<10}: {message}" 13 | appdaemon: 14 | time_zone: ### Example Europe/Oslo 15 | latitude: 16 | longitude: 17 | elevation: 18 | plugins: 19 | HASS: 20 | type: hass 21 | ha_url: http:// (or https) :8123 (or your custom port) 22 | token: 23 | ### You must create a long-lived token in HA for AppDaemon to be used here 24 | 25 | MQTT: 26 | type: mqtt 27 | namespace: mqtt 28 | client_host: 29 | client_user: 30 | client_password: 31 | 32 | http: 33 | url: http://:5050 34 | ### You can then login to AD Admin page in this address to see info and easily access logs 35 | admin: 36 | api: 37 | hadashboard: 38 | 39 | -------------------------------------------------------------------------------- /installer/appdaemon@appdaemon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AppDaemon 3 | 4 | [Service] 5 | Type=simple 6 | User=%i 7 | ExecStart=/srv/appdaemon/bin/appdaemon -c /home/appdaemon/.appdaemon/conf/ 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /installer/apps.yaml: -------------------------------------------------------------------------------- 1 | home_presence_app: 2 | module: home_presence_app 3 | class: HomePresenceApp 4 | plugin: 5 | - HASS 6 | - MQTT 7 | monitor_topic: ### Example monitor 8 | user_device_domain: mqtt # change to device_tracker if you want your devices to appear as device_tracker rather than as binary_sensor 9 | everyone_not_home: everyone_not_home # will be a binary_sensor 10 | everyone_home: everyone_home # will be a binary_sensor 11 | somebody_is_home: somebody_is_home # will be a binary_sensor 12 | depart_check_time: 30 13 | minimum_confidence: 80 14 | not_home_timeout: 15 15 | system_check: 30 16 | system_timeout: 60 17 | 18 | ### Read about RSSI and more control in main README, and remove remarks below if you want to use this feature 19 | # home_gateway_sensors: 20 | # - 21 | # home_motion_sensors: 22 | # - 23 | # - 24 | 25 | #pin_app: True 26 | #pin_thread: 3 27 | #log: apps_log 28 | #log_level: DEBUG 29 | 30 | ### If you want to be able to remotely reboot your monitor hardware from MQTT, automation or scripts, 31 | ### add your monitor(s) below. Be aware, you have to use same name of monitor as it is called in 32 | ### 'mqtt_publisher_identity' in 'mqtt_preferences' of monitor 33 | ### If not, remove entire section of 'remote_monitors' 34 | remote_monitors: 35 | : 36 | host: 37 | username: 38 | password: 39 | 40 | : 41 | host: 42 | username: 43 | password: 44 | ### etc etc 45 | 46 | known_devices: 47 | - xx:xx:xx:xx:xx:xx 48 | - xx:xx:xx:xx:xx:xx 49 | - xx:xx:xx:xx:xx:xx 50 | ### etc etc 51 | 52 | -------------------------------------------------------------------------------- /installer/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Bash script to offer installation and/or updates of Monitor-App and AppDaemon 4.x 3 | # Created for @Odianosen25 and his great app Monitor-App 4 | # 5 | # 6 | if sudo -q apt-get install dialog && sudo apt-get install curl ; 7 | then 8 | echo -e "\e[32m\e[0m" 9 | else 10 | echo -e "\e[31m\e[0m" 11 | fi 12 | 13 | 14 | TERMINAL=$(tty) 15 | HEIGHT=20 16 | WIDTH=60 17 | CHOICE_HEIGHT=5 18 | BACKTITLE="TheStigh's installerscript for Monitor-App & AppDaemon 4.x" 19 | TITLE="MENU" 20 | MENU "" 21 | #MENU="This menu gives you choices of what you want to do, either it is installing or updating Monitor-App and/or AppDaemon" 22 | 23 | OPTIONS=(1 "Install Standalone AppDaemon & Monitor-App" 24 | 2 "Install Standalone Monitor-App" 25 | 3 "Update Standalone AppDaemon & Monitor-App" 26 | 4 "Update Standalone Monitor-App") 27 | 28 | CHOICE=$(dialog --no-lines \ 29 | --clear \ 30 | --backtitle "$BACKTITLE" \ 31 | --title "$TITLE" \ 32 | --menu "$MENU" \ 33 | $HEIGHT $WIDTH $CHOICE_HEIGHT \ 34 | "${OPTIONS[@]}" \ 35 | 2>&1 >$TERMINAL) 36 | 37 | clear 38 | case $CHOICE in 39 | 1) 40 | echo "You chose: Install Standalone AppDaemon & Monitor-App" 41 | bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/install_ad.sh)" 42 | ;; 43 | 2) 44 | echo "You chose: Install Standalone Monitor-App" 45 | bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/install_ma_only.sh)" 46 | ;; 47 | 3) 48 | echo "You chose: Update Standalone AppDaemon & Monitor-App" 49 | bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/update_ad_ma.sh)" 50 | ;; 51 | 4) 52 | echo "You chose: Update Standalone Monitor-App" 53 | bash -c "$(curl -sL https://raw.githubusercontent.com/Odianosen25/Monitor-App/master/installer/update_ma.sh)" 54 | ;; 55 | esac 56 | -------------------------------------------------------------------------------- /installer/install_ad.sh: -------------------------------------------------------------------------------- 1 | # Bash script to install AppDaemon 4.x & Monitor-App 2 | # Recommended OS: Latest Raspbian downloaded from raspberrypi.org 3 | cd ~ 4 | clear 5 | echo -e "\e[0m" 6 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 7 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 8 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 9 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 10 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 11 | echo -e "\e[96m /_/ /_/ \e[90m" 12 | echo -e "\e[0m" 13 | echo -e "\e[0m" 14 | echo -e "\e[0m" 15 | cd ~ 16 | echo -e "\e[96m Preparing system for AppDaemon 4.x & Monitor-App...\e[90m" 17 | echo -e "\e[0m" 18 | 19 | # Prepare system 20 | echo -e "\e[96m[STEP 1/10] Updating system...\e[90m" 21 | if sudo apt-get update -y; 22 | then 23 | echo -e "\e[32m Updating | Done\e[0m" 24 | else 25 | echo -e "\e[31m Updating | Failed\e[0m" 26 | exit; 27 | fi 28 | echo -e "\e[0m" 29 | 30 | if sudo apt-get upgrade -y; 31 | then 32 | echo -e "\e[32m[STEP 1/10] Update & Upgrading | Done\e[0m" 33 | else 34 | echo -e "\e[31m[STEP 1/10] Update & Upgrading | Failed\e[0m" 35 | exit; 36 | fi 37 | echo -e "\e[0m" 38 | 39 | 40 | # Installing packages 41 | echo -e "\e[96m[STEP 2/10] Installing Python & Dependencies...\e[90m" 42 | if sudo apt install python3 python3-dev python3-venv python3-pip libffi-dev libssl-dev git -y; 43 | then 44 | echo -e "\e[32m Installing Python & Dependencies | Done\e[0m" 45 | else 46 | echo -e "\e[31m Installing Python & Dependencies | Failed\e[0m" 47 | exit; 48 | fi 49 | echo -e "\e[0m" 50 | 51 | if git clone https://github.com/Odianosen25/Monitor-App.git; 52 | then 53 | echo -e "\e[32m[STEP 2/10] Cloning Monitor-App | Done\e[0m" 54 | else 55 | echo -e "\e[31m[STEP 2/10] Cloning Monitor-App | Failed\e[0m" 56 | exit; 57 | fi 58 | echo -e "\e[0m" 59 | 60 | #Create User appdaemon 61 | echo -e "\e[96m[STEP 3/10] Creating users...\e[90m" 62 | if sudo useradd -rm appdaemon; 63 | then 64 | echo -e "\e[32m Creating user | Done\e[0m" 65 | else 66 | echo -e "\e[31m Creating user | Failed\e[0m" 67 | exit; 68 | fi 69 | echo -e "\e[0m" 70 | 71 | if sudo mkdir /srv/appdaemon; 72 | then 73 | echo -e "\e[32m Creating AppDaemon folder | Done\e[0m" 74 | else 75 | echo -e "\e[31m Creating AppDaemon folder | Failed\e[0m" 76 | exit; 77 | fi 78 | echo -e "\e[0m" 79 | 80 | 81 | if sudo chown appdaemon:appdaemon /srv/appdaemon; 82 | then 83 | echo -e "\e[32m[STEP 3/10] Creating users | Done\e[0m" 84 | else 85 | echo -e "\e[31m[STEP 3/10] Creating users | Failed\e[0m" 86 | exit; 87 | fi 88 | 89 | 90 | # Copy service to run AppDaemon as Service 91 | echo -e "\e[96m[STEP 4/10] Copying service to run AppDaemon as Service...\e[90m" 92 | if sudo cp ~/Monitor-App/installerscript/appdaemon@appdaemon.service /etc/systemd/system/appdaemon@appdaemon.service; 93 | then 94 | echo -e "\e[32m[STEP 4/10] Copy service | Done\e[0m" 95 | else 96 | echo -e "\e[31m[STEP 4/10] Copy service | Failed\e[0m" 97 | exit; 98 | fi 99 | 100 | # Prepare installerscript files for part 2 101 | if sudo cp -r ~/Monitor-App/installerscript /home/appdaemon/; 102 | then 103 | echo -e "\e[32mPreparation of scriptfiles for part 2 | Done\e[0m" 104 | else 105 | echo -e "\e[31mPreparation of scriptfiles for part 2 | Failed\e[0m" 106 | exit; 107 | fi 108 | 109 | sudo cp ~/Monitor-App/apps/home_presence_app/home_presence_app.py /home/appdaemon/installerscript/home_presence_app.py 110 | 111 | # Prepare installation part 2 file 112 | if sudo cp ~/Monitor-App/installerscript/install_ad_part2.sh ~/install_ad_part2.sh; 113 | then 114 | echo -e "\e[32mPreparation of installation part 2 | Done\e[0m" 115 | else 116 | echo -e "\e[31mPreparation of installation part 2 | Failed\e[0m" 117 | exit; 118 | fi 119 | 120 | if sudo chmod +x ~/install_ad_part2.sh; 121 | then 122 | echo -e "\e[32mDone\e[0m" 123 | else 124 | echo -e "\e[31mFailed\e[0m" 125 | exit; 126 | fi 127 | 128 | echo " " 129 | echo " " 130 | echo " " 131 | echo -e "\e[32mTo continue installation, type: \e[96mbash install_ad_part2.sh\e[0m" 132 | echo " " 133 | echo " " 134 | echo " " 135 | 136 | sudo -u appdaemon -H -s 137 | 138 | if sudo systemctl enable appdaemon@appdaemon.service --now; 139 | then 140 | echo -e "\e[32mAutostart Service | Done\e[0m" 141 | else 142 | echo -e "\e[31mAutostart Service | Failed\e[0m" 143 | exit; 144 | fi 145 | 146 | sudo rm -r /home/appdaemon/installerscript 147 | 148 | echo -e "\e[0m" 149 | echo -e "\e[0m" 150 | echo -e "\e[0m" 151 | echo -e "\e[0m" 152 | echo -e "\e[32mThe final step now are to fill in information about your own\e[0m" 153 | echo -e "\e[32menvironment, like IP address, username and password ++ for your\e[0m" 154 | echo -e "\e[32mMQTT broker in appdaemon.conf...\e[0m" 155 | echo -e "\e[32mYou will find the file here:\e[0m" 156 | echo -e "\e[96msudo nano /home/appdaemon/.appdaemon/conf/appdaemon.conf\e[0m" 157 | echo -e "\e[32mFinish the edit with ctrl+o & ctrl+x\e[0m" 158 | echo -e "\e[0m" 159 | echo -e "\e[32mThen you need to edit and complete missing information in\e[0m" 160 | echo -e "\e[32mapps.yaml that you will find here:\e[0m" 161 | echo -e "\e[96msudo nano /home/appdaemon/.appdaemon/conf/apps.yaml\e[0m" 162 | echo -e "\e[32mFinish the edit with ctrl+o & ctrl+x\e[0m" 163 | echo -e "\e[0m" 164 | echo -e "\e[32mWhen all above is done, \e[96msudo reboot now\e[32m your device.\e[0m" 165 | echo -e "\e[32mIf all went well, you should see new entities in HA\e[0m" 166 | echo -e "\e[0m" 167 | -------------------------------------------------------------------------------- /installer/install_ad_part2.sh: -------------------------------------------------------------------------------- 1 | 2 | clear 3 | echo -e "\e[0m" 4 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 5 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 6 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 7 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 8 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 9 | echo -e "\e[96m /_/ /_/ \e[90m" 10 | echo -e "\e[0m" 11 | echo -e "\e[0m" 12 | echo -e "\e[0m" 13 | cd ~ 14 | echo -e "\e[96m Installation Part II...\e[90m" 15 | echo -e "\e[0m" 16 | 17 | # Preparing Python environment 18 | echo -e "\e[96m[STEP 5/10] Preparing environment...\e[90m" 19 | cd /srv/appdaemon 20 | 21 | if python3 -m venv .; 22 | then 23 | echo -e "\e[32m Environment preparation | Done\e[0m" 24 | else 25 | echo -e "\e[31m Environment preparation | Failed\e[0m" 26 | exit; 27 | fi 28 | 29 | if source bin/activate; 30 | then 31 | echo -e "\e[32m[STEP 5/10] Moved to AD and ready for install | Done\e[0m" 32 | else 33 | echo -e "\e[31m[STEP 5/10] Moved to AD and ready for install | Failed\e[0m" 34 | exit; 35 | fi 36 | 37 | 38 | # Install AppDaemon from git 39 | echo -e "\e[96m[STEP 6/10] Installing AppDaemon...\e[90m" 40 | cd /srv/appdaemon 41 | 42 | if git clone https://github.com/home-assistant/appdaemon.git; 43 | then 44 | echo -e "\e[32m Downloading AppDaemon | Done\e[0m" 45 | else 46 | echo -e "\e[31m Downloading AppDaemon | Failed\e[0m" 47 | exit; 48 | fi 49 | 50 | cd appdaemon 51 | 52 | if pip3 install .; 53 | then 54 | echo -e "\e[32m[STEP 6/10] Installing AppDaemon | Done\e[0m" 55 | else 56 | echo -e "\e[31m[STEP 6/10] Installing AppDaemon | Failed\e[0m" 57 | exit; 58 | fi 59 | 60 | 61 | # Create folders 62 | echo -e "\e[96m[STEP 7/10] Create all needed folders...\e[90m" 63 | mkdir -p /home/appdaemon/.appdaemon/conf/apps 64 | mkdir /home/appdaemon/.appdaemon/conf/apps/home_presence_app 65 | mkdir /home/appdaemon/.appdaemon/log 66 | echo -e "\e[32m[STEP 7/10] Createing folders | Done\e[0m" 67 | 68 | 69 | # Copy remainig files to correct folders 70 | echo -e "\e[96m[STEP 8/10] Copy configuration files and Monitor-App to AppDaemon...\e[90m" 71 | if cp ~/installerscript/appdaemon.yaml /home/appdaemon/.appdaemon/conf/appdaemon.conf; 72 | then 73 | echo -e "\e[32m Copy configuration files | Done\e[0m" 74 | else 75 | echo -e "\e[31m Copy configuration files | Failed\e[0m" 76 | exit; 77 | fi 78 | 79 | if cp ~/installerscript/apps.yaml /home/appdaemon/.appdaemon/conf/apps/home_presence_app.yaml; 80 | then 81 | echo -e "\e[32m Copy Monitor-App to AppDaemon | Done\e[0m" 82 | else 83 | echo -e "\e[31m Copy Monitor-App to AppDaemon | Failed\e[0m" 84 | exit; 85 | fi 86 | 87 | if cp ~/installerscript/home_presence_app.py /home/appdaemon/.appdaemon/conf/apps/home_presence_app/home_presence_app.py; 88 | then 89 | echo -e "\e[32m[STEP 8/10] Copy final files | Done\e[0m" 90 | else 91 | echo -e "\e[31m[STEP 8/10] Copy final files | Failed\e[0m" 92 | exit; 93 | fi 94 | 95 | 96 | # Install Paramiko to be able to reboot external monitors 97 | echo -e "\e[96m[STEP 9/10] Installing Paramiko for remote reboot capabilities...\e[90m" 98 | if pip3 install paramiko; 99 | then 100 | echo -e "\e[32m[STEP 9/10] Installing Paramiko | Done\e[0m" 101 | else 102 | echo -e "\e[31m[STEP 9/10] Installing Paramiko | Failed\e[0m" 103 | exit; 104 | fi 105 | 106 | clear 107 | # Final instructions to make the final configurations of 108 | # the files appdaemon.yaml and apps.yaml, templates are already in place 109 | echo -e "\e[0m" 110 | echo -e "\e[0m" 111 | echo -e "\e[32mNow, type \e[96mexit\e[32m to quit AD environment!\e[0m" 112 | echo -e "\e[0m" 113 | echo -e "\e[0m" 114 | exit 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /installer/install_ma_only.sh: -------------------------------------------------------------------------------- 1 | 2 | # Bash script to install Monitor-App 3 | # Recommended OS: Latest Raspbian downloaded from raspberrypi.org 4 | cd ~ 5 | clear 6 | echo -e "\e[0m" 7 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 8 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 9 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 10 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 11 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 12 | echo -e "\e[96m /_/ /_/ \e[90m" 13 | echo -e "\e[0m" 14 | echo -e "\e[0m" 15 | echo -e "\e[0m" 16 | cd ~ 17 | echo -e "\e[96m Preparing system for Monitor-App, requires existing installation of AppDaemon 4.x\e[90m" 18 | echo -e "\e[96m where AppDaemon configuration files are installed to default folder. The script\e[90m" 19 | echo -e "\e[96m will check this and stop the installation if not.\e[90m" 20 | echo -e "\e[0m" 21 | echo -e "\e[96m Assuming path to /conf folder: \e[32m/home/appdaemon/.appdaemon/conf\e[96m \e[90m" 22 | echo -e "\e[0m" 23 | 24 | 25 | # Pre-check to see if conf folder are correct 26 | if cd /home/appdaemon/.appdaemon/conf; 27 | then 28 | echo -e "\e[32m Checking path to /conf | Done\e[0m" 29 | else 30 | echo -e "\e[31mChecking path to /conf | Failed\e[0m" 31 | exit; 32 | fi 33 | 34 | # Returning to user HOME folder 35 | cd ~ 36 | 37 | # Prepare system 38 | echo -e "\e[96m[STEP 1/6] Updating system...\e[90m" 39 | if sudo apt-get update -y; 40 | then 41 | echo -e "\e[32m Updating | Done\e[0m" 42 | else 43 | echo -e "\e[31m Updating | Failed\e[0m" 44 | exit; 45 | fi 46 | echo -e "\e[0m" 47 | 48 | if sudo apt-get upgrade -y; 49 | then 50 | echo -e "\e[32m[STEP 1/6] Update & Upgrading | Done\e[0m" 51 | else 52 | echo -e "\e[31m[STEP 1/6] Update & Upgrading | Failed\e[0m" 53 | exit; 54 | fi 55 | echo -e "\e[0m" 56 | 57 | echo -e "\e[96m[STEP 2/6] Cloning Monitor-App...\e[90m" 58 | if git clone https://github.com/Odianosen25/Monitor-App.git; 59 | then 60 | echo -e "\e[32m[STEP 2/6] Cloning Monitor-App | Done\e[0m" 61 | else 62 | echo -e "\e[31m[STEP 2/6] Cloning Monitor-App | Failed\e[0m" 63 | exit; 64 | fi 65 | echo -e "\e[0m" 66 | 67 | # Creating folder for Monitor-App 68 | echo -e "\e[96m[STEP 3/6] Creating folder for Monitor-App...\e[90m" 69 | if sudo mkdir /home/appdaemon/.appdaemon/conf/apps/home_presence_app; 70 | then 71 | echo -e "\e[32m[STEP 3/6] Creating folder for Monitor-App | Done\e[0m" 72 | else 73 | echo -e "\e[31m[STEP 3/6] Creating folder for Monitor-App | Failed\e[0m" 74 | exit; 75 | fi 76 | 77 | # Copy remainig files to correct folders 78 | echo -e "\e[96m[STEP 4/6] Copy Monitor-App configuration to AppDaemon...\e[90m" 79 | if cp ~/Monitor-App/installerscript/apps.yaml /home/appdaemon/.appdaemon/conf/apps/home_presence_app.yaml; 80 | then 81 | echo -e "\e[32m[STEP 4/6] Copy Monitor-App configuration | Done\e[0m" 82 | else 83 | echo -e "\e[31m[STEP 4/6] Copy Monitor-App configuration | Failed\e[0m" 84 | exit; 85 | fi 86 | 87 | echo -e "\e[96m[STEP 5/6] Copy Monitor-App to AppDaemon...\e[90m" 88 | if cp ~/Monitor-App/apps/home_presence_app/home_presence_app.py /home/appdaemon/.appdaemon/conf/apps/home_presence_app/home_presence_app.py; 89 | then 90 | echo -e "\e[32m[STEP 5/6] Copy Monitor-App | Done\e[0m" 91 | else 92 | echo -e "\e[31m[STEP 5/6] Copy Monitor-App | Failed\e[0m" 93 | exit; 94 | fi 95 | 96 | 97 | # Install Paramiko to be able to reboot external monitors 98 | echo -e "\e[96m[STEP 6/6] Installing Paramiko for remote reboot capabilities...\e[90m" 99 | if sudo pip3 install paramiko; 100 | then 101 | echo -e "\e[32m[STEP 6/6] Installing Paramiko | Done\e[0m" 102 | else 103 | echo -e "\e[31m[STEP 6/6] Installing Paramiko | Failed\e[0m" 104 | exit; 105 | fi 106 | 107 | 108 | 109 | echo -e "\e[0m" 110 | echo -e "\e[0m" 111 | echo -e "\e[0m" 112 | echo -e "\e[0m" 113 | echo -e "\e[32mFinally you need to edit and complete missing information in\e[0m" 114 | echo -e "\e[32mhome_presence_app.yaml that you will find here:\e[0m" 115 | echo -e "\e[96msudo nano /home/appdaemon/.appdaemon/conf/home_presence_app.yaml\e[0m" 116 | echo -e "\e[32mFinish the edit with ctrl+o & ctrl+x\e[0m" 117 | echo -e "\e[0m" 118 | echo -e "\e[32mWhen all above is done, \e[96msudo reboot now\e[32m your device.\e[0m" 119 | echo -e "\e[32mIf all went well, you should see new entities in HA\e[0m" 120 | echo -e "\e[0m" 121 | -------------------------------------------------------------------------------- /installer/screenshot_installer.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Odianosen25/Monitor-App/702b953063eff970f3b37ae3eea7d03a478081d4/installer/screenshot_installer.JPG -------------------------------------------------------------------------------- /installer/update_ad_ma.sh: -------------------------------------------------------------------------------- 1 | # Bash script to update both AppDaemon 4.x and Monitor-App to latest version 2 | # Recommended OS: Latest Raspbian downloaded from raspberrypi.org 3 | cd ~ 4 | clear 5 | echo -e "\e[0m" 6 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 7 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 8 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 9 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 10 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 11 | echo -e "\e[96m /_/ /_/ \e[90m" 12 | echo -e "\e[0m" 13 | echo -e "\e[0m" 14 | echo -e "\e[0m" 15 | cd ~ 16 | echo -e "\e[32m Preparing system for \e[96mupdating\e[32m of both AppDaemon 4.x & Monitor-App...\e[0m" 17 | echo -e "\e[0m" 18 | 19 | # Prepare system 20 | echo -e "\e[96m[STEP 1/10] Updating system...\e[90m" 21 | if sudo apt-get update -y; 22 | then 23 | echo -e "\e[32m Updating | Done\e[0m" 24 | else 25 | echo -e "\e[31m Updating | Failed\e[0m" 26 | exit; 27 | fi 28 | echo -e "\e[0m" 29 | 30 | if sudo apt-get upgrade -y; 31 | then 32 | echo -e "\e[32m[STEP 1/10] Update & Upgrading | Done\e[0m" 33 | else 34 | echo -e "\e[31m[STEP 1/10] Update & Upgrading | Failed\e[0m" 35 | exit; 36 | fi 37 | echo -e "\e[0m" 38 | 39 | cd ~/Monitor-App 40 | 41 | if git pull; 42 | then 43 | echo -e "\e[32m[STEP 2/10] Downloading latest Monitor-App | Done\e[0m" 44 | else 45 | echo -e "\e[31m[STEP 2/10] Downloading latest Monitor-App | Failed\e[0m" 46 | exit; 47 | fi 48 | echo -e "\e[0m" 49 | 50 | # Replacing old with new version of Monitor-App within AppDaemon 51 | sudo rm /home/appdaemon/.appdaemon/conf/apps/home_precense_app/home_precense_app.py 52 | sudo cp ~/Monitor-App/apps/home_precense_app/home_precense_app.py /appdaemon/.appdaemon/conf/apps/home_precense_app/home_precense_app.py 53 | 54 | 55 | # Prepare ipdate part 2 file 56 | if sudo cp ~/Monitor-App/installerscript/update_ad_ma_part2.sh ~/update_ad_ma_part2.sh; 57 | then 58 | echo -e "\e[32mPreparation of update part 2 | Done\e[0m" 59 | else 60 | echo -e "\e[31mPreparation of update part 2 | Failed\e[0m" 61 | exit; 62 | fi 63 | 64 | if sudo chmod +x ~/update_ad_ma_part2.sh; 65 | then 66 | echo -e "\e[32mDone\e[0m" 67 | else 68 | echo -e "\e[31mFailed\e[0m" 69 | exit; 70 | fi 71 | 72 | echo " " 73 | echo " " 74 | echo " " 75 | echo -e "\e[32mTo continue installation, type: \e[96mbash update_ad_ma_part2.sh\e[0m" 76 | echo " " 77 | echo " " 78 | echo " " 79 | 80 | if sudo -u appdaemon -H -s; 81 | then 82 | echo -e " " 83 | else 84 | echo -e " " 85 | exit; 86 | fi 87 | 88 | 89 | #####################################################################3 90 | # Here, 'update_ad_ma_part2.sh' are running 91 | ###################################################################### 92 | 93 | 94 | if sudo systemctl restart appdaemon@appdaemon.service --now; 95 | then 96 | echo -e "\e[32mAppDaemon Running again | Done\e[0m" 97 | else 98 | echo -e "\e[31mAppDaemon Running again | Failed\e[0m" 99 | exit; 100 | fi 101 | 102 | echo -e "\e[0m" 103 | echo -e "\e[0m" 104 | echo -e "\e[0m" 105 | echo -e "\e[0m" 106 | echo -e "\e[32mIf all went well, both Monitor-App and AppDaemon are now\e[0m" 107 | echo -e "\e[32mupdated to latest build. Both has been restarted successfully.\e[0m" 108 | echo -e "\e[0m" 109 | echo -e "\e[0m" -------------------------------------------------------------------------------- /installer/update_ad_ma_part2.sh: -------------------------------------------------------------------------------- 1 | clear 2 | echo -e "\e[0m" 3 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 4 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 5 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 6 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 7 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 8 | echo -e "\e[96m /_/ /_/ \e[90m" 9 | echo -e "\e[0m" 10 | echo -e "\e[0m" 11 | echo -e "\e[0m" 12 | cd ~ 13 | echo -e "\e[96m Update Part II...\e[90m" 14 | echo -e "\e[0m" 15 | 16 | 17 | # Install AppDaemon from git 18 | echo -e "\e[96m[STEP 6/10] Updating AppDaemon...\e[90m" 19 | cd /srv/appdaemon 20 | 21 | if source bin/activate; 22 | then 23 | echo -e "\e[32m[STEP 5/10] Moved to AD and ready for update | Done\e[0m" 24 | else 25 | echo -e "\e[31m[STEP 5/10] Moved to AD and ready for update | Failed\e[0m" 26 | exit; 27 | fi 28 | 29 | if cd /srv/appdaemon/appdaemon; 30 | then 31 | echo -e "\e[32m[STEP 5/10] Accessing appdaemon folder | Done\e[0m" 32 | else 33 | echo -e "\e[31m[STEP 5/10] Accessing appdaemon folder | Failed\e[0m" 34 | exit; 35 | fi 36 | 37 | if git pull; 38 | then 39 | echo -e "\e[32m Downloading latest AppDaemon | Done\e[0m" 40 | else 41 | echo -e "\e[31m Downloading latest AppDaemon | Failed\e[0m" 42 | exit; 43 | fi 44 | 45 | if pip3 install --upgrade .; 46 | then 47 | echo -e "\e[32m[STEP 6/10] Updating AppDaemon | Done\e[0m" 48 | else 49 | echo -e "\e[31m[STEP 6/10] Updating AppDaemon | Failed\e[0m" 50 | exit; 51 | fi 52 | 53 | 54 | # Everything are finished and should be running fine 55 | echo -e "\e[0m" 56 | echo -e "\e[0m" 57 | echo -e "\e[32mNow, type \e[96mexit\e[32m to quit AD environment!\e[0m" 58 | echo -e "\e[0m" 59 | echo -e "\e[0m" 60 | exit -------------------------------------------------------------------------------- /installer/update_ma.sh: -------------------------------------------------------------------------------- 1 | 2 | # Bash script to update Monitor-App 3 | # Recommended OS: Latest Raspbian downloaded from raspberrypi.org 4 | cd ~ 5 | clear 6 | echo -e "\e[0m" 7 | echo -e "\e[96m______ ___ __________ _______ \e[90m" 8 | echo -e "\e[96m___ |/ /_______________(_)_ /______________ ___ |_______________ \e[90m" 9 | echo -e "\e[96m__ /|_/ /_ __ \_ __ \_ /_ __/ __ \_ ___/________ /| |__ __ \__ __ \ \e[90m" 10 | echo -e "\e[96m_ / / / / /_/ / / / / / / /_ / /_/ / / _/_____/ ___ |_ /_/ /_ /_/ / \e[90m" 11 | echo -e "\e[96m/_/ /_/ \____//_/ /_//_/ \__/ \____//_/ /_/ |_| .___/_ .___/ \e[90m" 12 | echo -e "\e[96m /_/ /_/ \e[90m" 13 | echo -e "\e[0m" 14 | echo -e "\e[0m" 15 | echo -e "\e[0m" 16 | cd ~ 17 | echo -e "\e[96m Preparing update for Monitor-App, requires existing installation of AppDaemon 4.x\e[90m" 18 | echo -e "\e[96m where AppDaemon configuration files are installed to default folder. The script\e[90m" 19 | echo -e "\e[96m will check this and stop the installation if not.\e[90m" 20 | echo -e "\e[0m" 21 | echo -e "\e[96m Assuming path to /conf folder: \e[32m/home/appdaemon/.appdaemon/conf\e[96m \e[90m" 22 | echo -e "\e[0m" 23 | 24 | 25 | # Pre-check to see if conf folder are correct 26 | if cd /home/appdaemon/.appdaemon/conf; 27 | then 28 | echo -e "\e[32m Checking path to /conf | Done\e[0m" 29 | else 30 | echo -e "\e[31mChecking path to /conf | Failed\e[0m" 31 | exit; 32 | fi 33 | 34 | # Returning to user HOME folder 35 | if cd ~/Monitor-App; 36 | then 37 | echo -e "\e[32m\e[0m" 38 | else 39 | echo -e "\e[31mMonitor-App repo not cloned | Failed\e[0m" 40 | exit; 41 | fi 42 | 43 | # Prepare system 44 | echo -e "\e[96m[STEP 1/6] Updating system...\e[90m" 45 | if sudo apt-get update -y; 46 | then 47 | echo -e "\e[32m Updating | Done\e[0m" 48 | else 49 | echo -e "\e[31m Updating | Failed\e[0m" 50 | exit; 51 | fi 52 | echo -e "\e[0m" 53 | 54 | if sudo apt-get upgrade -y; 55 | then 56 | echo -e "\e[32m[STEP 1/6] Update & Upgrading | Done\e[0m" 57 | else 58 | echo -e "\e[31m[STEP 1/6] Update & Upgrading | Failed\e[0m" 59 | exit; 60 | fi 61 | echo -e "\e[0m" 62 | 63 | echo -e "\e[96m[STEP 2/6] updating Monitor-App from Git...\e[90m" 64 | if git pull; 65 | then 66 | echo -e "\e[32m[STEP 2/6] Update Monitor-App | Done\e[0m" 67 | else 68 | echo -e "\e[31m[STEP 2/6] Update Monitor-App | Failed\e[0m" 69 | exit; 70 | fi 71 | echo -e "\e[0m" 72 | 73 | # Deleting existing version of Monitor-App 74 | echo -e "\e[96m[STEP 3/6] Deleting existing version of Monitor-App...\e[90m" 75 | if sudo rm /home/appdaemon/.appdaemon/conf/apps/home_presence_app/home_presence_app.py; 76 | then 77 | echo -e "\e[32m[STEP 3/6] Deleting Monitor-App | Done\e[0m" 78 | else 79 | echo -e "\e[31m[STEP 3/6] Deleting folder for Monitor-App | Failed\e[0m" 80 | exit; 81 | fi 82 | 83 | 84 | echo -e "\e[96m[STEP 4/6] Copy Monitor-App to AppDaemon...\e[90m" 85 | if cp ~/Monitor-App/apps/home_presence_app/home_presence_app.py /home/appdaemon/.appdaemon/conf/apps/home_presence_app/home_presence_app.py; 86 | then 87 | echo -e "\e[32m[STEP 4/6] Copy Monitor-App | Done\e[0m" 88 | else 89 | echo -e "\e[31m[STEP 4/6] Copy Monitor-App | Failed\e[0m" 90 | exit; 91 | fi 92 | 93 | # Deleting old logs 94 | echo -e "\e[96m[STEP 5/6] Deleting old logs...\e[90m" 95 | if sudo rm /home/appdaemon/.appdaemon/log/*; 96 | then 97 | echo -e "\e[32m[STEP 5/6] Deleting logs | Done\e[0m" 98 | else 99 | echo -e "\e[31m[STEP 5/6] Deleting logs | Failed\e[0m" 100 | exit; 101 | fi 102 | 103 | # Restarting AppDaemon 104 | echo -e "\e[96m[STEP 6/6] Restarting AppDaemon...\e[90m" 105 | if sudo systemctl restart appdaemon@appdaemon.service --now; 106 | then 107 | echo -e "\e[32m[STEP 6/6] Restart AppDaemon | Done\e[0m" 108 | else 109 | echo -e "\e[31m[STEP 6/6] Restart AppDaemon | Failed\e[0m" 110 | exit; 111 | fi 112 | 113 | echo -e "\e[0m" 114 | echo -e "\e[0m" 115 | echo -e "\e[0m" 116 | echo -e "\e[0m" 117 | echo -e "\e[32mUpdate finished!\e[0m" 118 | echo -e "\e[0m" 119 | echo -e "\e[32mPlease check logs to see if everything runs as expected.\e[0m" 120 | echo -e "\e[0m" 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monitor-App", 3 | "version": "2.4.1", 4 | "description": "AppDaemon Monitor-App to control The Monitor Presence Detection system", 5 | "main": "home_presence_app.py", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/Odianosen25/Monitor-App.git" 9 | } 10 | } 11 | --------------------------------------------------------------------------------