Skip to content

Instantly share code, notes, and snippets.

@dries007
Last active December 16, 2022 21:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dries007/436fcd0549a52f26137bca942fef771a to your computer and use it in GitHub Desktop.
Save dries007/436fcd0549a52f26137bca942fef771a to your computer and use it in GitHub Desktop.
Dobiss Domotica / Home Automation Protocols

Dobiss Domotica / Home Automation Protocols

This is information related to the Dobiss protocols that I have discovered by probing various things on my devices. My installation is a pair of modules marked "Dobiss Domotics Relay Module single pole 8 channel Model DO4511" connected by a black CAN cable. The button bus is connected to the first module.

WARNING Assume that anything you can touch or probe or poke a wire into is on LIVE VOLTAGE and will kill you if you touch it. I'm not responsible for whatever you decide to do with this information.

In particular make sure of the following: Measure that there is no voltage between the ground and your earch connection.

I used can-utils on Linux to get this information, all packets are in the cansend can_frame format unless otherwise noted.

Pinouts

The CAN bus goes over the black RJ12 connector in the center of the unit, make sure you disable the terminator resistor if you add a cable. You can use an RJ11 (telephone) cable as well, since you only need 3 concuctors and the outermost pins are not used.

This is the pinout for an RJ12 cable:

  1. CAN H
  2. GND (don't forget!)
  3. CAN L

In my setup, I connected these via an unshielded handtwisted cable to the CAN screw terminals of a RS485/CAN HAT from Waveshare.

CAN Protocol

The CAN bus speed is 125kbit/s (sometimes known as "low speed can").

Get State

> 01FCmm01#mmrr
< 01FDFF01#ss
  • mm = module
  • rr = relay 0-7 (or 8-B for the linkout 12v pins)
  • ss = state 0 = off, 1 = on

Examples:

> 01FC0101#0104
< 01FDFF01#00
> 01FC0101#0103
< 01FDFF01#00
> 01FC0101#0102
< 01FDFF01#00
> 01FC0101#0101
< 01FDFF01#00
> 01FC0101#0100
< 01FDFF01#00

Set State

Send to control a relay/output. Note that if a relay is set on the module where the button bus comes in, you won't see the tx message on the CAN bus but you will get the rx message.

> 01FCmm02#mmrrssFFFF
< 0002FF01#mmrrss
  • mm = module
  • rr = relay 0-7 (or 8-B for the linkout 12v pins)
  • ss = state 0 = off, 1 = on (2 = toggle, tx only, rx is always 0 or 1)

Note that the actual modules send along some more bytes (0x64FFFF) but those don't seem to be required for simple on-off operations. Since I don't have dimmers or other modules I cannot test these, but I assume that's what these bytes are for.

Examples:

> 01FC0202#020300FFFF64FFFF
< 0002FF01#020300
> 01FC0202#020400FFFF64FFFF
< 0002FF01#020400
> 01FC0202#020500FFFF64FFFF
< 0002FF01#020500
> 01FC0202#020200FFFF64FFFF
< 0002FF01#020200
> 01FC0202#020100FFFF64FFFF
< 0002FF01#020100
> 01FC0202#020000FFFF64FFFF

Onewire

The button bus is a onewire based bus. Since I can control all of what I need via CAN, I'm not doing further investigation.

import can
# ip link set can0 type can bitrate 125000
# sudo ifconfig can0 txqueuelen 1000
# sudo ifconfig can0 up
bus = can.interface.Bus(bustype='socketcan', channel='can0', bitrate=125000)
MAPPING = {
# name -> (module, relay)
"wc": (1, 0),
"bed": (1, 1),
"hal": (1, 2),
"desk": (1, 3),
"spot1": (1, 4),
"living": (1, 5),
"spot2": (1, 6),
"kitchen": (1, 7),
"pantry": (2, 0),
"bathroom": (2, 1),
"out front": (2, 2),
"out back": (2, 3),
}
STATE = {
"off": 0,
"on": 1,
"toggle": 2,
}
def send(name, state):
if name not in MAPPING:
print("Name", name, "is not known. Options:", ','.join(MAPPING.keys()))
if state not in STATE:
print("Name", name, "is not known. Options:", ','.join(STATE.keys()))
m, r = MAPPING[name]
s = STATE[state]
bus.send(can.Message(arbitration_id=int(f"01FC0{m}02", 16), data=bytes.fromhex(f'0{m}0{r}0{s}FFFF'), is_extended_id=True))
while True:
cmd = input("> ")
name, state = cmd.split(" ", 2)
if name == "all":
for x in MAPPING:
send(x, state)
else:
send(name, state)
"""
Dobiss CAN bus light integration
Documentation of protocol: https://gist.github.com/dries007/436fcd0549a52f26137bca942fef771a
The CAN bus must be enabled by the system before this module is loaded.
systemd-networkd can do this for you, as CAN is a supported network.
Install by creating this file and `manifest.json` in `custom_components/dobiss_can`.
Author: Dries007 - 2021
Licence: MIT
"""
import logging
import asyncio
from typing import Optional, Any
import can
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.components.light import LightEntity, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_HOST, CONF_LIGHTS
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.DEBUG)
CONF_BUSTYPE = 'bustype'
CONF_MODULE = 'module'
CONF_RELAY = 'relay'
"""
light:
- platform: dobiss_can
lights:
- name: WC
module: 1
relay: 0
- name: Bedroom
module: 1
relay: 1
"""
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default="can0"): cv.string,
vol.Optional(CONF_BUSTYPE, default="socketcan"): cv.string,
vol.Required(CONF_LIGHTS): vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_MODULE): cv.positive_int,
vol.Required(CONF_RELAY): cv.positive_int,
})]),
})
async def async_setup_platform(hass: HomeAssistant, config, async_add_entities, discovery_info=None):
# Load CAN bus. Must be operational already (done by external network tool).
# Setting bitrate might work, but ideally that should also be set alreaday.
# We only care about messages related to feedback from a SET command or the reply from a GET command.
bus = can.Bus(channel=config[CONF_HOST], bustype=config[CONF_BUSTYPE], bitrate=125000, receive_own_messages=False, can_filters=[
{"can_id": 0x0002FF01, "can_mask": 0x1FFFFFFF, "extended": True}, # Reply to SET
{"can_id": 0x01FDFF01, "can_mask": 0x1FFFFFFF, "extended": True}, # Reply to GET
])
# Global CAN bus lock, required since the reply to a GET does not include any differentiator.
lock = asyncio.Lock(loop=hass.loop)
# All config entries get turned into entities that listen for updates on the bus.
entities = [DobissLight(bus, o, lock) for o in config[CONF_LIGHTS]]
can.Notifier(bus, entities, loop=hass.loop)
async_add_entities(entities)
# Success.
return True
class DobissLight(LightEntity, can.Listener):
def __init__(self, bus: can.BusABC, o: dict, lock: asyncio.Lock):
self._bus = bus
self._lock = lock
# Unpack some config values
self._name: str = o[CONF_NAME]
self._module = o[CONF_MODULE]
self._relay = o[CONF_RELAY]
# Prepare fixed ids & payloads
self._set_id: int = 0x01FC0002 | (self._module << 8)
self._bytes_off: bytes = bytes((self._module, self._relay, 0, 0xFF, 0xFF))
self._bytes_on: bytes = bytes((self._module, self._relay, 1, 0xFF, 0xFF))
self._bytes_status: bytes = bytes((self._module, self._relay))
# Internal state of light
self._state: Optional[bool] = None
# Internals to do locking & support GET operation
self._awaiting_update = False
self._event_update = asyncio.Event()
# Logger
self._log = _LOGGER.getChild(self.name)
@property
def name(self):
return self._name
@property
def is_on(self):
return self._state
@property
def unique_id(self) -> str:
return f"dobiss.{self._module}.{self._relay}"
async def async_turn_on(self, **kwargs: Any) -> None:
self._bus.send(can.Message(arbitration_id=self._set_id, data=self._bytes_on, is_extended_id=True), timeout=.1)
async def async_turn_off(self, **kwargs: Any) -> None:
self._bus.send(can.Message(arbitration_id=self._set_id, data=self._bytes_off, is_extended_id=True), timeout=.1)
async def on_message_received(self, msg):
# Reply to SET, this we can filter because data contains data from.
if msg.arbitration_id == 0x0002FF01 and msg.data[0] == self._module and msg.data[1] == self._relay:
self._state = msg.data[2] == 1
self.schedule_update_ha_state()
# Reply to GET, this we can only filter by _knowing_ that we are waiting on an update.
if msg.arbitration_id == 0x01FDFF01 and self._awaiting_update:
self._state = msg.data[0] == 1
async def async_update(self):
# The update cycle must be blocked on the CAN bus lock.
async with self._lock:
# Inform handler that we expect an update.
self._awaiting_update = True
# Small delay, otherwise we overload the CAN module.
await asyncio.sleep(.01)
# Ask CAN module for an update
self._bus.send(can.Message(arbitration_id=0x01FCFF01, data=self._bytes_status, is_extended_id=True), timeout=.1)
# Wait for reply to come
await self._event_update.wait()
# Small delay, otherwise we overload the CAN module.
await asyncio.sleep(.01)
# OK, we're done.
self._awaiting_update = False
{
"domain": "dobiss_can",
"name": "Dobiss CAN Bus",
"documentation": "https://gist.github.com/dries007/436fcd0549a52f26137bca942fef771a",
"dependencies": [],
"codeowners": [],
"requirements": [
"python-can"
],
"iot_class": "local_push",
"version": "0.0.1"
}
@dries007
Copy link
Author

dries007 commented Nov 8, 2021

Update: By adding the call to schedule_update_ha_state from on_message_received HA is instantly updated when an external source triggers a state change.
I still keep polling enabled in the entities, as it keeps the state in sync at startup or if weird stuff happens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment