Skip to content
Snippets Groups Projects
Commit 1f978a4e authored by jkerdreu's avatar jkerdreu
Browse files

Initial release, not ready for production.

Supports Lamp, Powerrelays, Temperature, Humidity, Contact, Binary sensors..




git-svn-id: https://redmine.imt-atlantique.fr/svn/xaal/code/Python/branches/0.7@2968 b32b6428-25c9-4566-ad07-03861ab6144f
parent 3226793b
No related branches found
No related tags found
No related merge requests found
# xAAL bridge for Home Assistant #
A bridge to provide access to xAAL Bus in Home-Assistant
##Important:
- Don't use this in production! only for test right now
- Missing config entry: Right now, there is no configuration for this addon
- Integration on start: If you want to add a new device, you have to restart hass
- Slow startup: The addon search for devices at startup, this can take some time.
##Install:
Simply copy the xaal folder in the Home-Assistant custom-component folder. Go
to configuration/integration panel and add xAAL. That's all.
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import bridge
from .const import DOMAIN
PLATFORMS: list[str] = ["light", "switch", "sensor", "binary_sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# create the hub and load the platforms
br = bridge.Bridge(hass)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = br
await br.wait_is_ready()
print("READY")
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# stop xAAL tasks before removing reference
# FIXME: some tasks never ends: RecvQ, Timers
await hass.data[DOMAIN][entry.entry_id].engine.stop()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
import logging
import functools
from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass
from homeassistant.const import STATE_ON, STATE_OFF
from .const import DOMAIN
from .core import XAALEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities ):
bridge = hass.data[DOMAIN][config_entry.entry_id]
for dev in bridge._mon.devices:
entity = None
if dev.dev_type.startswith('motion.'):
entity = Motion(dev,bridge)
if dev.dev_type.startswith('contact.'):
entity = Contact(dev,bridge)
if dev.dev_type.startswith('switch.'):
entity = Switch(dev,bridge)
if dev.dev_type.startswith('button.'):
entity = Button(dev,bridge)
if entity:
async_add_entities([entity])
bridge.add_entity(dev.address, entity)
ptr = functools.partial(buttons_handler,bridge)
bridge._eng.subscribe(ptr)
def buttons_handler(bridge,msg):
if msg.dev_type.startswith('button.'):
entity = bridge._entities.get(msg.source, None)
if entity:
entity.fire_event("xaal.click")
class Motion(XAALEntity, BinarySensorEntity):
device_class = BinarySensorDeviceClass.MOTION
@property
def unique_id(self) -> str:
return f'binary_sensor.{str(self._dev.address)}_motion'
@property
def state(self):
value =self._dev.attributes.get('presence',None)
return STATE_ON if value else STATE_OFF
class Contact(XAALEntity, BinarySensorEntity):
device_class = BinarySensorDeviceClass.OPENING
@property
def unique_id(self) -> str:
return f'binary_sensor.{str(self._dev.address)}_contact'
@property
def state(self):
value = self._dev.attributes.get('detected',None)
#return STATE_OPEN if value else STATE_CLOSED
return STATE_ON if value else STATE_OFF
class Switch(XAALEntity, BinarySensorEntity):
@property
def unique_id(self) -> str:
return f'binary_sensor.{str(self._dev.address)}_switch'
@property
def state(self):
value = self._dev.attributes.get('position',None)
#return STATE_OPEN if value else STATE_CLOSED
return STATE_ON if value else STATE_OFF
class Button(XAALEntity, BinarySensorEntity):
@property
def unique_id(self) -> str:
return f'binary_sensor.{str(self._dev.address)}_button'
@property
def state(self):
return False
def fire_event(self,event):
_LOGGER.warning(f"Button event: {event} {self.entity_id}")
self.hass.bus.fire("xaal_event", {'entity_id': self.entity_id, "click_type": "single"})
\ No newline at end of file
import asyncio
from homeassistant.core import HomeAssistant
from xaal.lib import AsyncEngine, tools
from xaal.schemas import devices as schemas
from xaal.monitor import Monitor, Notification
import logging
_LOGGER = logging.getLogger(__name__)
#DB_SERVER = tools.get_uuid('d28fbc27-190f-4ee5-815a-fe05233400a2')
DB_SERVER = tools.get_uuid('9064ccbc-84ea-11e8-80cc-82ed25e6aaaa')
def filter_msg(msg):
if msg.source == DB_SERVER:
return True
if msg.dev_type.startswith('lamp.'):
return True
if msg.dev_type.startswith('powerrelay.'):
return True
if msg.dev_type.startswith('thermometer.'):
return True
if msg.dev_type.startswith('hygrometer.'):
return True
if msg.dev_type.startswith('battery.'):
return True
return False
class Bridge:
def __init__(self, hass: HomeAssistant) -> None:
"""Init dummy hub."""
self._hass = hass
self._eng = AsyncEngine()
self._dev = self.setup_device()
self._mon = Monitor(self._dev, db_server=DB_SERVER)
self._mon.subscribe (self.monitor_event)
self._eng.start()
self._eng.on_start(self.on_start)
self._eng.on_stop(self.on_stop)
self._entities = {}
@property
def engine(self):
return self._eng
def on_start(self):
_LOGGER.warning(f"{self._eng} started")
def on_stop(self):
_LOGGER.warning(f"{self._eng} stopped")
def add_entity(self, addr, entity):
self._entities.update({addr:entity})
def remove_entity(self, addr):
self._entities.pop(addr)
def setup_device(self):
dev = schemas.hmi()
dev.dev_type = 'hmi.hass'
dev.vendor_id = 'IMT Atlantique'
dev.product_id = 'xAAL to HASS Brigde'
self._eng.add_device(dev)
return dev
def send_request(self, targets,action, body=None):
self._mon.engine.send_request(self._dev, targets, action, body)
async def wait_is_ready(self) -> bool:
while 1:
if self._mon.boot_finished == True:
return True
await asyncio.sleep(1)
return False
def monitor_event(self,event,dev):
_LOGGER.info(f"New event {event} {dev}")
entity = self._entities.get(dev.address, None)
#if entity == None or entity.hass is None:
# return
if entity == None:
return
# if event == Notification.new_device:
if event in [Notification.attribute_change]:
entity.async_schedule_update_ha_state()
# FIXME: monitor_event isn't a coroutine.
# if event in [Notification.description_change, Notification.metadata_change]:
# await entity.async_device_update()
import voluptuous as vol
from homeassistant import config_entries
import logging
from . import bridge
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({})
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
async def async_step_user(self, user_input=None):
# no user input right now, so we just show the empty form
errors = {}
#br = bridge.Bridge(self.hass)
#r = await br.wait_is_ready()
#print(f"READY={r}")
#br.engine.stop()
# if we have some user_input let's start
if user_input is not None:
try:
return self.async_create_entry(title="Bridge", data=user_input)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
# else we show the empty form
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors)
"""Constants for the Detailed Hello World Push integration."""
# This is the internal name of the integration, it should also match the directory
# name for the integration.
DOMAIN = "xaal"
from .const import DOMAIN
from homeassistant.helpers.entity import Entity
class XAALEntity(Entity):
def __init__(self, dev, bridge):
self._dev = dev
self._bridge = bridge
@property
def device_info(self):
print(f"DB: {self.unique_id} {self._dev.db}")
dev = self._dev
group_id = dev.description.get('group_id', None)
if group_id:
ident = str(group_id)
else:
ident = str(dev.address)
return {
"identifiers": {(DOMAIN, ident)},
# If desired, the name for the device could be different to the entity
#"name": dev.description.get("info", ""),
"name": dev.display_name,
#"sw_version": dev.description[""],
"model": dev.description.get("product_id", ""),
"manufacturer": dev.description.get("vendor_id", ""),
}
def send_request(self, action, body=None):
self._bridge.send_request([self._dev.address,], action, body)
@property
def available(self) -> bool:
return True
@property
def should_poll(self):
"""No polling needed."""
return False
#@property
#def name(self) -> str:
# return f"xAAL {self.__class__.__name__} {str(self._dev.address)}"
import logging
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, LightEntity)
from homeassistant.util import color as color_util
from .const import DOMAIN
from .core import XAALEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities ):
bridge = hass.data[DOMAIN][config_entry.entry_id]
for dev in bridge._mon.devices:
if dev.dev_type.startswith('lamp.'):
entity = Lamp(dev,bridge)
async_add_entities([entity])
bridge.add_entity(dev.address, entity)
class Lamp(XAALEntity, LightEntity):
@property
def supported_color_modes(self) -> str:
dev_type = self._dev.dev_type
if dev_type in ['lamp.color']:
return {"brightness", "hs", "color_temp"}
if dev_type in ['lamp.dimmer']:
return {"brightness"}
@property
def unique_id(self) -> str:
return f'light.{str(self._dev.address)}'
@property
def color_mode(self):
mode = self._dev.attributes.get('mode', None)
if mode == 'white':
return 'color_temp'
elif mode == 'color':
return 'hs'
# FIXME: xAAL don't have this kind of lamp
return 'brightness'
@property
def brightness(self):
brightness = self._dev.attributes.get('brightness', 0)
return round(255 * (int(brightness) / 100))
@property
def hs_color(self):
hsv = self._dev.attributes.get('hsv',None)
if hsv:
return (hsv[0], hsv[1]*100)
@property
def color_temp(self) -> int | None:
white_temp = self._dev.attributes.get('white_temperature', None)
if white_temp:
return color_util.color_temperature_kelvin_to_mired(white_temp)
@property
def is_on(self) -> bool | None:
return self._dev.attributes.get('light',None)
def turn_on(self, **kwargs) -> None:
_LOGGER.debug(f"turn_on: {kwargs}")
color = kwargs.get(ATTR_HS_COLOR,None)
brightness = kwargs.get(ATTR_BRIGHTNESS,None)
color_temp = kwargs.get(ATTR_COLOR_TEMP,None)
# FIX: support duration
#duration = kwargs.get('duration',None)
if color_temp:
white_temp = color_util.color_temperature_mired_to_kelvin(color_temp)
self.send_request('set_white_temperature', {'white_temperature': white_temp})
if brightness:
brightness = int(brightness / 255 * 100)
self.send_request('set_brightness', {'brightness': brightness})
if color:
h = int(color[0])
s = color[1] / 100
v = self._dev.attributes.get('brightness', 100) / 100
self.send_request('set_hsv', {'hsv': [h, s, v]})
if not self.is_on:
self.send_request('turn_on')
def turn_off(self, **kwargs) -> None:
self.send_request('turn_off')
{
"domain": "xaal",
"name": "xAAL",
"config_flow": true,
"documentation": "https://recherche.imt-atlantique.fr/xaal/",
"requirements": [],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@jkerdreux-imt"],
"iot_class": "local_push",
"version": "0.1.0"
}
import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
TEMP_CELSIUS,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_ILLUMINANCE,
PERCENTAGE,
POWER_WATT,
)
from .const import DOMAIN
from .core import XAALEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities ):
bridge = hass.data[DOMAIN][config_entry.entry_id]
for dev in bridge._mon.devices:
entity = None
if dev.dev_type.startswith('thermometer.'):
entity = Thermometer(dev,bridge)
if dev.dev_type.startswith('hygrometer.'):
entity = Hygrometer(dev,bridge)
if dev.dev_type.startswith('battery.'):
entity = Battery(dev,bridge)
if dev.dev_type.startswith('powermeter.'):
entity = PowerMeter(dev,bridge)
if entity:
async_add_entities([entity])
bridge.add_entity(dev.address, entity)
class Thermometer(XAALEntity, SensorEntity):
device_class = DEVICE_CLASS_TEMPERATURE
_attr_unit_of_measurement = TEMP_CELSIUS
@property
def unique_id(self) -> str:
return f'sensor.{str(self._dev.address)}_temperature'
@property
def name(self) -> str:
return 'xAAL Thermometer:' + str(self._dev.address)
@property
def state(self):
return self._dev.attributes.get('temperature',None)
class Hygrometer(XAALEntity, SensorEntity):
device_class = DEVICE_CLASS_HUMIDITY
_attr_unit_of_measurement = PERCENTAGE
@property
def unique_id(self) -> str:
return f'sensor.{str(self._dev.address)}_humidity'
@property
def state(self):
return self._dev.attributes.get('humidity',None)
class Battery(XAALEntity, SensorEntity):
device_class = DEVICE_CLASS_BATTERY
_attr_unit_of_measurement = PERCENTAGE
@property
def unique_id(self) -> str:
return f'sensor.{str(self._dev.address)}_battery'
@property
def state(self):
return self._dev.attributes.get('level',None)
class PowerMeter(XAALEntity, SensorEntity):
device_class = DEVICE_CLASS_POWER
_attr_unit_of_measurement = POWER_WATT
@property
def unique_id(self) -> str:
return f'sensor.{str(self._dev.address)}_power'
@property
def state(self):
return self._dev.attributes.get('power',None)
import logging
from homeassistant.components.switch import SwitchEntity
from .const import DOMAIN
from .core import XAALEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities ):
bridge = hass.data[DOMAIN][config_entry.entry_id]
for dev in bridge._mon.devices:
entity = None
if dev.dev_type.startswith('powerrelay.'):
entity = PowerRelay(dev,bridge)
if entity:
async_add_entities([entity])
bridge.add_entity(dev.address, entity)
class PowerRelay(XAALEntity,SwitchEntity):
@property
def unique_id(self) -> str:
return f'switch.{str(self._dev.address)}'
@property
def is_on(self) -> bool | None:
return self._dev.attributes.get('power',None)
def turn_on(self, **kwargs) -> None:
_LOGGER.debug(f"turn_on: {kwargs}")
self.send_request('turn_on')
def turn_off(self, **kwargs) -> None:
self.send_request('turn_off')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment