Skip to content
This repository has been archived by the owner on Jan 13, 2022. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Dec 30, 2019
0 parents commit f8b02a9
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
@@ -0,0 +1,46 @@
# Twinkly for Home-Assistant

This projects lets you control your [twinkly christmas lights](https://twinkly.com/)
from [Home-Assistant](https://www.home-assistant.io/)

Using this component you are able to:
- Turn lights on and off
- Configure the brigthness

![integration example](./assets/integration.png "Integration example")

## Setup
This integration is currently acheived as a _"custom component"_ which has to be installed manually:

1. From the root directory of your HA, create a directory `custom_components/twinkly`
1. Downalod all files from the [`twinkly` directory](./twinkly) of this repo and copy them in the folder you just created
1. In you `configuration.yaml`, in the `light` section add your twinkly device:

```yaml
light:
- platform: twinkly
host: 192.168.123.123 # cf. remaks below
```

> **Remaks**
>
> We currently do not support floating IP address, so make sure to assign a static IP to your twkinly device.
> You can configure it in your router.
## Road map
- [ ] Configure HACS
- [ ] Add support of online / offline (and make sure that we don't have to restart HA when we plug-in a device)
- [ ] Add discovery of devices on LAN
- [ ] Add support of floating IP adress
- [ ] Merge as a component in the HA repo

## Thanks and ref
https://labs.f-secure.com/blog/twinkly-twinkly-little-star

@joshkay https://github.com/joshkay/home-assistant-twinkly

https://xled-docs.readthedocs.io/en/latest/rest_api.html




Binary file added assets/integration.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions twinkly/__init__.py
@@ -0,0 +1,3 @@
"""The twinkly component"""


182 changes: 182 additions & 0 deletions twinkly/light.py
@@ -0,0 +1,182 @@
"""The Twinkly platform for light component"""

import logging
from typing import Any, Optional
import voluptuous as vol
from aiohttp import ClientResponseError
from homeassistant.components.light import (ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession

_LOGGER = logging.getLogger(__name__)

ATTR_HOST = 'host'
HIDDEN_ATTR = (
'device_name', # Normalized in the name property
'code', # This is the internal status code of the API response
'copyright', # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI
'mac' # Does not report the actual device mac address
)

AUTH_HEADER = 'X-Auth-Token'

EP_DEVICE_INFO = "gestalt"
EP_MODE = "led/mode"
EP_BRIGHTNESS = "led/out/brightness"
EP_LOGIN = "login"
EP_VERIFY = "verify"

CONF_HOST = 'host'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
}
)

async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Setup callback of the platform."""

session = async_get_clientsession(hass)
async_add_entities([TwinklyLight(session, config[CONF_HOST])], True)

return True

class TwinklyLight(Light):
"""Implementation of the light for the Twinkly service."""

def __init__(self, session, host):
"""Initialize a TwinklyLight."""
self._name = 'Twinkly light'
self._is_on = False
self._brightness = 0

self._session = session
self._host = host
self._base_url = "http://" + host + "/xled/v1/"
self._token = None
self._attributes = { ATTR_HOST: self._host }

@property
def supported_features(self):
return SUPPORT_BRIGHTNESS

@property
def should_poll(self) -> bool:
return True

@property
def available(self) -> bool:
return True

@property
def name(self) -> str:
"""Name of the device."""
return self._name

@property
def is_on(self) -> bool:
"""Return true if light is on."""
return self._is_on

@property
def brightness(self) -> Optional[int]:
"""Return the brightness of the light."""
return self._brightness

@property
def state_attributes(self) -> dict:
"""Return device specific state attributes."""

attributes = self._attributes

# Make sure to update any normalized property
attributes[ATTR_HOST] = self._host
attributes[ATTR_BRIGHTNESS] = self._brightness

return attributes

async def async_turn_on(self, **kwargs) -> None:
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
await self.set_brightness(kwargs[ATTR_BRIGHTNESS])

await self.set_is_on(True)

async def async_turn_off(self, **kwargs) -> None:
"""Turn device off."""
await self.set_is_on(False)

async def async_update(self) -> None:
"""Asynchronously updates the device properties."""
_LOGGER.info("Updating '%s'", self._host)
self._is_on = await self.get_is_on()
self._brightness = await self.get_brigthness()

device_info = await self.get_device_info()
self._name = device_info['device_name']
for key,value in device_info.items():
if key not in HIDDEN_ATTR:
self._attributes[key] = value

async def set_is_on(self, is_on: bool) -> None:
await self.send_request(EP_MODE, {'mode': "movie" if is_on else "off"})

async def set_brightness(self, brightness) -> None:
await self.send_request(EP_BRIGHTNESS, {"value":int(int(brightness) / 2.55), "type": "A"})

async def get_device_info(self) -> None:
return await self.send_request(EP_DEVICE_INFO)

async def get_is_on(self) -> bool:
return (await self.send_request(EP_MODE))['mode'] != "off"

async def get_brigthness(self) -> int:
brightness = await self.send_request(EP_BRIGHTNESS)
return int(int(brightness['value']) * 2.55) if brightness['mode'] == "enabled" else 255

async def send_request(self, endpoint: str, data: Any=None, retry: int=1) -> Any:
"""Send an authenticated request with auto retry if not yet auth."""
if self._token is None:
await self.auth()

try:
response = await self._session.request(
method = "GET" if data is None else "POST",
url = self._base_url + endpoint,
json = data,
headers = {AUTH_HEADER: self._token},
raise_for_status = True
)
result = await response.json() if data is None else None
return result
except ClientResponseError as err:
if err.code == 401 and retry > 0:
self._token = None
return await self.send_request(endpoint, data, retry - 1)
raise

async def auth(self) -> None:
"""Authenticates to the device."""
_LOGGER.info("Authenticating to '%s'", self._host)

# Login to the device using a hard-coded challenge
login_response = await self._session.post(
url = self._base_url + EP_LOGIN,
json = {"challenge":"Uswkc0TgJDmwl5jrsyaYSwY8fqeLJ1ihBLAwYcuADEo="},
raise_for_status = True)
login_result = await login_response.json()
_LOGGER.debug("Sucessfully logged-in to '%s'", self._host)

# Get the token, but do not store it until it get verified
token = login_result['authentication_token']

# Verify the token is valid
await self._session.post(
url = self._base_url + EP_VERIFY,
headers= {AUTH_HEADER: token},
raise_for_status = True
)
_LOGGER.debug("Sucessfully verified token to '%s'", self._host)

self._token = token
8 changes: 8 additions & 0 deletions twinkly/manifest.json
@@ -0,0 +1,8 @@
{
"domain": "twinkly",
"name": "Twkinkly",
"documentation": "https://www.home-assistant.io/integrations/twinkly",
"requirements": [],
"dependencies": [],
"codeowners": ["@dr1rrb"]
}

0 comments on commit f8b02a9

Please sign in to comment.