Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py

homeassistant/components/kira.py
homeassistant/components/*/kira.py

homeassistant/components/lutron.py
homeassistant/components/*/lutron.py

Expand Down
142 changes: 142 additions & 0 deletions homeassistant/components/kira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""KIRA interface to receive UDP packets from an IR-IP bridge."""
# pylint: disable=import-error
import logging
import os
import yaml

import voluptuous as vol
from voluptuous.error import Error as VoluptuousError

from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv

from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SENSORS,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
STATE_UNKNOWN)

REQUIREMENTS = ["pykira==0.1.1"]

DOMAIN = 'kira'

_LOGGER = logging.getLogger(__name__)

DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 65432

CONF_CODE = "code"
CONF_REPEAT = "repeat"
CONF_REMOTES = "remotes"
CONF_SENSOR = "sensor"
CONF_REMOTE = "remote"

CODES_YAML = '{}_codes.yaml'.format(DOMAIN)

CODE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_CODE): cv.string,
vol.Optional(CONF_TYPE): cv.string,
vol.Optional(CONF_DEVICE): cv.string,
vol.Optional(CONF_REPEAT): cv.positive_int,
})

SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DOMAIN):
vol.Exclusive(cv.string, "sensors"),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})

REMOTE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DOMAIN):
vol.Exclusive(cv.string, "remotes"),
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA],
vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]})
}, extra=vol.ALLOW_EXTRA)


def load_codes(path):
"""Load Kira codes from specified file."""
codes = []
if os.path.exists(path):
with open(path) as code_file:
data = yaml.load(code_file) or []
for code in data:
try:
codes.append(CODE_SCHEMA(code))
except VoluptuousError as exception:
# keep going
_LOGGER.warning('Kira Code Invalid Data: %s', exception)
else:
with open(path, 'w') as code_file:
code_file.write('')
return codes


def setup(hass, config):
"""Setup KIRA capability."""
import pykira

sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, [])
remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, [])
# If no sensors or remotes were specified, add a sensor
if not(sensors or remotes):
sensors.append({})

codes = load_codes(hass.config.path(CODES_YAML))

hass.data[DOMAIN] = {
CONF_SENSOR: {},
CONF_REMOTE: {},
}

def load_module(platform, idx, module_conf):
"""Set up Kira module and load platform."""
# note: module_name is not the HA device name. it's just a unique name
# to ensure the component and platform can share information
module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN
device_name = module_conf.get(CONF_NAME, DOMAIN)
port = module_conf.get(CONF_PORT, DEFAULT_PORT)
host = module_conf.get(CONF_HOST, DEFAULT_HOST)

if platform == CONF_SENSOR:
module = pykira.KiraReceiver(host, port)
module.start()
else:
module = pykira.KiraModule(host, port)

hass.data[DOMAIN][platform][module_name] = module
for code in codes:
code_tuple = (code.get(CONF_NAME),
code.get(CONF_DEVICE, STATE_UNKNOWN))
module.registerCode(code_tuple, code.get(CONF_CODE))

discovery.load_platform(hass, platform, DOMAIN,
{'name': module_name, 'device': device_name},
config)

for idx, module_conf in enumerate(sensors):
load_module(CONF_SENSOR, idx, module_conf)

for idx, module_conf in enumerate(remotes):
load_module(CONF_REMOTE, idx, module_conf)

def _stop_kira(_event):
for receiver in hass.data[DOMAIN][CONF_SENSOR].values():
receiver.stop()
_LOGGER.info("Terminated receivers")

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira)

return True
79 changes: 79 additions & 0 deletions homeassistant/components/remote/kira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Support for Keene Electronics IR-IP devices.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.kira/
"""
import logging
import functools as ft

import homeassistant.components.remote as remote
from homeassistant.helpers.entity import Entity

from homeassistant.const import (
STATE_UNKNOWN,
CONF_DEVICE,
CONF_NAME)

DOMAIN = 'kira'

_LOGGER = logging.getLogger(__name__)

CONF_REMOTE = "remote"


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Kira platform."""
if discovery_info:
name = discovery_info.get(CONF_NAME)
device = discovery_info.get(CONF_DEVICE)

kira = hass.data[DOMAIN][CONF_REMOTE][name]
add_devices([KiraRemote(device, kira)])
return True


class KiraRemote(Entity):
"""Remote representation used to send commands to a Kira device."""

def __init__(self, name, kira):
"""Initialize KiraRemote class."""
_LOGGER.debug("KiraRemote device init started for: %s", name)
self._name = name
self._state = STATE_UNKNOWN

self._kira = kira

@property
def name(self):
"""Return the Kira device's name."""
return self._name

