Skip to content

Commit

Permalink
Support Sonoff RF Bridge 433
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Jan 18, 2020
1 parent 2f2c4ff commit 1049806
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 12 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.2.3] - 2020-01-18

### Added

- Support Sonoff RF Bridge 433

## [0.2.2] - 2020-01-18

### Fixed
Expand Down
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@
удобной интеграции в голосовые ассистенты
- есть возможность объединить несколько каналов в один источник света и
управлять яркостью

## Протестированные устройства

[Changelog in English](./CHANGELOG.md)
- Sonoff Basic (самой первой версии)
- Sonoff Mini (режим DIY включать не нужно)
- Sonoff TH (показывает температуру и влажность)
- Sonoff 4CH Pro R2
- Sonoff RF Bridge 433
- Выключатели [MiniTiger](https://ru.aliexpress.com/item/33016227381.html)

## Примеры конфигов

Expand Down Expand Up @@ -79,6 +86,45 @@ sonoff:
channels: [3, 4]
```

## Sonoff RF Bridge 433

Хоть компонент и поддерживает обучение - рекомендуется обучать кнопки через
приложение eWeLink.

`command` - порядковый номер изученной кнопки в приложении

При получении комманды создаётся событие `sonoff.remote` с порядковым номером
команды и временем срабатывание (в UTC, присылает устройство).

```yaml
automation:
- alias: Test RF
trigger:
platform: event
event_type: sonoff.remote
event_data:
command: 0
action:
service: homeassistant.toggle
entity_id: remote.sonoff_1000abcdefg
script:
send_num1:
sequence:
- service: remote.send_command
data:
entity_id: remote.sonoff_1000abcdefg
command: 1
send_num111:
sequence:
- service: remote.send_command
data:
entity_id: remote.sonoff_1000abcdefg
command: [1, 1, 1]
delay_secs: 1
```

## Параметры:

- **reload** - *optional*
Expand Down
41 changes: 30 additions & 11 deletions custom_components/sonoff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
ZEROCONF_NAME = 'eWeLink_{}._ewelink._tcp.local.'


def setup(hass, config):
config = config[DOMAIN]
def setup(hass, hass_config):
config = hass_config[DOMAIN]

# load devices from file in config dir
filename = hass.config.path('.sonoff.json')
Expand Down Expand Up @@ -57,6 +57,11 @@ def setup(hass, config):
_LOGGER.error("Empty device list")
return False

# add deviceid to all device config
for k, v in devices.items():
if 'deviceid' not in v:
v['deviceid'] = k

hass.data[DOMAIN] = devices

def add_device(devicecfg: dict, state: dict):
Expand All @@ -72,13 +77,18 @@ def add_device(devicecfg: dict, state: dict):
if not device_class:
if 'switch' in state:
device_class = 'switch'
else:
elif 'switches' in state:
device_class = ['switch'] * 4
elif devicecfg.get('uiid') == 28:
device_class = 'remote'
else:
_LOGGER.error(f"Unknown device_class {deviceid}")
return

if isinstance(device_class, str):
# read single device_class
info = {'deviceid': deviceid, 'channels': None}
load_platform(hass, device_class, DOMAIN, info, config)
load_platform(hass, device_class, DOMAIN, info, hass_config)
else:
# read multichannel device_class
for channels, component in enumerate(device_class, 1):
Expand All @@ -91,7 +101,7 @@ def add_device(devicecfg: dict, state: dict):
channels = [channels]

info = {'deviceid': deviceid, 'channels': channels}
load_platform(hass, component, DOMAIN, info, config)
load_platform(hass, component, DOMAIN, info, hass_config)

listener = EWeLinkListener(devices)
listener.listen(add_device)
Expand Down Expand Up @@ -153,15 +163,14 @@ def add_service(self, zeroconf: Zeroconf, type_: str, name: str):
return

data = utils.decrypt(properties, devicekey)
# Fix Sonoff RF Bridge sintax bug
if data.startswith(b'{"rf'):
data = data.replace(b'"="', b'":"')
state = json.loads(data)
_LOGGER.debug(f"State: {state}")
else:
raise NotImplementedError()

# TODO: fix me
if 'deviceid' not in config:
config['deviceid'] = deviceid

self.devices[deviceid] = EWeLinkDevice(host, config, state, zeroconf)

self._add_device(config, state)
Expand Down Expand Up @@ -235,6 +244,9 @@ def update_service(self, zeroconf: Zeroconf, type_: str, name: str):

if properties.get('encrypt'):
data = utils.decrypt(properties, self.config['devicekey'])
# Fix Sonoff RF Bridge sintax bug
if data.startswith(b'{"rf'):
data = data.replace(b'"="', b'":"')
data = json.loads(data)
_LOGGER.debug(f"Data: {data}")
else:
Expand All @@ -252,18 +264,19 @@ def send(self, command: str, data: dict):
:param data: Данные для команды
:return:
"""
_LOGGER.debug(f"Send {command} to {self.deviceid}")

payload = utils.encrypt({
'sequence': str(int(time.time())),
'deviceid': self.deviceid,
'selfApikey': '123',
'data': data
}, self.devicekey)

_LOGGER.debug(f"Send {command} to {self.deviceid}: {payload}")

try:
r = requests.post(f'http://{self.host}:8081/zeroconf/{command}',
json=payload, timeout=10)
_LOGGER.debug(r.text)
if r.json()['error'] != 0:
_LOGGER.warning(
f"Error when send {command} to {self.deviceid}")
Expand Down Expand Up @@ -323,3 +336,9 @@ def turn_bulk(self, channels: dict):
for channel, switch in channels.items()
]
self.send('switches', {'switches': switches})

def transmit(self, channel: int):
self.send('transmit', {"rfChl": channel})

def learn(self, channel: int):
self.send('capture', {"rfChl": channel})
89 changes: 89 additions & 0 deletions custom_components/sonoff/remote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import asyncio
import logging
from typing import Optional

from homeassistant.components.remote import RemoteDevice, ATTR_DELAY_SECS, \
ATTR_COMMAND, SUPPORT_LEARN_COMMAND, DEFAULT_DELAY_SECS

from . import DOMAIN, EWeLinkDevice

_LOGGER = logging.getLogger(__name__)


def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None:
return

deviceid = discovery_info['deviceid']
device = hass.data[DOMAIN][deviceid]
add_entities([EWeLinkRemote(device)])


class EWeLinkRemote(RemoteDevice):
def __init__(self, device: EWeLinkDevice):
self.device = device
self._name = None
self._state = True

device.listen(self._update)

async def async_added_to_hass(self) -> None:
# Присваиваем имя устройства только на этом этапе, чтоб в `entity_id`
# было "sonoff_{unique_id}". Если имя присвоить в конструкторе - в
# `entity_id` попадёт имя в латинице.
self._name = self.device.name

def _update(self, device: EWeLinkDevice, schedule_update: bool = True):
for k, v in device.state.items():
if k.startswith('rfTrig'):
channel = int(k[6:])
self.hass.bus.fire('sonoff.remote', {
'entity_id': self.entity_id, 'command': channel, 'ts': v})
elif k.startswith('rfChl'):
channel = int(k[5:])
_LOGGER.info(f"Learn command {channel}: {v}")
else:
break

@property
def should_poll(self) -> bool:
# Устройство само присылает обновление своего состояния по Multicast.
return False

@property
def unique_id(self) -> Optional[str]:
return self.device.deviceid

@property
def is_on(self) -> bool:
return self._state

def turn_on(self, **kwargs):
self._state = True
self.schedule_update_ha_state()

def turn_off(self, **kwargs):
self._state = False
self.schedule_update_ha_state()

@property
def supported_features(self):
return SUPPORT_LEARN_COMMAND

async def async_send_command(self, command, **kwargs):
if not self._state:
return

delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
for i, channel in enumerate(command):
if i:
await asyncio.sleep(delay)

self.device.transmit(int(channel))

def learn_command(self, **kwargs):
if not self._state:
return

command = kwargs[ATTR_COMMAND]
self.device.learn(int(command[0]))

0 comments on commit 1049806

Please sign in to comment.