I’m currently using the last solution @loom mentioned:
Home Assistant with custom component (host) <–> creator with Malos as coordinator <–> two Ikea Tradfri bulbs
I wrote a custom home assistant component which listens for and sends Malos Zigbee messages via ZMQ/Protobuf and acts as a standard light platform.
Currently it’s rather hardcoded to match my setup, so only color temperature, brightness and on/off are supported. The python code is based on the zigbee js example: https://github.com/matrix-io/matrix-malos-zigbee/blob/master/src/js_test/test_zigbee.js.
I don’t really have experience with python so the code can be better for sure.
I hope to find the time to improve/generalize the component and add it to Home Assistant, but for those interested in the current version:
-
Your Creator should be running with the latest Malos release without the Kernel Modules
-
Drop the code below into a file in your home assistant config folder:
custom_components/light/matrix_zigbee.py
-
Add a config entry to your home assistant config:
light:
- platform: matrix_zigbee
host: "192.168.178.48" <- matrix creator ip
-
Restart Home Assistant
-
Devices already bound to the creator zigbee coordinator should be added as lights, alternatively switch them on now to join the network.
custom_components/light/matrix_zigbee.py:
import logging
import asyncio
import voluptuous as vol
# Import the device class from the component that you want to support from
from enum import Enum
import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, ATTR_XY_COLOR, ATTR_WHITE_VALUE, EFFECT_COLORLOOP, EFFECT_RANDOM,
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA, DOMAIN)
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_callback_threadsafe
REQUIREMENTS = ['pyzmq==17.0.0b3', 'protobuf==3.3.0', 'matrix_io-proto==0.0.17']
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.INFO)
# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string
})
class Zigbee(object):
def __init__(self, hass, zmq_socket, driver_pb2, comm_pb2):
"""Initialize an Zigbee proxy."""
self._hass = hass
self._socket = zmq_socket
self._driver_pb2 = driver_pb2
self._comm_pb2 = comm_pb2
@asyncio.coroutine
def ResetGateway(self):
_LOGGER.info('Reseting the Gateway App')
_LOGGER.info(self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.keys())
driver_config_proto = self._driver_pb2.DriverConfig()
driver_config_proto.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT")
driver_config_proto.zigbee_message.network_mgmt_cmd.type = self._comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes.Value("RESET_PROXY")
self._socket.send(driver_config_proto.SerializeToString())
@asyncio.coroutine
def IsGatewayActive(self):
_LOGGER.info('Checking connection with the Gateway')
driver_config_proto = self._driver_pb2.DriverConfig()
driver_config_proto.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT")
driver_config_proto.zigbee_message.network_mgmt_cmd.type = self._comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes.Value("IS_PROXY_ACTIVE")
self._socket.send(driver_config_proto.SerializeToString())
@asyncio.coroutine
def RequestNetworkStatus(self):
_LOGGER.info('Requesting network status')
driver_config_proto = self._driver_pb2.DriverConfig()
driver_config_proto.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT")
driver_config_proto.zigbee_message.network_mgmt_cmd.type = self._comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes.Value("NETWORK_STATUS")
driver_config_proto.zigbee_message.network_mgmt_cmd.permit_join_params.time = 60
self._socket.send(driver_config_proto.SerializeToString())
return Status.WAITING_FOR_NETWORK_STATUS
@asyncio.coroutine
def CreateNetwork(self):
_LOGGER.info('NO NETWORK')
_LOGGER.info('CREATING A ZigBee Network')
driver_config_proto = self._driver_pb2.DriverConfig()
driver_config_proto.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT")
driver_config_proto.zigbee_message.network_mgmt_cmd.type = self._comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes.Value("CREATE_NWK")
driver_config_proto.zigbee_message.network_mgmt_cmd.permit_join_params.time = 60
self._socket.send(driver_config_proto.SerializeToString())
return Status.WAITING_FOR_NETWORK_STATUS
@asyncio.coroutine
def PermitJoin(self):
_LOGGER.info('Permitting join')
driver_config_proto = self._driver_pb2.DriverConfig()
driver_config_proto.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT")
driver_config_proto.zigbee_message.network_mgmt_cmd.type = self._comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes.Value("PERMIT_JOIN")
driver_config_proto.zigbee_message.network_mgmt_cmd.permit_join_params.time = 60
self._socket.send(driver_config_proto.SerializeToString())
_LOGGER.info('Please reset your zigbee devices')
_LOGGER.info('.. Waiting 60s for new devices')
return Status.WAITING_FOR_DEVICES
@asyncio.coroutine
def checkGatewayActive(self):
yield from asyncio.sleep(3)
yield from self.IsGatewayActive()
class Status(Enum):
NONE = 1
WAITING_FOR_DEVICES = 2
WAITING_FOR_NETWORK_STATUS = 3
NODES_DISCOVERED = 4
status = Status.NONE
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Matrix Creator Everloop platform."""
import zmq
import zmq.asyncio
zmq.asyncio.install()
from matrix_io.proto.malos.v1 import driver_pb2
from matrix_io.proto.malos.v1 import comm_pb2
zmq_host = config.get(CONF_HOST)
zmq_base_port = 40000 + 1
context = zmq.asyncio.Context()
zmq_socket = context.socket(zmq.PUSH)
_LOGGER.info("Host: %s", 'tcp://{0}'.format(zmq_host))
zmq_socket.connect('tcp://{0}:{1}'.format(zmq_host, zmq_base_port))
# Print errors
error_socket = context.socket(zmq.SUB)
error_socket.connect('tcp://{0}:{1}'.format(zmq_host, zmq_base_port + 2))
error_socket.subscribe('')
@asyncio.coroutine
def recvError():
while True:
message = yield from error_socket.recv()
_LOGGER.error("Error: %s", message)
hass.loop.create_task(recvError())
# Ping driver
ping_socket = context.socket(zmq.PUSH)
ping_socket.connect('tcp://{0}:{1}'.format(zmq_host, zmq_base_port + 1))
ping_socket.send_string('')
@asyncio.coroutine
def ping():
while True:
yield from asyncio.sleep(1)
ping_socket.send_string('')
hass.loop.create_task(ping())
# Receive data messages
message_socket = context.socket(zmq.SUB)
message_socket.connect('tcp://{0}:{1}'.format(zmq_host, zmq_base_port + 3))
message_socket.subscribe('')
nodes_id = []
gateway_up = False
zigbee_proxy = Zigbee(hass, zmq_socket, driver_pb2, comm_pb2)
def call_zigbee_service(fn):
@asyncio.coroutine
def service_fn(service):
return fn()
return service_fn
def addConnectedZigbeeDevices(zig_msg):
nodes_id = []
for node in zig_msg.network_mgmt_cmd.connected_nodes:
for endpoint in node.endpoints:
for cluster in endpoint.clusters:
if cluster.cluster_id == 6:
nodes_id.append((node.node_id, endpoint.endpoint_index))
_LOGGER.info("add %s devices", len(nodes_id))
add_devices([ ZigbeeBulb(hass,x,y,zmq_socket,driver_pb2,comm_pb2) for x, y in nodes_id])
@asyncio.coroutine
def checkNetwork(zig_msg, networkStatusType, networkStates):
global status
_LOGGER.info('Network status type: %s', networkStatusType)
if networkStatusType == networkStates.Value("NO_NETWORK"):
yield from zigbee_proxy.CreateNetwork()
status = Status.WAITING_FOR_NETWORK_STATUS
elif networkStatusType == networkStates.Value("JOINED_NETWORK"):
addConnectedZigbeeDevices(zig_msg)
yield from zigbee_proxy.PermitJoin()
status = Status.WAITING_FOR_DEVICES
else:
message = 'JOINING_NETWORK message received' if networkStatusType == networkStates.Value("JOINING_NETWORK") else 'JOINED_NETWORK_NO_PARENT' if networkStatusType == networkStates.Value("JOINED_NETWORK_NO_PARENT") else 'LEAVING_NETWORK message received' if networkStatusType == networkStates.Value("LEAVING_NETWORK") else None
_LOGGER.info(message)
@asyncio.coroutine
def checkNetworkManagementType(zig_msg, networkMgmtCmdType, allNetworkMgmtCmdTypes):
global status
nodes_id = []
_LOGGER.info('Check network type: %s', networkMgmtCmdType)
_LOGGER.info('Status: %s', status)
if networkMgmtCmdType == allNetworkMgmtCmdTypes.Value("DISCOVERY_INFO") and status == Status.WAITING_FOR_DEVICES:
_LOGGER.info('Device found: %s', zig_msg.network_mgmt_cmd.connected_nodes)
_LOGGER.info('Looking for nodes that have an on-off cluster.')
for node in zig_msg.network_mgmt_cmd.connected_nodes:
for endpoint in node.endpoints:
for cluster in endpoint.clusters:
if cluster.cluster_id == 6:
nodes_id.append((node.node_id, endpoint.endpoint_index))
_LOGGER.info("found devices %s", nodes_id)
if len(nodes_id) > 0:
status = Status.NODES_DISCOVERED
_LOGGER.info('Nodes discovered')
else:
_LOGGER.warning('No devices found!')
return
_LOGGER.warn("add devices %s", nodes_id)
add_devices([ ZigbeeBulb(hass,x,y,zmq_socket,driver_pb2,comm_pb2) for x, y in nodes_id ])
elif networkMgmtCmdType == allNetworkMgmtCmdTypes.Value("IS_PROXY_ACTIVE"):
if zig_msg.network_mgmt_cmd.is_proxy_active:
_LOGGER.info('Gateway connected')
gateway_up = True
elif not status == Status.RESETTING:
yield from ResetGateway()
_LOGGER.info('Waiting 3 sec ....')
hass.loop.create_task(zigbee_proxy.checkGatewayActive())
status = Status.RESETTING
return
else:
_LOGGER.warning('Gateway reset failed')
return
yield from zigbee_proxy.RequestNetworkStatus()
status = Status.WAITING_FOR_NETWORK_STATUS
elif networkMgmtCmdType == allNetworkMgmtCmdTypes.Value("NETWORK_STATUS") and status == Status.WAITING_FOR_NETWORK_STATUS:
_LOGGER.info('NETWORK STATUS: ')
status = Status.NONE
yield from checkNetwork(zig_msg, zig_msg.network_mgmt_cmd.network_status.type, comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkStatus.Status)
@asyncio.coroutine
def recvMessage():
while True:
message = yield from message_socket.recv()
zig_msg = comm_pb2.ZigBeeMsg.FromString(message)
_LOGGER.info("Message: %s", zig_msg)
_LOGGER.info("network mgmt: %s", zig_msg.type == comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT"))
if zig_msg.type == comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("NETWORK_MGMT"):
yield from checkNetworkManagementType(zig_msg, zig_msg.network_mgmt_cmd.type, comm_pb2.ZigBeeMsg.NetworkMgmtCmd.NetworkMgmtCmdTypes)
hass.loop.create_task(recvMessage())
hass.services.async_register(DOMAIN, "reset_gateway", call_zigbee_service(zigbee_proxy.ResetGateway))
hass.services.async_register(DOMAIN, 'isGatewayActive', call_zigbee_service(zigbee_proxy.IsGatewayActive))
hass.services.async_register(DOMAIN, 'requestNetworkStatus', call_zigbee_service(zigbee_proxy.RequestNetworkStatus))
hass.services.async_register(DOMAIN, 'createNetwork', call_zigbee_service(zigbee_proxy.CreateNetwork))
hass.services.async_register(DOMAIN, 'permitJoin', call_zigbee_service(zigbee_proxy.PermitJoin))
# Create a new driver config
driver_config_proto = driver_pb2.DriverConfig()
driver_config_proto.delay_between_updates = 1.0
driver_config_proto.timeout_after_last_ping = 1.0
zmq_socket.send(driver_config_proto.SerializeToString())
yield from zigbee_proxy.ResetGateway()
# Trigger zigbee setup
hass.loop.create_task(zigbee_proxy.checkGatewayActive())
return True
class ZigbeeBulb(Light):
def __init__(self, hass, nodeId, endointIndex, zmq_socket, driver_pb2, comm_pb2):
"""Initialize an AwesomeLight."""
self._hass = hass
self._nodeId = nodeId
self._endpointIndex = endointIndex
self._socket = zmq_socket
self._driver_pb2 = driver_pb2
self._comm_pb2 = comm_pb2
self._name = str(nodeId)
self._state = None
self._brightness = 50
self._colorTemp = 340
@property
def name(self):
"""Return the display name of this light."""
return self._name
#@property
#def should_poll(self) -> bool:
# """No polling needed for a demo light."""
# return False
@property
def brightness(self):
"""Return the brightness of the light.
This method is optional. Removing it indicates to Home Assistant
that brightness is not supported for this light.
"""
return self._brightness
@property
def white_value(self):
"""Return the white value of this light between 0..255."""
return self._colorTemp
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def supported_features(self):
"""Flag supported features."""
# TODO: SUPPORT_FLASH = 8, SUPPORT_TRANSITION = 32
return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP)
def turn_on(self, **kwargs):
"""Instruct the light to turn on.
You can skip the brightness part if your light does not support
brightness control.
"""
_LOGGER.warning("kwargs: %s", kwargs)
self._state = True
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
config = self._driver_pb2.DriverConfig()
config.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("ZCL")
config.zigbee_message.zcl_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ZCLCmdType.Value("LEVEL")
config.zigbee_message.zcl_cmd.level_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.LevelCmd.ZCLLevelCmdType.Value("MOVE_TO_LEVEL")
config.zigbee_message.zcl_cmd.level_cmd.move_to_level_params.level = self._brightness
config.zigbee_message.zcl_cmd.level_cmd.move_to_level_params.transition_time = 10
config.zigbee_message.zcl_cmd.node_id = self._nodeId
config.zigbee_message.zcl_cmd.endpoint_index = self._endpointIndex
run_callback_threadsafe(
self._hass.loop, self._socket.send, config.SerializeToString()).result()
elif ATTR_COLOR_TEMP in kwargs:
self._colorTemp = kwargs[ATTR_COLOR_TEMP]
config = self._driver_pb2.DriverConfig()
config.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("ZCL")
config.zigbee_message.zcl_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ZCLCmdType.Value("COLOR_CONTROL")
config.zigbee_message.zcl_cmd.colorcontrol_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ColorControlCmd.ZCLColorControlCmdType.Value("MOVETOCOLORTEMP")
config.zigbee_message.zcl_cmd.colorcontrol_cmd.movetocolortemp_params.color_temperature = self._colorTemp
config.zigbee_message.zcl_cmd.colorcontrol_cmd.movetocolortemp_params.transition_time = 10
config.zigbee_message.zcl_cmd.node_id = self._nodeId
config.zigbee_message.zcl_cmd.endpoint_index = self._endpointIndex
run_callback_threadsafe(
self._hass.loop, self._socket.send, config.SerializeToString()).result()
else:
config = self._driver_pb2.DriverConfig()
config.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("ZCL")
config.zigbee_message.zcl_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ZCLCmdType.Value("ON_OFF")
config.zigbee_message.zcl_cmd.onoff_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.OnOffCmd.ZCLOnOffCmdType.Value("ON")
config.zigbee_message.zcl_cmd.node_id = self._nodeId
config.zigbee_message.zcl_cmd.endpoint_index = self._endpointIndex
run_callback_threadsafe(
self._hass.loop, self._socket.send, config.SerializeToString()).result()
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self._state = False
config = self._driver_pb2.DriverConfig()
config.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("ZCL")
config.zigbee_message.zcl_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ZCLCmdType.Value("ON_OFF")
config.zigbee_message.zcl_cmd.onoff_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.OnOffCmd.ZCLOnOffCmdType.Value("OFF")
config.zigbee_message.zcl_cmd.node_id = self._nodeId
config.zigbee_message.zcl_cmd.endpoint_index = self._endpointIndex
run_callback_threadsafe(
self._hass.loop, self._socket.send, config.SerializeToString()).result()
def update(self):
"""Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
# TODO:
#self._light.update()
#self._state = self._light.is_on()
#self._brightness = self._light.brightness
def set(self, socket, whiteValue, intensity):
"""Sets all of the LEDS to a given rgbw value"""
config = self._driver_pb2.DriverConfig()
config.zigbee_message.type = self._comm_pb2.ZigBeeMsg.ZigBeeCmdType.Value("ZCL")
config.zigbee_message.zcl_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.ZCLCmdType.Value("ON_OFF")
config.zigbee_message.zcl_cmd.onoff_cmd.type = self._comm_pb2.ZigBeeMsg.ZCLCmd.OnOffCmd.ZCLOnOffCmdType.Value("TOGGLE")
config.zigbee_message.zcl_cmd.node_id = self._nodeId
config.zigbee_message.zcl_cmd.endpoint_index = self._endpointIndex
run_callback_threadsafe(
self._hass.loop, self._socket.send, config.SerializeToString()).result()