├── .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 | [](https://github.com/custom-components/hacs)
4 |
5 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------