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
4 changes: 4 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ homeassistant/components/melissa.py @kennedyshead
homeassistant/components/*/melissa.py @kennedyshead
homeassistant/components/*/mystrom.py @fabaff

# N
homeassistant/components/ness_alarm.py @nickw444
homeassistant/components/*/ness_alarm.py @nickw444

# O
homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya
Expand Down
107 changes: 107 additions & 0 deletions homeassistant/components/alarm_control_panel/ness_alarm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Support for Ness D8X/D16X alarm panel.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ness_alarm/
"""

import logging

import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.ness_alarm import (
DATA_NESS, SIGNAL_ARMING_STATE_CHANGED)
from homeassistant.const import (
Comment thread
nickw444 marked this conversation as resolved.
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING,
STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, STATE_ALARM_DISARMED)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['ness_alarm']


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ness Alarm alarm control panel devices."""
if discovery_info is None:
return

device = NessAlarmPanel(hass.data[DATA_NESS], 'Alarm Panel')
async_add_entities([device])


class NessAlarmPanel(alarm.AlarmControlPanel):
"""Representation of a Ness alarm panel."""

def __init__(self, client, name):
"""Initialize the alarm panel."""
self._client = client
self._name = name
self._state = None

async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ARMING_STATE_CHANGED,
self._handle_arming_state_change)

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

@property
def should_poll(self):
"""Return the polling state."""
return False

@property
def code_format(self):
"""Return the regex for code format or None if no code is required."""
return 'Number'

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

async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
await self._client.disarm(code)

async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
await self._client.arm_away(code)

async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
await self._client.arm_home(code)

async def async_alarm_trigger(self, code=None):
"""Send trigger/panic command."""
await self._client.panic(code)

@callback
def _handle_arming_state_change(self, arming_state):
"""Handle arming state update."""
from nessclient import ArmingState

if arming_state == ArmingState.UNKNOWN:
self._state = None
elif arming_state == ArmingState.DISARMED:
self._state = STATE_ALARM_DISARMED
elif arming_state == ArmingState.ARMING:
self._state = STATE_ALARM_ARMING
elif arming_state == ArmingState.EXIT_DELAY:
self._state = STATE_ALARM_ARMING
elif arming_state == ArmingState.ARMED:
self._state = STATE_ALARM_ARMED_AWAY
elif arming_state == ArmingState.ENTRY_DELAY:
self._state = STATE_ALARM_PENDING
elif arming_state == ArmingState.TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
else:
_LOGGER.warning("Unhandled arming state: %s", arming_state)

self.async_schedule_update_ha_state()
81 changes: 81 additions & 0 deletions homeassistant/components/binary_sensor/ness_alarm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
Support for Ness D8X/D16X zone states - represented as binary sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ness_alarm/
"""
import logging

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.ness_alarm import (
CONF_ZONES, CONF_ZONE_TYPE, CONF_ZONE_NAME, CONF_ZONE_ID,
SIGNAL_ZONE_CHANGED, ZoneChangedData)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

DEPENDENCIES = ['ness_alarm']
_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ness Alarm binary sensor devices."""
if not discovery_info:
return

configured_zones = discovery_info[CONF_ZONES]

devices = []

for zone_config in configured_zones:
zone_type = zone_config[CONF_ZONE_TYPE]
zone_name = zone_config[CONF_ZONE_NAME]
zone_id = zone_config[CONF_ZONE_ID]
device = NessZoneBinarySensor(zone_id=zone_id, name=zone_name,
zone_type=zone_type)
devices.append(device)

async_add_entities(devices)


class NessZoneBinarySensor(BinarySensorDevice):
"""Representation of an Ness alarm zone as a binary sensor."""

def __init__(self, zone_id, name, zone_type):
"""Initialize the binary_sensor."""
self._zone_id = zone_id
self._name = name
self._type = zone_type
self._state = 0

async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_ZONE_CHANGED, self._handle_zone_change)

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

@property
def should_poll(self):
"""No polling needed."""
return False

@property
def is_on(self):
"""Return true if sensor is on."""
return self._state == 1

@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._type

@callback
def _handle_zone_change(self, data: ZoneChangedData):
"""Handle zone state update."""
if self._zone_id == data.zone_id:
self._state = data.state
self.async_schedule_update_ha_state()
121 changes: 121 additions & 0 deletions homeassistant/components/ness_alarm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Support for Ness D8X/D16X devices.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ness_alarm/
"""
import logging
from collections import namedtuple

import voluptuous as vol

from homeassistant.components.binary_sensor import DEVICE_CLASSES
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send

REQUIREMENTS = ['nessclient==0.9.9']

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'ness_alarm'
DATA_NESS = 'ness_alarm'

CONF_DEVICE_HOST = 'host'
CONF_DEVICE_PORT = 'port'
CONF_ZONES = 'zones'
CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type'
CONF_ZONE_ID = 'id'
ATTR_CODE = 'code'
ATTR_OUTPUT_ID = 'output_id'
ATTR_STATE = 'state'
DEFAULT_ZONES = []

SIGNAL_ZONE_CHANGED = 'ness_alarm.zone_changed'
SIGNAL_ARMING_STATE_CHANGED = 'ness_alarm.arming_state_changed'

ZoneChangedData = namedtuple('ZoneChangedData', ['zone_id', 'state'])

DEFAULT_ZONE_TYPE = 'motion'
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Required(CONF_ZONE_ID): cv.positive_int,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE):
vol.In(DEVICE_CLASSES)})

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE_HOST): cv.string,
vol.Required(CONF_DEVICE_PORT): cv.port,
vol.Optional(CONF_ZONES, default=DEFAULT_ZONES):
vol.All(cv.ensure_list, [ZONE_SCHEMA]),
}),
}, extra=vol.ALLOW_EXTRA)

SERVICE_PANIC = 'panic'
SERVICE_AUX = 'aux'

SERVICE_SCHEMA_PANIC = vol.Schema({
vol.Required(ATTR_CODE): cv.string,
})
SERVICE_SCHEMA_AUX = vol.Schema({
vol.Required(ATTR_OUTPUT_ID): cv.positive_int,
vol.Optional(ATTR_STATE, default=True): cv.boolean,
})


async def async_setup(hass, config):
"""Set up the Ness Alarm platform."""
from nessclient import Client, ArmingState
conf = config[DOMAIN]

zones = conf[CONF_ZONES]
host = conf[CONF_DEVICE_HOST]
port = conf[CONF_DEVICE_PORT]

client = Client(host=host, port=port, loop=hass.loop)
hass.data[DATA_NESS] = client

async def _close(event):
await client.close()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)

hass.async_create_task(
async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones},
config))
hass.async_create_task(
async_load_platform(hass, 'alarm_control_panel', DOMAIN, {}, config))

def on_zone_change(zone_id: int, state: bool):
"""Receives and propagates zone state updates."""
async_dispatcher_send(hass, SIGNAL_ZONE_CHANGED, ZoneChangedData(
zone_id=zone_id,
state=state,
))

def on_state_change(arming_state: ArmingState):
"""Receives and propagates arming state updates."""
async_dispatcher_send(hass, SIGNAL_ARMING_STATE_CHANGED, arming_state)

client.on_zone_change(on_zone_change)
client.on_state_change(on_state_change)

# Force update for current arming status and current zone states
hass.loop.create_task(client.keepalive())
hass.loop.create_task(client.update())

async def handle_panic(call):
await client.panic(call.data[ATTR_CODE])

async def handle_aux(call):
await client.aux(call.data[ATTR_OUTPUT_ID], call.data[ATTR_STATE])

hass.services.async_register(DOMAIN, SERVICE_PANIC, handle_panic,
schema=SERVICE_SCHEMA_PANIC)
hass.services.async_register(DOMAIN, SERVICE_AUX, handle_aux,
schema=SERVICE_SCHEMA_AUX)

return True
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,9 @@ nanoleaf==0.4.1
# homeassistant.components.device_tracker.keenetic_ndms2
ndms2_client==0.0.6

# homeassistant.components.ness_alarm
nessclient==0.9.9

# homeassistant.components.sensor.netdata
netdata==0.1.2

Expand Down
Loading