Source code for fauxmo.plugins.commandlineplugin

"""Fauxmo plugin that runs a command on the local machine.

Runs a `shlex`ed command using `subprocess.run`, keeping the default of
`shell=False`. This is probaby frought with security concerns, which is why
this plugin is not included by default in `fauxmo.plugins`. By installing or
using it, you acknowledge that it could run commands from your config.json that
could lead to data compromise, corruption, loss, etc. Consider making your
config.json read-only. If there are parts of this you don't understand, you
should probably not use this plugin.

If the command runs with a return code of 0, Alexa should respond prompty
"Okay" or something that indicates it seems to have worked. If the command has
a return code of anything other than 0, Alexa stalls for several seconds and
subsequently reports that there was a problem (which should notify the user
that something didn't go as planned).

Note that `subprocess.run` as implemented in this plugin doesn't handle complex
commands with pipes, redirection, or multiple statements joined by `&&`, `||`,
`;`, etc., so you can't just use e.g. `"command that sometimes fails || true"`
to avoid the delay and Alexa's response. If you really want to handle more
complex commands, consider using this plugin as a template for another one
using `os.system` instead of `subprocess.run`, but realize that this comes with
substantial security risks that exceed my ability to explain.

Example config:
```
{
  "FAUXMO": {
    "ip_address": "auto"
  },
  "PLUGINS": {
    "CommandLinePlugin": {
      "path": "/path/to/commandlineplugin.py",
      "DEVICES": [
        {
            "name": "output stuff to a file",
            "port": 49915,
            "on_cmd": "touch testfile.txt",
            "off_cmd": "rm testfile.txt",
            "state_cmd": "ls testfile.txt"
        },
        {
            "name": "command with fake state",
            "port": 49916,
            "on_cmd": "touch testfile.txt",
            "off_cmd": "rm testfile.txt",
            "use_fake_state": true
        }
      ]
    }
  }
}
```
"""

import shlex
import subprocess

from fauxmo.plugins import FauxmoPlugin


[docs]class CommandLinePlugin(FauxmoPlugin): """Fauxmo Plugin for running commands on the local machine."""
[docs] def __init__( self, name: str, port: int, on_cmd: str, off_cmd: str, state_cmd: str = None, use_fake_state: bool = False, ) -> None: """Initialize a CommandLinePlugin instance. Args: name: Name for this Fauxmo device port: Port on which to run a specific CommandLinePlugin instance on_cmd: Command to be called when turning device on off_cmd: Command to be called when turning device off state_cmd: Command to check device state (return code 0 == on) use_fake_state: If `True`, override `get_state` to return the latest action as the device state. NB: The proper json boolean value for Python's `True` is `true`, not `True` or `"true"`. """ self.on_cmd = on_cmd self.off_cmd = off_cmd self.state_cmd = state_cmd self.use_fake_state = use_fake_state super().__init__(name=name, port=port)
[docs] def run_cmd(self, cmd: str) -> bool: """Partialmethod to run command. Args: cmd: Command to be run Returns: True if command seems to have run without error """ shlexed_cmd = shlex.split(cmd) process = subprocess.run(shlexed_cmd) return process.returncode == 0
[docs] def on(self) -> bool: """Run on command. Returns: True if command seems to have run without error. """ return self.run_cmd(self.on_cmd)
[docs] def off(self) -> bool: """Run off command. Returns: True if command seems to have run without error. """ return self.run_cmd(self.off_cmd)
[docs] def get_state(self) -> str: """Get device state. NB: Return code of `0` (i.e. ran without error) indicates "on" state, otherwise will be off. making it easier to have something like `ls path/to/pidfile` suggest `on`. Many command line switches may not actually have a "state" per se (just an arbitary command you want to run), in which case you could just put "false" as the command, which should always return "off". Returns: "on" or "off" if `state_cmd` is defined, "unknown" if undefined """ if self.use_fake_state is True: return super().get_state() if self.state_cmd is None: return "unknown" returned_zero = self.run_cmd(self.state_cmd) if returned_zero is True: return "on" return "off"