Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
# Conflicts:
#	amazon_dash/__init__.py
#	amazon_dash/install/__init__.py
  • Loading branch information
Nekmo committed Jul 18, 2018
2 parents 199214c + 0d393c8 commit 6eaa7b7
Show file tree
Hide file tree
Showing 23 changed files with 337 additions and 64 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6. Check
3. The pull request should work for Python 2.7, 3.4, 3.5, 3.6 and 3.7. Check
https://travis-ci.org/Nekmo/amazon-dash/pull_requests
and make sure that the tests pass for all supported Python versions.

Expand Down
12 changes: 12 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
History
=======

v1.1.0 (2018-07-18)
-------------------

- Home Assistant authentication
- Improved installation process compatibility
- Sniffing network interface
- System Command over SSH
- Python 3.7 compatibility
- Fixed OS X compatibility
- IFTTT support


v1.0.0 (2018-03-13)
-------------------

Expand Down
22 changes: 16 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,35 @@ Also available on `AUR <https://aur.archlinux.org/packages/amazon-dash-git/>`_.
settings:
delay: 10
devices:
0C:47:C9:98:4A:12:
0C:47:C9:98:4A:12: # Command example
name: Hero
user: nekmo
cmd: spotify
AC:63:BE:67:B2:F1:
AC:63:BE:75:1B:6F: # SSH example
name: Tassimo
cmd: door --open
ssh: 192.168.1.23:2222
AC:63:BE:67:B2:F1: # Url Webhook example
name: Kit Kat
url: 'http://domain.com/path/to/webhook'
method: post
content-type: json
body: '{"mac": "AC:63:BE:67:B2:F1", "action": "toggleLight"}'
confirmation: send-tg
40:B4:CD:67:A2:E1:
40:B4:CD:67:A2:E1: # Home Assistant example
name: Fairy
homeassistant: hassio.local
event: toggle_kitchen_light
18:74:2E:87:01:F2:
18:74:2E:87:01:F2: # OpenHAB example
name: Doritos
openhab: 192.168.1.140
item: open_door
state: "ON"
44:65:0D:75:A7:B2: # IFTTT example
name: Pompadour
ifttt: cdxxx-_gEJ3wdU04yyyzzz
event: pompadour_button
data: {"value1": "Pompadour button"}
confirmations:
send-tg:
service: telegram
Expand All @@ -108,8 +117,8 @@ The following execution methods are supported with your Amazon Dash button with
================================ ================================ ================================
.. image:: https://goo.gl/VqgMZJ .. image:: https://goo.gl/a6TS7X .. image:: https://goo.gl/zrjisq
`System command`_ `Call url`_ `Homeassistant`_
.. image:: https://goo.gl/Cq4bYC
`OpenHAB`_
.. image:: https://goo.gl/Cq4bYC .. image:: https://goo.gl/L7ng8k
`OpenHAB`_ `IFTTT`_
================================ ================================ ================================


Expand Down Expand Up @@ -166,4 +175,5 @@ See all the examples `in the community`_.
.. _Call url: http://docs.nekmo.org/amazon-dash/config_file.html#call-url
.. _Homeassistant: http://docs.nekmo.org/amazon-dash/config_file.html#homeassistant-event
.. _OpenHAB: http://docs.nekmo.org/amazon-dash/config_file.html#openhab-event
.. _IFTTT: http://docs.nekmo.org/amazon-dash/config_file.html#ifttt-event
.. _in the community: http://docs.nekmo.org/amazon-dash/community.html
Binary file modified amazon-dash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion amazon_dash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

__version__ = '1.0.4'
__version__ = '1.1.0'
13 changes: 11 additions & 2 deletions amazon_dash/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"properties": {
"delay": {
"type": "integer"
}
},
"interface": {
"type": "string"
},
}
},
"devices": {
Expand Down Expand Up @@ -78,6 +81,9 @@
"homeassistant": {
"type": "string"
},
"ifttt": {
"type": "string"
},
"event": {
"type": "string"
},
Expand Down Expand Up @@ -138,7 +144,10 @@ def get_file_group(file):
:return: group id
:rtype: int
"""
return getgrgid(os.stat(file).st_uid)[0]
try:
return getgrgid(os.stat(file).st_uid)[0]
except KeyError:
return '???'


def bitperm(s, perm, pos):
Expand Down
4 changes: 2 additions & 2 deletions amazon_dash/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ def discovery_print(pkt):
click.secho(text, fg='magenta') if 'Amazon' in text else click.echo(text)


def discover():
def discover(interface=None):
"""Print help and scan devices on screen.
:return: None
"""
click.secho(HELP, fg='yellow')
scan_devices(discovery_print, lfilter=lambda d: d.src not in mac_id_list)
scan_devices(discovery_print, lfilter=lambda d: d.src not in mac_id_list, iface=interface)
81 changes: 69 additions & 12 deletions amazon_dash/execute.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
import logging
import threading

import getpass

Expand Down Expand Up @@ -73,10 +72,33 @@ def execute_cmd(cmd, cwd=None, timeout=5):
return stdout, stderr


def execute_over_ssh(cmd, ssh, cwd=None, shell='bash'):
"""Excecute command on remote machine using SSH
:param cmd: Command to execute
:param ssh: Server to connect. Port is optional
:param cwd: current working directory
:return: None
"""
port = None
parts = ssh.split(':', 1)
if len(parts) > 1 and not parts[1].isdigit():
raise InvalidConfig(extra_body='Invalid port number on ssh config: {}'.format(parts[1]))
elif len(parts) > 1:
port = parts[1]
quoted_cmd = ' '.join([x.replace("'", """'"'"'""") for x in cmd.split(' ')])
remote_cmd = ' '.join([
' '.join(get_shell(shell)), # /usr/bin/env bash
' '.join([EXECUTE_SHELL_PARAM, "'", ' '.join((['cd', cwd, ';'] if cwd else []) + [quoted_cmd]), "'"])],
)
return ['ssh', parts[0]] + (['-p', port] if port else []) + ['-C'] + [remote_cmd]


