diff --git a/ChangeLog b/ChangeLog index aa37077..5cf19f4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ # ChangeLog +Mon, 01 May 2023 16:51:08 -0600 v1.8.5 + +- Handle resubscription if MQTT broker restarts (#99) +- Simplify installation / setup documentation (#97) +- Repair bug in running shell to execute commands + Tue, 28 Mar 2023 23:24:03 -0600 v1.8.4 - Remove unused Requirements file from repo. (#92) diff --git a/Docs/images/Device-list.png b/Docs/images/Device-list.png new file mode 100644 index 0000000..b73aa6e Binary files /dev/null and b/Docs/images/Device-list.png differ diff --git a/Docs/images/Discovered-Device.png b/Docs/images/Discovered-Device.png new file mode 100644 index 0000000..8e52356 Binary files /dev/null and b/Docs/images/Discovered-Device.png differ diff --git a/ISP-RPi-mqtt-daemon.py b/ISP-RPi-mqtt-daemon.py index 0b9caec..68f6d83 100755 --- a/ISP-RPi-mqtt-daemon.py +++ b/ISP-RPi-mqtt-daemon.py @@ -34,7 +34,7 @@ except ImportError: apt_available = False -script_version = "1.8.4" +script_version = "1.8.5" script_name = 'ISP-RPi-mqtt-daemon.py' script_info = '{} v{}'.format(script_name, script_version) project_name = 'RPi Reporter MQTT2HA Daemon' @@ -146,14 +146,21 @@ def clean_identifier(name): def on_connect(client, userdata, flags, rc): global mqtt_client_connected if rc == 0: - print_line('* MQTT connection established', - console=True, sd_notify=True) + print_line('* MQTT connection established', console=True, sd_notify=True) print_line('') # blank line?! #_thread.start_new_thread(afterMQTTConnect, ()) mqtt_client_connected = True print_line('on_connect() mqtt_client_connected=[{}]'.format( mqtt_client_connected), debug=True) - client.on_publish = on_publish + + # ------------------------------------------------------------------------- + # Commands Subscription + if (len(commands) > 0): + print_line('MQTT subscription to {}/+ enabled'.format(command_base_topic), console=True, sd_notify=True) + mqtt_client.subscribe('{}/+'.format(command_base_topic)) + else: + print_line('MQTT subscripton to {}/+ disabled'.format(command_base_topic), console=True, sd_notify=True) + # ------------------------------------------------------------------------- else: print_line('! Connection error with result code {} - {}'.format(str(rc), @@ -167,6 +174,13 @@ def on_connect(client, userdata, flags, rc): # kill main thread os._exit(1) +def on_disconnect(client, userdata, mid): + global mqtt_client_connected + mqtt_client_connected = False + print_line('* MQTT connection lost', console=True, sd_notify=True) + print_line('on_disconnect() mqtt_client_connected=[{}]'.format( + mqtt_client_connected), debug=True) + pass def on_publish(client, userdata, mid): #print_line('* Data successfully published.') @@ -179,16 +193,27 @@ def on_publish(client, userdata, mid): def on_subscribe(client, userdata, mid, granted_qos): print_line('on_subscribe() - {} - {}'.format(str(mid),str(granted_qos)), debug=True, sd_notify=True) +shell_cmd_fspec = '' def on_message(client, userdata, message): - print_line('on_message() Topic=[{}] payload=[{}]'.format(message.topic, message.payload), console=True, sd_notify=True, debug=True) + global shell_cmd_fspec + if shell_cmd_fspec == '': + shell_cmd_fspec = getShellCmd() + if shell_cmd_fspec == '': + print_line('* Failed to locate shell Command!', error=True) + # kill main thread + os._exit(1) decoded_payload = message.payload.decode('utf-8') command = message.topic.split('/')[-1] + print_line('on_message() Topic=[{}] payload=[{}] command=[{}]'.format(message.topic, message.payload, command), console=True, sd_notify=True, debug=True) if command != 'status': if command in commands: print_line('- Command "{}" Received - Run {} {} -'.format(command, commands[command], decoded_payload), console=True, debug=True) - subprocess.Popen(["/usr/bin/sh", "-c", commands[command].format(decoded_payload)]) + pHandle = subprocess.Popen([shell_cmd_fspec, "-c", commands[command].format(decoded_payload)]) + output, errors = pHandle.communicate() + if errors: + print_line('- Command exec says: errors=[{}]'.format(errors), console=True, debug=True) else: print_line('* Invalid Command received.', error=True) @@ -920,6 +945,17 @@ def getVcGenCmd(): print_line('Found vcgencmd(1)=[{}]'.format(desiredCommand), debug=True) return desiredCommand +def getShellCmd(): + cmd_locn1 = '/usr/bin/sh' + cmd_locn2 = '/bin/sh' + desiredCommand = cmd_locn1 + if os.path.exists(desiredCommand) == False: + desiredCommand = cmd_locn2 + if os.path.exists(desiredCommand) == False: + desiredCommand = '' + if desiredCommand != '': + print_line('Found sh(1)=[{}]'.format(desiredCommand), debug=True) + return desiredCommand def getIPCmd(): cmd_locn1 = '/bin/ip' @@ -1248,8 +1284,11 @@ def isAliveTimerRunning(): print_line('Connecting to MQTT broker ...', verbose=True) mqtt_client = mqtt.Client() +# hook up MQTT callbacks mqtt_client.on_connect = on_connect - +mqtt_client.on_disconnect = on_disconnect +mqtt_client.on_publish = on_publish +mqtt_client.on_message = on_message mqtt_client.will_set(lwt_sensor_topic, payload=lwt_offline_val, retain=True) mqtt_client.will_set(lwt_command_topic, payload=lwt_offline_val, retain=True) @@ -1281,16 +1320,6 @@ def isAliveTimerRunning(): error=True, sd_notify=True) sys.exit(1) else: - # ------------------------------------------------------------------------- - # Commands Subscription - if (len(commands) > 0): - print_line('MQTT subscription to {}/+ enabled'.format(command_base_topic), console=True, sd_notify=True) - mqtt_client.on_message = on_message - mqtt_client.subscribe('{}/+'.format(command_base_topic)) - else: - print_line('MQTT subscripton to {}/+ disabled'.format(command_base_topic), console=True, sd_notify=True) - # ------------------------------------------------------------------------- - mqtt_client.publish(lwt_sensor_topic, payload=lwt_online_val, retain=False) mqtt_client.publish(lwt_command_topic, payload=lwt_online_val, retain=False) mqtt_client.loop_start() diff --git a/README.md b/README.md index c5078e2..f62cfc5 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Additional pages: ## Features -- Tested on Raspberry Pi's 2/3/4 with Jessie, Stretch and Buster -- Tested with Home Assistant v0.111.0 -> 2023.2.5 -- Tested with Mosquitto broker v5.1 - v6.1.3 +- Tested on Raspberry Pi's zero, 2, 3, and 4 with Jessie, Stretch, Buster, and Bullseye +- Tested with Home Assistant v0.111.0 -> 2023.4.6 +- Tested with Mosquitto broker - Data is published via MQTT - MQTT discovery messages are sent so RPi's are automatically registered with Home Assistant (if MQTT discovery is enabled in your HA installation) - MQTT authentication support @@ -129,7 +129,7 @@ First install extra packages the script needs (select one of the two following c ### Packages for Ubuntu, Raspberry pi OS, and the like ```shell -sudo apt-get install git python3 python3-pip python3-tzlocal python3-sdnotify python3-colorama python3-unidecode python3-apt python3-paho-mqtt +sudo apt-get install git python3 python3-pip python3-tzlocal python3-sdnotify python3-colorama python3-unidecode python3-apt python3-paho-mqtt python3-requests ``` ### Additional Packages for pure Ubuntu @@ -234,6 +234,8 @@ password = {your mqtt password if your setup requires one} Now that your config.ini is setup let's test! +**NOTE:** *If you wish to support remote commanding of your RPi then you can find additional configuration steps in [Setting up RPi Control from Home Assistant](./RMTCTRL.md) However, to simplifly your effort, please complete the following steps to ensure all is running as desired before you attempt to set up remote control.* + ## Execution ### Initial Test @@ -278,6 +280,8 @@ $ daemon : daemon video # ^^^^^ now it is present ``` +*NOTE: Yes, `video` is correct. This appears to be due to our accessing the GPU temperatures.* + ### Choose Run Style You can choose to run this script as a `systemd service` or as a `Sys V init script`. If you are on a newer OS than `Jessie` or if as a system admin you are just more comfortable with Sys V init scripts then you can use the latter style. @@ -458,6 +462,15 @@ This data can be subscribed to and processed by your home assistant installation ## Troubleshooting +### Issue: I've updated my RPi OS and now I'm getting reporter script startup errors + +Most often fix: _Re-add the video perms to the daemon group_ + +See Closed Issues: [#94](https://github.com/ironsheep/RPi-Reporter-MQTT2HA-Daemon/issues/94), [#98](https://github.com/ironsheep/RPi-Reporter-MQTT2HA-Daemon/issues/98) + +We occasionaly have reports of users who updated their RPi afterwhich the RPI reporter Daemon script fails to start. The issue is that one of the packages updated appears to have reset the `daemon` group perminsions. For instructions on resetting the permissions to what is needed see: [Set up daemon account to allow access to temperature values](https://github.com/ironsheep/RPi-Reporter-MQTT2HA-Daemon#set-up-daemon-account-to-allow-access-to-temperature-values) + + ### Issue: Some of my RPi's don't show up in HA Most often fix: _install the missing package._ diff --git a/RMTECTRL.md b/RMTECTRL.md index 92e351b..1dd36ec 100644 --- a/RMTECTRL.md +++ b/RMTECTRL.md @@ -64,24 +64,42 @@ The Daemon also reports five topics for each RPi device: ### RPi MQTT Command Topics -Once the commanding is enable then the Daemon also reports the commanding interface for the RPi. By default we've provided examples for enabling three commands (See `config.ini.dist`.) This is what the commanding interface looks like when all threee are enabled: +Once the commanding is enabled then the Daemon also reports the commanding interface for the RPi. By default we've provided examples for enabling three commands (See `config.ini.dist`.) This is what the commanding interface looks like when all three are enabled: | Name | Device Class | Description | | --------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `~/shutdown` | button | Send request to this endpoint to shut the RPi down | | `~/reboot` | button | Send request to this endpoint to reboot the RPi | -| `~/restart_service` | button | Send request to this endpoint to restart the Daemon service +| `~/restart_service` | button | Send request to this endpoint to restart the Daemon service +The new content in `config.ini.dist` should look like something like this: + +```shell +[Commands] +#shutdown = /usr/bin/sudo /sbin/shutdown -h now 'shutdown rqst via MQTT' +#reboot = /usr/bin/sudo /sbin/shutdown -r now 'reboot rqst via MQTT' +#restart_service = /usr/bin/sudo systemctl restart isp-rpi-reporter.service +``` + ## Configuring the Daemon -By adding more information to your configuration `config.ini` you will now be able to add and execute commands in the monitored Raspberry Pis using MQTT, meaning yes, from buttons in your Home Assistant interface! +By adding commands you'd like to initiate remotely to your configuration file `config.ini` you will then be able to execute these commands on the monitored Raspberry Pis using MQTT, meaning yes, from buttons in your Home Assistant interface! + +After adding the comamnds to your `config.ini`, you then need to accomplish a couple more steps to activate these commands. Here is the overall list of 4 steps \[S1-S4\] we need to take: + +- S1: Place commands you wish to use into your `config.ini` +- S2: Verify the path to each command you are going to use + - Ensure the command paths are correct in the newly copied lines in your `config.ini' +- S3: Enable the `Daemon` user to run these commands (by modifying the [sudo(8)](https://linux.die.net/man/8/sudo) config file. See: [sudoers(5)](https://linux.die.net/man/5/sudoers)) +- S4: Ensure our monitor script has the correct ownership so the daemon can run it and so sudo can verify it as a valid script to run commands. -We've provided examples in `config.ini.dist` in the `[Commands]` section +**NOTE:** *Every time you want to add another command, you will need to repeat these steps for the new command.* -### New configuration options +### S1: Add new configuration options -Added the new `[Commands]` section to `config.ini`. -An example to reboot or shutdown the Pi: +Copy this new `[Commands]` section from the `config.ini.dist` (which we provide for reference) to your `config.ini` and then uncomment the commands you wish to activate (by removing the leading '#' character on each line). + +In the following we've enabled all three commands which now gives us commands to reboot, shutdown, or restart the RPi reporting service on the Pi: ```shell [Commands] @@ -90,78 +108,124 @@ reboot = /usr/bin/sudo /sbin/shutdown -r now 'reboot rqst via MQTT' restart_service = /usr/bin/sudo systemctl restart isp-rpi-reporter.service ``` -*NOTE* the message in the `{action} rqst via MQTT` message is logged in `/var/log/auth.log` so one can keep track of when commands are executed via MQTT. +*NOTE* the message in the `{action} rqst via MQTT` message (shutdown or reboot) is logged in `/var/log/auth.log` so one can keep track of when commands are executed via MQTT. -*If you wish, you can simply add all three that we provide in the examples.* +### S2: Verify absolute path to each command you use + +By default we want to keep our RPi security very tight. To that end, we actually specify absolute paths for commands that we want the Daemon to be able to execute. + +In some systems the path for `systemctl` / `reboot` / `shutdown` can be different. Make sure the path you specify is correct for your system. + +You can do a quick check of what the actual path is by using the `type` command: +```bash +$ type systemctl shutdown +systemctl is /usr/bin/systemctl +shutdown is /usr/sbin/shutdown +``` + +Ensure that the new lines you added to your `config.ini` use exactly these full paths. + +**NOTE:** *We use absolute paths so that user scripts or other commands can't be substituted for the executables we expect to be run. (If these alternates happened to be in the search path and were found before the ones we want then the alternate commands would be run by our script. The effect of this happening can range from nothing happening to seriously malicious things happening. So we just avoid the possibility by using absolute paths in this way.)* + +### S3: Enabling the Daemon to run external commands + +Again, by default, we want to keep our RPi security very tight. To that end, we actually specify each command that we want the Daemon to be able to execute. We do this my making changes to the sudo(8) control file `/etc/sudoers` -## Enabling the Daemon to run external commands +The "daemon" user we use to start the daemon in the installation instructions doesn't have enough privileges to reboot or power down the computer. The workaround we'll use is to give permissions to daemon to be able to run the commands we want to execute using the sudoers configuration file. There is an older and a newer way to do this. Here are both ways #1, and #2 (choose #1 if the directory is present): -By default we want to keep our RPi security very tight. To that end we actually specify each command that we want the Daemon to be able to execute. We do this my making changes to the sudo(8) control file `/etc/sudoers` +#### Alternate #1: The /etc/sudoers.d/ directory is present -The "daemon" user proposed to start the daemon in the installation instructions doesn't have enough privileges to reboot or -power down the computer. A possible workaround is to give permissions to daemon to the commands we want to execute using -the sudoers configuration file: +On my newer systems there is an `/etc/sudoers.d/` directory. In that directory I create a new file numbering it so it is read later in the loading effort. I use the name `020_daemon` for this file. + +*NOTE: in the following you MUST replace `` with the name of your RPi. So in the case of my RPi with a hostname of `rpibtle.home` I replace `` with `rpibtle`* ```shell # edit sudoers file - sudo vim /etc/sudoers + sudo visudo -f /etc/sudoers.d/020_daemon + + # add the following lines to the empty file: - # add the following lines at the bottom. # note that every service that we want to allow to restart must be specified here daemon =NOPASSWD: /usr/bin/systemctl restart isp-rpi-reporter.service,/sbin/shutdown ``` + +**NOTE:** *We use visudo(8) so that we are not locked out of our system if we leave an error in our sudoers file! This is VERY IMPORTANT, please be careful. If yhou do get into trouble refer to [How to restore a broken sudoers file without being able to use sudo](https://unix.stackexchange.com/questions/677591/how-to-restore-a-broken-sudoers-file-without-being-able-to-use-sudo) to get things working again.* -NOTE: In some systems the path for `systemctl` / `reboot` / `shutdown` can be different. Make sure the path you specify is correct for your system. +#### Alternate #2: No /etc/sudoers.d/ directory found -You can do a quick check of what the actual path is by using the `type` command: +When the directory is not found then we, instead, add our new content to the existing control file by placing it at the end of the file (after all existing content): -```bash -$ type shutdown -shutdown is /sbin/shutdown -``` +*NOTE: in the following you MUST replace `` with the name of your RPi. So in the case of my RPi with a hostname of `rpibtle.home` I replace `` with `rpibtle`* + + ```shell + # edit sudoers file + sudo visudo + + # add the following lines at the bottom of the file + + # note that every service that we want to allow to restart must be specified here + daemon =NOPASSWD: /usr/bin/systemctl restart isp-rpi-reporter.service,/sbin/shutdown + ``` + +**NOTE:** *We use visudo(8) so that we are not locked out of our system if we leave an error in our sudoers file! This is VERY IMPORTANT, please be careful. If yhou do get into trouble refer to [How to restore a broken sudoers file without being able to use sudo](https://unix.stackexchange.com/questions/677591/how-to-restore-a-broken-sudoers-file-without-being-able-to-use-sudo) to get things working again.* + +### S4: Adjusting the ownership of our reporter script -Additionally, the daemon user needs permission to execute the shell script referenced in the run-script command (and any command referenced there/access to the directories specified). If the script has been created by the standard pi user, a simple workaround could be: +Additionally, the daemon user needs permission to execute the shell script referenced in the run-script command (and any command referenced there/access to the directories specified). If the script has been created by the standard pi user, a simple workaround is: ```shell -chown daemon RPi-mqtt-daemon-script.sh +sudo chown daemon RPi-mqtt-daemon-script.sh ``` - ## Verifying your configuration -After getting this configured you'll want to verify that everying is configured correctly. I recommend the following steps (it's what I do...): +After getting this configured you'll want to verify that everying is configured correctly. I recommend the following 4 steps \[V1-V4\] (it's what I do...): -- Restart the daemon -- Use a tool like [MQTT Explorer](http://mqtt-explorer.com/) to verifiy that the new MQTT command interface appeared -- Build a quick card to test a command from HA -- Ensure the action occurred, if you were logged in did you see a 'wall message'? -- Verify the message appeared in the logs +- V1: Restart the daemon + - Use a tool like [MQTT Explorer](http://mqtt-explorer.com/) to verifiy that the new MQTT command interface appeared +- V2: Use HA's device page to test your new commands +- V3: Ensure the action occurred, if you were logged in did you see a 'wall message'? +- V4: Verify taht the action appeared in the logs Let's go into a bit more detail for some of these steps. -### Restart the daemon +### V1: Restart the daemon You'll need to restart the Daemon or reboot the RPi to get your changes to take effect. Then you'll want to see if the new control interface is exposed. I check out what's appearing in MQTT by using a tool like [MQTT Explorer](http://mqtt-explorer.com/). -### Build a quick card to test a command from HA +### V2: Use the card built by Home Assistant Discovery to test your new controls -Refer to the [Lovelace RPi Monitor Card](https://github.com/ironsheep/lovelace-rpi-monitor-card) page for details but there is a [specific example button card](https://github.com/ironsheep/lovelace-rpi-monitor-card#example-control-of-your-rpi-avail-in-daemon-v180-and-later) +In your Home Assistant Interface navigate to: Settings->Devices & Services->Devices and scroll to the device that represents your RPi just configured. -This was originally built by copying the card suggested by looking at the RPi Device as discovered by home assistant. In that display it shows an example interface card and allows you to copy the suggestion to your clipboard. I then pasted this card into the page yaml where I wanted the card to be shown. I then overrode the names with simple more direct names than the default button names. That's it. It just worked. +You should see somthing like this: +![Discovery List](./Docs/images/Device-list.png) + +Next click on the device that is your RPi. I'll click on RPi-pibtle.home in this list. + +You should now see something like this: +![Discovery List](./Docs/images/Discovered-Device.png) + +The green arrow (above) points out the control interface that device discovery created for you. -### Ensure the action occurred + +### V3: Ensure the action occurred Next I pressed the reboot button on the new interface. I was logged into the RPi at the time so when the reboot occurred it kicked me off which told me it was working well. -### Verify the message appeared in the logs +### V4: Verify that the message appeared in the logs -Lastly I wanted to ensure the action was logged so I did a simple grep for "via" in the `/var/log/augh.log` file and sure enough there was the entry. +Lastly I wanted to ensure the action was logged so I did a simple grep for "via" in the `/var/log/auth.log` file and sure enough there was the entry. With this finding i've verified that this is all working for me! (*now you can do the same!*) +## Want to go further? Build a custom HA RPi Control card + +Refer to the [Lovelace RPi Monitor Card](https://github.com/ironsheep/lovelace-rpi-monitor-card) page for details but there is a [specific example button card](https://github.com/ironsheep/lovelace-rpi-monitor-card#example-control-of-your-rpi-avail-in-daemon-v180-and-later) + +This was originally built by copying the card suggested by looking at the RPi Device as discovered by home assistant. In that display it shows an example interface card and allows you to copy the suggestion to your clipboard. I then pasted this card into the page yaml where I wanted the card to be shown. I then overrode the names with simple more direct names than the default button names. That's it. It just worked. ---