mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2025-11-06 17:51:36 +00:00
Initial Configuration Push
This commit is contained in:
1
deps/roku/__init__.py
vendored
Normal file
1
deps/roku/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
from roku.core import Roku, Application, RokuException, __version__
|
||||
BIN
deps/roku/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/__pycache__/core.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/core.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/__pycache__/proxy.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/proxy.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/__pycache__/server.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/server.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/__pycache__/tests.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/__pycache__/tests.cpython-34.pyc
vendored
Normal file
Binary file not shown.
249
deps/roku/core.py
vendored
Normal file
249
deps/roku/core.py
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
import logging
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from six.moves.urllib_parse import urlparse
|
||||
|
||||
from roku import discovery
|
||||
|
||||
__version__ = '3.1.2'
|
||||
|
||||
roku_logger = logging.getLogger('roku')
|
||||
|
||||
COMMANDS = {
|
||||
'home': 'Home',
|
||||
'reverse': 'Rev',
|
||||
'forward': 'Fwd',
|
||||
'play': 'Play',
|
||||
'select': 'Select',
|
||||
'left': 'Left',
|
||||
'right': 'Right',
|
||||
'down': 'Down',
|
||||
'up': 'Up',
|
||||
'back': 'Back',
|
||||
'replay': 'InstantReplay',
|
||||
'info': 'Info',
|
||||
'backspace': 'Backspace',
|
||||
'search': 'Search',
|
||||
'enter': 'Enter',
|
||||
'literal': 'Lit',
|
||||
'volume_down': 'VolumeDown',
|
||||
'volume_mute': 'VolumeMute',
|
||||
'volume_up': 'VolumeUp',
|
||||
}
|
||||
|
||||
SENSORS = ('acceleration', 'magnetic', 'orientation', 'rotation')
|
||||
|
||||
TOUCH_OPS = ('up', 'down', 'press', 'move', 'cancel')
|
||||
|
||||
|
||||
class RokuException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Application(object):
|
||||
|
||||
def __init__(self, id, version, name, roku=None):
|
||||
self.id = str(id)
|
||||
self.version = version
|
||||
self.name = name
|
||||
self.roku = roku
|
||||
|
||||
def __repr__(self):
|
||||
return ('<Application: [%s] %s v%s>' %
|
||||
(self.id, self.name, self.version))
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
if self.roku:
|
||||
return self.roku.icon(self)
|
||||
|
||||
def launch(self):
|
||||
if self.roku:
|
||||
self.roku.launch(self)
|
||||
|
||||
def store(self):
|
||||
if self.roku:
|
||||
self.roku.store(self)
|
||||
|
||||
|
||||
class DeviceInfo(object):
|
||||
|
||||
def __init__(self, modelname, modelnum, swversion, sernum, userdevicename):
|
||||
self.modelname = modelname
|
||||
self.modelnum = modelnum
|
||||
self.swversion = swversion
|
||||
self.sernum = sernum
|
||||
self.userdevicename = userdevicename
|
||||
|
||||
def __repr__(self):
|
||||
return ('<DeviceInfo: %s-%s, SW v%s, Ser# %s, Name %s>' %
|
||||
(self.modelname, self.modelnum, self.swversion, self.sernum, self.userdevicename))
|
||||
|
||||
|
||||
class Roku(object):
|
||||
|
||||
@classmethod
|
||||
def discover(self, *args, **kwargs):
|
||||
rokus = []
|
||||
for device in discovery.discover(*args, **kwargs):
|
||||
o = urlparse(device.location)
|
||||
rokus.append(Roku(o.hostname, o.port))
|
||||
return rokus
|
||||
|
||||
def __init__(self, host, port=8060):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._conn = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<Roku: %s:%s>" % (self.host, self.port)
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
if name not in COMMANDS and name not in SENSORS:
|
||||
raise AttributeError('%s is not a valid method' % name)
|
||||
|
||||
def command(*args):
|
||||
if name in SENSORS:
|
||||
keys = ['%s.%s' % (name, axis) for axis in ('x', 'y', 'z')]
|
||||
params = dict(zip(keys, args))
|
||||
self.input(params)
|
||||
elif name == 'literal':
|
||||
for char in args[0]:
|
||||
path = '/keypress/%s_%s' % (COMMANDS[name], char.upper())
|
||||
self._post(path)
|
||||
else:
|
||||
path = '/keypress/%s' % COMMANDS[name]
|
||||
self._post(path)
|
||||
|
||||
return command
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = str(key)
|
||||
app = self._app_for_name(key)
|
||||
if not app:
|
||||
app = self._app_for_id(key)
|
||||
return app
|
||||
|
||||
def _app_for_name(self, name):
|
||||
for app in self.apps:
|
||||
if app.name == name:
|
||||
return app
|
||||
|
||||
def _app_for_id(self, app_id):
|
||||
for app in self.apps:
|
||||
if app.id == app_id:
|
||||
return app
|
||||
|
||||
def _connect(self):
|
||||
if self._conn is None:
|
||||
self._conn = requests.Session()
|
||||
|
||||
def _get(self, path, *args, **kwargs):
|
||||
return self._call('GET', path, *args, **kwargs)
|
||||
|
||||
def _post(self, path, *args, **kwargs):
|
||||
return self._call('POST', path, *args, **kwargs)
|
||||
|
||||
def _call(self, method, path, *args, **kwargs):
|
||||
|
||||
self._connect()
|
||||
|
||||
roku_logger.debug(path)
|
||||
|
||||
url = 'http://%s:%s%s' % (self.host, self.port, path)
|
||||
|
||||
if method not in ('GET', 'POST'):
|
||||
raise ValueError('only GET and POST HTTP methods are supported')
|
||||
|
||||
func = getattr(self._conn, method.lower())
|
||||
resp = func(url, *args, **kwargs)
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise RokuException(resp.content)
|
||||
|
||||
return resp.content
|
||||
|
||||
@property
|
||||
def apps(self):
|
||||
applications = []
|
||||
resp = self._get('/query/apps')
|
||||
root = ET.fromstring(resp)
|
||||
for app_node in root:
|
||||
app = Application(
|
||||
id=app_node.get('id'),
|
||||
version=app_node.get('version'),
|
||||
name=app_node.text,
|
||||
roku=self,
|
||||
)
|
||||
applications.append(app)
|
||||
return applications
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
resp = self._get('/query/device-info')
|
||||
root = ET.fromstring(resp)
|
||||
|
||||
dinfo = DeviceInfo(
|
||||
modelname=root.find('model-name').text,
|
||||
modelnum=root.find('model-number').text,
|
||||
swversion=''.join([
|
||||
root.find('software-version').text,
|
||||
'.',
|
||||
root.find('software-build').text
|
||||
]),
|
||||
sernum=root.find('serial-number').text,
|
||||
userdevicename=root.find('user-device-name').text
|
||||
)
|
||||
return dinfo
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
return sorted(COMMANDS.keys())
|
||||
|
||||
def icon(self, app):
|
||||
return self._get('/query/icon/%s' % app.id)
|
||||
|
||||
def launch(self, app):
|
||||
if app.roku and app.roku != self:
|
||||
raise RokuException('this app belongs to another Roku')
|
||||
return self._post('/launch/%s' % app.id)
|
||||
|
||||
def store(self, app):
|
||||
return self._post('/launch/11', params={'contentID': app.id})
|
||||
|
||||
def input(self, params):
|
||||
return self._post('/input', params=params)
|
||||
|
||||
def touch(self, x, y, op='down'):
|
||||
|
||||
if op not in TOUCH_OPS:
|
||||
raise RokuException('%s is not a valid touch operation' % op)
|
||||
|
||||
params = {
|
||||
'touch.0.x': x,
|
||||
'touch.0.y': y,
|
||||
'touch.0.op': op,
|
||||
}
|
||||
|
||||
self.input(params)
|
||||
|
||||
@property
|
||||
def current_app(self):
|
||||
resp = self._get('/query/active-app')
|
||||
root = ET.fromstring(resp)
|
||||
|
||||
app_node = root.find('screensaver')
|
||||
if app_node is None:
|
||||
app_node = root.find('app')
|
||||
|
||||
if app_node is None:
|
||||
return None
|
||||
|
||||
return Application(
|
||||
id=app_node.get('id'),
|
||||
version=app_node.get('version'),
|
||||
name=app_node.text,
|
||||
roku=self,
|
||||
)
|
||||
67
deps/roku/discovery.py
vendored
Normal file
67
deps/roku/discovery.py
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Code adapted from Dan Krause.
|
||||
https://gist.github.com/dankrause/6000248
|
||||
http://github.com/dankrause
|
||||
"""
|
||||
import socket
|
||||
import six
|
||||
from six.moves import http_client
|
||||
|
||||
ST_DIAL = 'urn:dial-multiscreen-org:service:dial:1'
|
||||
ST_ECP = 'roku:ecp'
|
||||
|
||||
|
||||
class _FakeSocket(six.BytesIO):
|
||||
def makefile(self, *args, **kw):
|
||||
return self
|
||||
|
||||
|
||||
class SSDPResponse(object):
|
||||
|
||||
def __init__(self, response):
|
||||
self.location = response.getheader('location')
|
||||
self.usn = response.getheader('usn')
|
||||
self.st = response.getheader('st')
|
||||
self.cache = response.getheader('cache-control').split('=')[1]
|
||||
|
||||
def __repr__(self):
|
||||
return '<SSDPResponse({location}, {st}, {usn})'.format(**self.__dict__)
|
||||
|
||||
|
||||
def discover(timeout=2, retries=1, st=ST_ECP):
|
||||
|
||||
group = ('239.255.255.250', 1900)
|
||||
|
||||
message = '\r\n'.join([
|
||||
'M-SEARCH * HTTP/1.1',
|
||||
'HOST: {0}:{1}'.format(*group),
|
||||
'MAN: "ssdp:discover"',
|
||||
'ST: {st}', 'MX: 3', '', ''])
|
||||
|
||||
socket.setdefaulttimeout(timeout)
|
||||
|
||||
responses = {}
|
||||
|
||||
for _ in range(retries):
|
||||
sock = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
|
||||
m = message.format(st=st)
|
||||
if six.PY2:
|
||||
sock.sendto(m, group)
|
||||
elif six.PY3:
|
||||
sock.sendto(m.encode(), group)
|
||||
|
||||
while 1:
|
||||
try:
|
||||
rhttp = http_client.HTTPResponse(_FakeSocket(sock.recv(1024)))
|
||||
rhttp.begin()
|
||||
if rhttp.status == 200:
|
||||
rssdp = SSDPResponse(rhttp)
|
||||
responses[rssdp.location] = rssdp
|
||||
except socket.timeout:
|
||||
break
|
||||
|
||||
return responses.values()
|
||||
1
deps/roku/emulator/__init__.py
vendored
Normal file
1
deps/roku/emulator/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
from roku.emulator.core import Emulator, DEFAULT_APPS
|
||||
BIN
deps/roku/emulator/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/emulator/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/roku/emulator/__pycache__/core.cpython-34.pyc
vendored
Normal file
BIN
deps/roku/emulator/__pycache__/core.cpython-34.pyc
vendored
Normal file
Binary file not shown.
29
deps/roku/emulator/core.py
vendored
Normal file
29
deps/roku/emulator/core.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
from roku import Application
|
||||
|
||||
DEFAULT_APPS = [
|
||||
Application(1, '1.0', 'Hulu Plus'),
|
||||
Application(2, '2.0', 'TWiT'),
|
||||
Application(3, '3.0', 'Whisky Media'),
|
||||
Application(4, '4.0', 'Netflix'),
|
||||
]
|
||||
|
||||
|
||||
class Emulator(object):
|
||||
|
||||
def __init__(self, apps=None):
|
||||
self._apps = apps or DEFAULT_APPS
|
||||
|
||||
def __call__(self, command, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def add_app(self, app):
|
||||
pass
|
||||
|
||||
def get_icon(self, app_id):
|
||||
pass
|
||||
|
||||
def launch_app(self, app_id):
|
||||
pass
|
||||
|
||||
def list_apps(self):
|
||||
pass
|
||||
7
deps/roku/proxy.py
vendored
Normal file
7
deps/roku/proxy.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
from roku.core import Roku
|
||||
|
||||
|
||||
class Proxy(object):
|
||||
|
||||
def __init__(self, remote_host, remote_port=8060, local_port=8060):
|
||||
pass
|
||||
28
deps/roku/server.py
vendored
Normal file
28
deps/roku/server.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/keypress/<key>')
|
||||
def keypress(key):
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/launch/<code>')
|
||||
def launch(code):
|
||||
app_id = request.args.get('contentID', None)
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/query/apps')
|
||||
def list_apps():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/query/icon/<app_id>')
|
||||
def app_icon(app_id):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=8060, debug=True)
|
||||
155
deps/roku/tests.py
vendored
Normal file
155
deps/roku/tests.py
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
import unittest
|
||||
from .core import Roku, COMMANDS
|
||||
|
||||
TEST_APPS = (
|
||||
('11', '1.0.1', 'Fake Roku Channel Store'),
|
||||
('22', '2.0.2', 'Fake Netflix'),
|
||||
('33', '3.0.3', 'Fake YouTube'),
|
||||
)
|
||||
|
||||
TEST_DEV_INFO = ''.join([
|
||||
'<?xml version="1.0" encoding="UTF-8" ?>',
|
||||
'<device-info>',
|
||||
' <udn>00000000-1111-2222-3333-444444444444</udn>',
|
||||
' <serial-number>111111111111</serial-number>',
|
||||
' <device-id>222222222222</device-id>',
|
||||
' <vendor-name>Roku</vendor-name>',
|
||||
' <model-number>4200X</model-number>',
|
||||
' <model-name>Roku 3</model-name>',
|
||||
' <wifi-mac>00:11:22:33:44:55</wifi-mac>',
|
||||
' <ethernet-mac>00:11:22:33:44:56</ethernet-mac>',
|
||||
' <network-type>ethernet</network-type>',
|
||||
' <user-device-name/>',
|
||||
' <software-version>7.00</software-version>',
|
||||
' <software-build>09044</software-build>',
|
||||
' <secure-device>true</secure-device>',
|
||||
' <language>en</language>',
|
||||
' <country>US</country>',
|
||||
' <locale>en_US</locale>',
|
||||
' <time-zone>US/Eastern</time-zone>',
|
||||
' <time-zone-offset>-300</time-zone-offset>',
|
||||
' <power-mode>PowerOn</power-mode>',
|
||||
' <developer-enabled>false</developer-enabled>',
|
||||
' <search-enabled>true</search-enabled>',
|
||||
' <voice-search-enabled>true</voice-search-enabled>',
|
||||
' <notifications-enabled>true</notifications-enabled>',
|
||||
' <notifications-first-use>false</notifications-first-use>',
|
||||
' <headphones-connected>false</headphones-connected>',
|
||||
'</device-info>'
|
||||
])
|
||||
|
||||
|
||||
class TestRoku(Roku):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestRoku, self).__init__(*args, **kwargs)
|
||||
self._calls = []
|
||||
|
||||
def _call(self, method, path, *args, **kwargs):
|
||||
|
||||
resp = None
|
||||
|
||||
if path == '/query/apps':
|
||||
fmt = '<app id="%s" version="%s">%s</app>'
|
||||
resp = '<apps>%s</apps>' % "".join(fmt % a for a in TEST_APPS)
|
||||
elif path == '/query/device-info':
|
||||
resp = TEST_DEV_INFO
|
||||
|
||||
self._calls.append((method, path, args, kwargs, resp))
|
||||
|
||||
return resp
|
||||
|
||||
def calls(self):
|
||||
return self._calls
|
||||
|
||||
def last_call(self):
|
||||
return self._calls[-1]
|
||||
|
||||
|
||||
class RokuTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.roku = TestRoku('0.0.0.0')
|
||||
|
||||
def testApps(self):
|
||||
|
||||
apps = self.roku.apps
|
||||
self.assertEqual(len(apps), 3)
|
||||
|
||||
for i, app in enumerate(apps):
|
||||
self.assertEqual(app.id, TEST_APPS[i][0])
|
||||
self.assertEqual(app.version, TEST_APPS[i][1])
|
||||
self.assertEqual(app.name, TEST_APPS[i][2])
|
||||
|
||||
app = self.roku['22']
|
||||
self.assertEqual(app.id, TEST_APPS[1][0])
|
||||
self.assertEqual(app.version, TEST_APPS[1][1])
|
||||
self.assertEqual(app.name, TEST_APPS[1][2])
|
||||
|
||||
app = self.roku['Fake YouTube']
|
||||
self.assertEqual(app.id, TEST_APPS[2][0])
|
||||
self.assertEqual(app.version, TEST_APPS[2][1])
|
||||
self.assertEqual(app.name, TEST_APPS[2][2])
|
||||
|
||||
def testDeviceInfo(self):
|
||||
|
||||
d = self.roku.device_info
|
||||
|
||||
self.assertEqual(d.modelname, 'Roku 3')
|
||||
self.assertEqual(d.modelnum, '4200X')
|
||||
self.assertEqual(d.swversion, '7.00.09044')
|
||||
self.assertEqual(d.sernum, '111111111111')
|
||||
|
||||
def testCommands(self):
|
||||
|
||||
for cmd in self.roku.commands:
|
||||
|
||||
if cmd == 'literal':
|
||||
continue
|
||||
|
||||
getattr(self.roku, cmd)()
|
||||
call = self.roku.last_call()
|
||||
|
||||
self.assertEqual(call[0], 'POST')
|
||||
self.assertEqual(call[1], '/keypress/%s' % COMMANDS[cmd])
|
||||
self.assertEqual(call[2], ())
|
||||
self.assertEqual(call[3], {})
|
||||
|
||||
def testLiteral(self):
|
||||
|
||||
text = 'Stargate'
|
||||
self.roku.literal(text)
|
||||
|
||||
for i, call in enumerate(self.roku.calls()):
|
||||
self.assertEqual(call[0], 'POST')
|
||||
self.assertEqual(call[1], '/keypress/Lit_%s' % text[i].upper())
|
||||
self.assertEqual(call[2], ())
|
||||
self.assertEqual(call[3], {})
|
||||
|
||||
def testStore(self):
|
||||
|
||||
for app in self.roku.apps:
|
||||
|
||||
self.roku.store(app)
|
||||
call = self.roku.last_call()
|
||||
|
||||
self.assertEqual(call[0], 'POST')
|
||||
self.assertEqual(call[1], '/launch/11')
|
||||
self.assertEqual(call[2], ())
|
||||
self.assertEqual(call[3], {'params': {'contentID': app.id}})
|
||||
|
||||
def testLaunch(self):
|
||||
|
||||
for app in self.roku.apps:
|
||||
|
||||
self.roku.launch(app)
|
||||
call = self.roku.last_call()
|
||||
|
||||
self.assertEqual(call[0], 'POST')
|
||||
self.assertEqual(call[1], '/launch/%s' % app.id)
|
||||
self.assertEqual(call[2], ())
|
||||
self.assertEqual(call[3], {'params': {'contentID': app.id}})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user