Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nergal committed Jul 11, 2021
0 parents commit f6c7774
Show file tree
Hide file tree
Showing 17 changed files with 1,638 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[flake8]
ignore = E203, E266, E501, W503
# line length is intentionally set to 80 here because black uses Bugbear
# See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details
max-line-length = 80
max-complexity = 18
select = B,C,E,F,W,T4,B9
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Artem Poliukhovych

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
RUNNER=pipenv
SOURCE_FOLDER=custom_components/

.PHONY: lint
lint: black mypy flake

.PHONY: lintfix
lintfix: isort blackfix lint

.DEFAULT_GOAL := lint

.PHONY: mypy
mypy:
$(RUNNER) run mypy --ignore-missing-imports $(SOURCE_FOLDER)

.PHONY: flake
flake:
$(RUNNER) run flake8 $(SOURCE_FOLDER)

.PHONY: black
black:
$(RUNNER) run black --check $(SOURCE_FOLDER)

.PHONY: blackfix
blackfix:
$(RUNNER) run black $(SOURCE_FOLDER)

.PHONY: isort
isort:
$(RUNNER) run isort --atomic $(SOURCE_FOLDER)
16 changes: 16 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
homeassistant = "==2021.6.6"

[dev-packages]
black = "==21.6b0"
flake8 = "==3.9.2"
mypy = "==0.910"
isort = "==5.9.2"

[requires]
python_version = "3.8"
734 changes: 734 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Homeassistant Orvibo remote