class Execute(object):
"""Execute base class
"""

def __init__(self, name, data):
"""
Expand Down Expand Up @@ -129,13 +151,20 @@ def execute(self, root_allowed=False):
:param bool root_allowed: Allow execute as root commands
:return:
"""
if self.user == ROOT_USER and not root_allowed:
if self.user == ROOT_USER and not root_allowed and not self.data.get('ssh'):
raise SecurityException('For security, execute commands as root is not allowed. '
'Use --root-allowed to allow executing commands as root. '
' It is however recommended to add a user to the configuration '
'of the device (device: {})'.format(self.name))
cmd = run_as_cmd(self.data['cmd'], self.user)
output = execute_cmd(cmd, self.data.get('cwd'))
if self.data.get('user') and self.data.get('ssh'):
raise InvalidConfig('User option is unsupported in ssh mode. The ssh user must be defined in '
'the ssh option. For example: user@machine')
if self.data.get('ssh'):
cmd = execute_over_ssh(self.data['cmd'], self.data['ssh'], self.data.get('cwd'))
output = execute_cmd(cmd)
else:
cmd = run_as_cmd(self.data['cmd'], self.user)
output = execute_cmd(cmd, self.data.get('cwd'))
if output:
return output[0]

Expand Down Expand Up @@ -275,6 +304,19 @@ def get_url(self):
url += ':{}'.format(self.default_port)
return url

def get_body(self):
"""Return "data" value on self.data
:return: data to send
:rtype: str
"""
if self.default_body:
return self.default_body
data = self.data.get('data')
if isinstance(data, dict):
return json.dumps(data)
return data


class ExecuteHomeAssistant(ExecuteOwnApiBase):
"""Send Home Assistant event
Expand All @@ -301,14 +343,6 @@ def get_headers(self):
'x-ha-access': self.data['access']
} if 'access' in self.data else {}

def get_body(self):
"""Return "data" value on self.data
:return: data to send
:rtype: str
"""
return self.data.get('data')


class ExecuteOpenHab(ExecuteOwnApiBase):
"""Send Open Hab event
Expand Down Expand Up @@ -336,3 +370,26 @@ def get_url(self):

def get_body(self):
return self.data.get('state', 'ON')


class ExecuteIFTTT(ExecuteOwnApiBase):
"""Send IFTTT Webhook event.
"""
execute_name = 'ifttt'
url_pattern = 'https://maker.ifttt.com/trigger/{event}/with/key/{key}'

def get_url(self):
"""IFTTT Webhook url
:return: url
:rtype: str
"""
if not self.data[self.execute_name]:
raise InvalidConfig(extra_body='Value for IFTTT is required on {} device. Get your key here: '
'https://ifttt.com/services/maker_webhooks/settings'.format(self.name))
if not self.data.get('event'):
raise InvalidConfig(extra_body='Event option is required for IFTTT on {} device. '
'You define the event name when creating a Webhook '
'applet'.format(self.name))
url = self.url_pattern.format(event=self.data['event'], key=self.data[self.execute_name])
return url
16 changes: 12 additions & 4 deletions amazon_dash/install/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,25 @@
'/usr/lib/systemd/system',
'/lib/systemd/system',
]
__dir__ = os.path.dirname(os.path.abspath(__file__))
CONFIG_EXAMPLE = os.path.join(__dir__, 'amazon-dash.yml')
SYSTEMD_SERVICE = os.path.join(__dir__, 'services', 'amazon-dash.service')
dirname = os.path.dirname(os.path.abspath(__file__))
CONFIG_EXAMPLE = os.path.join(dirname, 'amazon-dash.yml')
SYSTEMD_SERVICE = os.path.join(dirname, 'services', 'amazon-dash.service')