@property
def device_state_attributes(self):
"""Add platform specific attributes."""
return {}

@property
def is_on(self):
"""Return True. Power state doesn't apply to this device."""
return True

def update(self):
"""No-op."""

def send_command(self, **kwargs):
"""Send a command to one device."""
code_tuple = (kwargs.get(remote.ATTR_COMMAND),
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)

self._kira.sendCode(code_tuple)

def async_send_command(self, **kwargs):
"""Send a command to a device.

This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.send_command, **kwargs))
4 changes: 2 additions & 2 deletions homeassistant/components/remote/services.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Describes the format for available remote services

turn_on:
description: Semds the Power On Command
description: Sends the Power On Command

fields:
entity_id:
Expand All @@ -20,7 +20,7 @@ turn_off:
example: 'remote.family_room'

send_command:
description: Semds a single command to a single device
description: Sends a single command to a single device

fields:
entity_id:
Expand Down
79 changes: 79 additions & 0 deletions homeassistant/components/sensor/kira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""KIRA interface to receive UDP packets from an IR-IP bridge."""
# pylint: disable=import-error
import logging

from homeassistant.const import (
CONF_DEVICE,
CONF_NAME,
STATE_UNKNOWN)

from homeassistant.helpers.entity import Entity

DOMAIN = 'kira'

_LOGGER = logging.getLogger(__name__)

ICON = 'mdi:remote'

CONF_SENSOR = "sensor"


# pylint: disable=unused-argument, too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup Kira sensor."""
if discovery_info is not None:
name = discovery_info.get(CONF_NAME)
device = discovery_info.get(CONF_DEVICE)
kira = hass.data[DOMAIN][CONF_SENSOR][name]
add_devices_callback([KiraReceiver(device, kira)])


class KiraReceiver(Entity):
"""Implementation of a Kira Receiver."""

def __init__(self, name, kira):
"""Initialize the sensor."""
self._name = name
self._state = STATE_UNKNOWN
self._device = STATE_UNKNOWN

kira.registerCallback(self._update_callback)

def _update_callback(self, code):
code_name, device = code
_LOGGER.info("Kira Code: %s", code_name)
self._state = code_name
self._device = device
self.schedule_update_ha_state()

@property
def name(self):
"""Return the name of the receiver."""
return self._name

@property
def icon(self):
"""Return icon."""
return ICON

@property
def state(self):
"""Return the state of the receiver."""
return self._state

@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
attr[CONF_DEVICE] = self._device
return attr

@property
def should_poll(self) -> bool:
"""Entity should not be polled."""
return False

@property
def force_update(self) -> bool:
"""Kira should force updates. Repeated states have meaning."""
return True
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,9 @@ pyiss==1.0.1
# homeassistant.components.remote.itach
pyitachip2ir==0.0.6

# homeassistant.components.kira
pykira==0.1.1

# homeassistant.components.sensor.kwb
pykwb==0.0.8

Expand Down
57 changes: 57 additions & 0 deletions tests/components/remote/test_kira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""The tests for Kira sensor platform."""
import unittest
from unittest.mock import MagicMock

from homeassistant.components.remote import kira as kira

from tests.common import get_test_home_assistant

SERVICE_SEND_COMMAND = 'send_command'

TEST_CONFIG = {kira.DOMAIN: {
'devices': [{'host': '127.0.0.1',
'port': 17324}]}}

DISCOVERY_INFO = {
'name': 'kira',
'device': 'kira'
}


class TestKiraSensor(unittest.TestCase):
"""Tests the Kira Sensor platform."""

# pylint: disable=invalid-name
DEVICES = []

def add_devices(self, devices):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)

def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.mock_kira = MagicMock()
self.hass.data[kira.DOMAIN] = {kira.CONF_REMOTE: {}}
self.hass.data[kira.DOMAIN][kira.CONF_REMOTE]['kira'] = self.mock_kira

def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()

def test_service_call(self):
"""Test Kira's ability to send commands."""
kira.setup_platform(self.hass, TEST_CONFIG, self.add_devices,
DISCOVERY_INFO)
assert len(self.DEVICES) == 1
remote = self.DEVICES[0]

assert remote.name == 'kira'

command = "FAKE_COMMAND"
device = "FAKE_DEVICE"
commandTuple = (command, device)
remote.send_command(device=device, command=command)

self.mock_kira.sendCode.assert_called_with(commandTuple)
Loading