[![hacs_badge][hasc-shield]](https://github.com/custom-components/hacs)
![Project Stage][stage-shield]
[![Code style: black][black-shield]](https://github.com/psf/black)
![GitHub][license-shield]

> :warning: **DISCLAIMER:** This code is an early alpha release with all related consequences. If you decide to use it, any feedback is appreciated
Remote integration for Orvibo AllOne IR remote. Designed to be used with [smartHomeHub/SmartIR](https://github.com/smartHomeHub/SmartIR) integration.

## Installation
You can install this remote via HACS, just add it as a custom repository by clicking a three dots in the top left corder on a HACS page.

Configuration example:
``` yaml
orvibo_remote:
remote:
- platform: orvibo_remote
host: 192.168.1.1
name: Orvibo AllOne
```
Configuration example with a SmartIR:
``` yaml
orvibo_remote:
remote:
- platform: orvibo_remote
host: 192.168.1.93
name: Orvibo AllOne

smartir:
climate:
- platform: smartir
name: Living AC
unique_id: living_ac
device_code: 1066
controller_data: remote.orvibo_remote_xxxxxxxxxxxx
```
> Small notice about included sources of asyncio_orvibo - it is a slightly modified code, and it has to be there to avoid raising an issue using a `reuse_address = True` inside that lib.

## Disclaimer
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with the Shenzhen ORVIBO Technology Co., LTD, or any of its subsidiaries or its affiliates. The official Orvibo website can be found at https://www.orvibo.com/en.

## License
This project is under the MIT license.

[!["Buy Me A Coffee"][coffee-shield]](https://www.buymeacoffee.com/nalecz)


[license-shield]: https://img.shields.io/github/license/nergal/homeassistant-orvibo-remote
[hasc-shield]: https://img.shields.io/badge/HACS-Custom-orange.svg
[coffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png
[stage-shield]: https://img.shields.io/badge/project%20stage-stage-orange.svg
[black-shield]: https://img.shields.io/badge/code%20style-black-000000.svg
Empty file added custom_components/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions custom_components/orvibo_remote/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import asyncio

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

PLATFORMS = ["remote"]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Orvibo remote from a config entry."""
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
return all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
3 changes: 3 additions & 0 deletions custom_components/orvibo_remote/asyncio_orvibo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import logging

_LOGGER = logging.getLogger(__name__)
178 changes: 178 additions & 0 deletions custom_components/orvibo_remote/asyncio_orvibo/allone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Created on 25 apr 2019
@author: Matteo
"""
import asyncio
import binascii
import logging
import struct
import time
from random import randint

from . import _LOGGER
from .const import CD_CONTINUE_WAITING, CD_RETURN_IMMEDIATELY
from .orvibo_udp import DISCOVERY_ALLONE, MAGIC, PADDING_1, OrviboUDP

LEARNIR_ID = b"\x6c\x73"
LEARNIR_LEN = b"\x00\x18"
LEARNIR_2 = b"\x01\x00\x00\x00\x00\x00"
EMITIR_ID = b"\x69\x63"
EMITIR_2 = b"\x65\x00\x00\x00"

LEARN_MAX_TIME = 40
# Datagram protocol


class AllOne(OrviboUDP):
def check_emitir_packet(self, data, addr):
return (
CD_RETURN_IMMEDIATELY
if len(data) >= 6 and data[4:6] == EMITIR_ID and self.is_my_mac(data)
else CD_CONTINUE_WAITING
)

async def emit_ir(self, irc, timeout=-1, retry=3):
if await self.subscribe_if_necessary():
plen = struct.pack(">H", len(irc) + 26)
ilen = struct.pack("<H", len(irc))
rnd = struct.pack("<H", randint(0, 65535))
pkt = (
MAGIC
+ plen
+ EMITIR_ID
+ self.mac
+ PADDING_1
+ EMITIR_2
+ rnd
+ ilen
+ irc
)
timeout = self.timeout if timeout <= 0 else timeout
rv = await OrviboUDP.protocol(
pkt, self.hp, self.check_emitir_packet, timeout, retry
)
if rv:
return rv[0]
return None

def check_learnir_init_packet(self, data, addr):
return (
CD_RETURN_IMMEDIATELY
if len(data) >= 6
and data[4:6] == (LEARNIR_ID)
and data[2:4] == LEARNIR_LEN
and self.is_my_mac(data)
else CD_CONTINUE_WAITING
)

def check_learnir_get_packet(self, data, addr):
return (
CD_RETURN_IMMEDIATELY
if len(data) >= 6
and data[4:6] == (LEARNIR_ID)
and data[2:4] > LEARNIR_LEN
and self.is_my_mac(data)
else CD_CONTINUE_WAITING
)

async def enter_learning_mode(self, timeout=-1, retry=3):
if await self.subscribe_if_necessary():
pkt = MAGIC + LEARNIR_LEN + LEARNIR_ID + self.mac + PADDING_1 + LEARNIR_2
timeout = self.timeout if timeout <= 0 else timeout
if await OrviboUDP.protocol(
pkt, self.hp, self.check_learnir_init_packet, timeout, retry
):
self.learning_time = time.time()
return True
return False

async def get_learned_key(self, timeout=30):
to = min(LEARN_MAX_TIME - (time.time() - self.learning_time), timeout)
if to > 0:
rv = await OrviboUDP.protocol(
None, self.hp, self.check_learnir_get_packet, to, 1
)
if rv and len(rv[0]) > 26:
return rv[0][26:]
return None

@staticmethod
async def discovery(broadcast_address="255.255.255.255", timeout=5, retry=3):
disc = await OrviboUDP.discovery(broadcast_address, timeout, retry)
hosts = dict()
for k, v in disc.items():
if v["type"] == DISCOVERY_ALLONE:
hosts[k] = AllOne(**v)
return hosts


if __name__ == "__main__": # pragma: no cover
import sys

async def testFake(n):
for i in range(n):
_LOGGER.debug("Counter is %d", i)
await asyncio.sleep(1)

async def discoveryTest():
v = await AllOne.discovery("192.168.25.255", 7, 3)
if v:
_LOGGER.info("Discovery str %s", v)
else:
_LOGGER.warning("Discovery failed")

async def subscribe_test():
a = AllOne(("192.168.25.41", 10000), b"\xac\xcf\x23\x72\x5a\x50")
rv = await a.subscribe_if_necessary()
if rv:
_LOGGER.info("Subscribe OK")
else:
_LOGGER.warning("Subscribe failed")

async def emit_test(keystr):
payload = binascii.unhexlify(keystr)
a = AllOne(("192.168.25.41", 10000), b"\xac\xcf\x23\x72\x5a\x50")
rv = await a.emit_ir(payload)
if rv:
_LOGGER.info("Emit OK %s", binascii.hexlify(rv).decode("utf-8"))
else:
_LOGGER.warning("Emit failed")

async def learn_test():
a = AllOne(("192.168.25.41", 10000), b"\xac\xcf\x23\x72\x5a\x50")
rv = await a.enter_learning_mode()
if rv:
_LOGGER.info("Entered learning mode: please press key")
rv = await a.get_learned_key()
if rv:
_LOGGER.info("Obtained %s", binascii.hexlify(rv).decode("utf-8"))
else:
_LOGGER.warning("No key pressed")
else:
_LOGGER.warning("Enter learning failed")

_LOGGER.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
_LOGGER.addHandler(handler)
loop = asyncio.get_event_loop()
try:
# asyncio.ensure_future(testFake(10))
# loop.run_until_complete(emit_test('00000000a801000000000000000098018e11951127029b0625029906270299062702380227023a0225023802270238022d023202270299062702990627029806270238022702380227023802270238022802370227023802270238022702980627023802240245021c02380227023802270238022702980627029c0623023802270298062702990627029b062502990627029906270220b7a1119d11270299062702990628029b06250238022702380227023802270238022702380227029906270299062702990627023802270238022a0234022702380227023802260238022702380226029a06260238022602380226023802260241021e02380227029b0624029906270238022702980627029b0625029906270299062702990629021db79f11a2112502990627029b0625029906270238022702380227023802270238022a02350227029906270299062702990628023702260238022702380227023802270238022702380226023b02240299062702380226023802270238022602380227023c0223029906270299062702380226029b062402990627029906270299062802980627020000'))
loop.run_until_complete(discoveryTest())
loop.run_until_complete(subscribe_test())
loop.run_until_complete(
emit_test(
"00000000ec000000000000000000dc00d2008604d200ec02d400e80ad4000d05d9000b05d2008404d4007103d4008132d400fc03d4001e06d400ec02d400ec02d4009705d2008404d400ec02d400ea02dc00ffff283e0100d200fd03d4008404d400ec02d400eb0ad2000d05d4000d05d4008404d4007103d6008132d200fd03d400610ad4002e07d400ec02d4009405d4008504d300ec02d400ea02d400ffff333e0100cc00fd03d4008404d300ed02d300eb0ad1000e05d3000e05d4008404d4007203d3008432d100fd03d300640ad1002f07d300ed02d3009505d3008504d400ee02d000eb02d3000000"
)
)
# loop.run_until_complete(learn_test())
except BaseException as ex:
_LOGGER.error("Test error %s", str(ex))
finally:
loop.close()
Loading

0 comments on commit f6c7774

Please sign in to comment.