if sys.version_info < (3,0):
FileNotFoundError = OSError


def get_pid(name):
return check_output(["pidof", name])


def get_init_system():
return check_output(['ps', '--no-headers', '-o', 'comm', '1']).strip(b'\n ').decode('utf-8')
try:
return check_output(['ps', '--no-headers', '-o', 'comm', '1']).strip(b'\n ').decode('utf-8')
except FileNotFoundError:
raise IsInstallableException('"ps" command is unavailable on your OS. systemd.'
'The systemd check could not be finalized.')


def get_systemd_services_path():
Expand Down
12 changes: 12 additions & 0 deletions amazon_dash/install/amazon-dash.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ devices:
user: your-user # System user. Necessary if it is executed as root
cmd: spotify # Command to execute

## Example of how to execute a system command over SSH
# AC:63:BE:75:1B:6F:
# name: Tassimo
# cmd: door --open
# ssh: 192.168.1.23:2222

## Example of how to execute a url
# AC:63:BE:67:B2:F1:
Expand All @@ -33,6 +38,13 @@ devices:
# item: open_door # Openhab item. Required
# state: "ON" # item state to send. TOGGLE by default

## Example of how to execute a IFTTT Webhook event
# 44:65:0D:75:A7:B2:
# name: Pompadour
# ifttt: cdxxx-_gEJ3wdU04yyyzzz
# event: pompadour_button
# data: {"value1": "Pompadour button"}

## Uncomment this for use confirmations
# confirmations:
## Example of how to send a Telegram confirmation on execution success or failure
Expand Down
8 changes: 5 additions & 3 deletions amazon_dash/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from amazon_dash.config import Config
from amazon_dash.confirmations import get_confirmation
from amazon_dash.exceptions import InvalidConfig, InvalidDevice
from amazon_dash.execute import logger, ExecuteCmd, ExecuteUrl, ExecuteHomeAssistant, ExecuteOpenHab
from amazon_dash.execute import logger, ExecuteCmd, ExecuteUrl, ExecuteHomeAssistant, ExecuteOpenHab, ExecuteIFTTT
from amazon_dash.scan import scan_devices

DEFAULT_DELAY = 10
Expand All @@ -18,6 +18,7 @@
'url': ExecuteUrl,
'homeassistant': ExecuteHomeAssistant,
'openhab': ExecuteOpenHab,
'ifttt': ExecuteIFTTT,
}
"""
Execute classes registered.
Expand Down Expand Up @@ -88,6 +89,7 @@ def execute(self, root_allowed=False):
if result is None else result
result = result or 'The {} device has been executed successfully'.format(self.name)
self.send_confirmation(result)
return result

def send_confirmation(self, message, success=True):
"""Send success or error message to configured confirmation
Expand Down Expand Up @@ -151,7 +153,7 @@ def run(self, root_allowed=False):
:return: loop
"""
self.root_allowed = root_allowed
scan_devices(self.on_push, lambda d: d.src.lower() in self.devices)
scan_devices(self.on_push, lambda d: d.src.lower() in self.devices, self.settings.get('interface'))


def test_device(device, file, root_allowed=False):
Expand All @@ -166,4 +168,4 @@ def test_device(device, file, root_allowed=False):
config.read()
if not device in config['devices']:
raise InvalidDevice('Device {} is not in config file.'.format(device))
Device(device, config['devices'][device], config).execute(root_allowed)
logger.info(Device(device, config['devices'][device], config).execute(root_allowed))
5 changes: 3 additions & 2 deletions amazon_dash/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def test_device(device, config, root_allowed):


@cli.command(help='Discover Amazon Dash device on network.')
def discovery():
@click.option('--interface', help='Network interface.', default=None)
def discovery(interface):
from amazon_dash.discovery import discover
discover()
discover(interface)
4 changes: 2 additions & 2 deletions amazon_dash/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
PermissionError = socket.error


def scan_devices(fn, lfilter):
def scan_devices(fn, lfilter, iface=None):
"""Sniff packages
:param fn: callback on packet
:param lfilter: filter packages
:return: loop
"""
try:
sniff(prn=fn, store=0, filter="udp", lfilter=lfilter)
sniff(prn=fn, store=0, filter="udp", lfilter=lfilter, iface=iface)
except PermissionError:
raise SocketPermissionError
4 changes: 2 additions & 2 deletions amazon_dash/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from amazon_dash.exceptions import SecurityException, InvalidConfig
from amazon_dash.tests.base import FileMockBase

__dir__ = os.path.abspath(os.path.dirname(__file__))
config_data = open(os.path.join(__dir__, 'fixtures', 'config.yml')).read()
dirname = os.path.abspath(os.path.dirname(__file__))
config_data = open(os.path.join(dirname, 'fixtures', 'config.yml')).read()


try:
Expand Down
Loading

0 comments on commit 6eaa7b7

Please sign in to comment.