Quick and Dirty Custom Component - Bringing it up to speed

A couple of years ago, on a much older version of home assistant, I wrote a very quick and dirty custom component to control my “Lifesmart Cololight Quantum” lights bought from AliExpress.
In the old version. I had one file called “quantumlight.py” in the folder “config/custom_components/light”
I tried dropping it in there on the new version (I am on the latest 2022.3.7) but this does not seem to work. What do I need to do to bring this up to speed so I can use it again?
Here is the contents of the quantumlight.py

"""
Support for Quantum lights.
"""
import logging

import voluptuous as vol

from homeassistant.components.light import (
    ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, ATTR_EFFECT, Light,
    PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
import socket
import codecs
import colorsys

IPADDR = '192.168.0.45'
PORTNUM = 8900


_LOGGER = logging.getLogger(__name__)

CONF_IP = 'ip'

DEFAULT_NAME = 'Quantum Light'

SUPPORT_QUANTUM = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT 

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_IP, default="192.168.0.209"): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})


EFFECT_SAVASANA = "Savasana"
EFFECT_SUNRISE = "Sunrise"
EFFECT_UNICORNS = "Unicorns"
EFFECT_PENSIVE = "Pensive"
EFFECT_CIRCUS = "The Circus"
EFFECT_INSTAGRAMMER = "Instagrammer"
EFFECT_EIGHTIESCLUB= "80's Club"
EFFECT_CHERRYBLOSSOMS = "Cherry Blossoms"
EFFECT_COCKTAILPARADE = "Cocktail Parade"
EFFECT_CUSTOMIZED = "Customized"

QUANTUM_EFFECT_LIST = [
    EFFECT_SAVASANA,
    EFFECT_SUNRISE,
    EFFECT_UNICORNS,
    EFFECT_PENSIVE,
    EFFECT_CIRCUS,
    EFFECT_INSTAGRAMMER,
    EFFECT_EIGHTIESCLUB,
    EFFECT_CHERRYBLOSSOMS,
    EFFECT_COCKTAILPARADE,
    EFFECT_CUSTOMIZED]


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up Quantum device specified by serial number."""

    name = config.get(CONF_NAME)
    serial = config.get(CONF_IP)


    stick = "hello"
    add_entities([QuantumLight(stick, name)], True)


class QuantumLight(Light):
    """Representation of a Quantum light."""

    def __init__(self, stick, name):
        """Initialize the light."""
        self._name = name
        self._hs_color = None
        self._effect = None
        self._brightness = 255
        self._isOn = False
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)



    @property
    def should_poll(self):
        """Set up polling."""
        return True

    @property
    def name(self):
        """Return the name of the light."""
        return self._name

    @property
    def brightness(self):
        """Read back the brightness of the light."""
        return self._brightness

    @property
    def effect(self):
        """Read back the effect of the light."""
        return self._effect

    @property
    def hs_color(self):
        """Read back the color of the light."""
        return self._hs_color

    @property
    def is_on(self):
        """Return True if entity is on."""
        return self._isOn

    @property
    def supported_features(self):
        """Flag supported features."""
        return SUPPORT_QUANTUM

    @property
    def effect_list(self):
        """Return the list of supported effects."""
        return QUANTUM_EFFECT_LIST

    # def update(self):
    #     """Read back the device state."""
    #     //rgb_color = self._stick.get_color()
    #     hsv = color_util.color_RGB_to_hsv(*rgb_color)
    #     self._hs_color = hsv[:2]
    #     self._brightness = hsv[2]

    def turn_on(self, **kwargs):
        """Turn the device on."""
        self._socket.connect((IPADDR, PORTNUM))
        _LOGGER.info(kwargs)
        self._isOn=True
        if ATTR_HS_COLOR in kwargs:
            self._hs_color = kwargs[ATTR_HS_COLOR]
            self._effect = None
            
        if ATTR_BRIGHTNESS in kwargs:
            self._brightness = kwargs[ATTR_BRIGHTNESS]

        if ATTR_EFFECT in kwargs:      
            self._effect = kwargs[ATTR_EFFECT]
            self._hs_color = None
        else:
            decode_hex = codecs.getdecoder("hex_codec")
            self._socket.send(decode_hex('535a3030000000000020000000000000000000000000000000003900000000000000000004390301cf0f')[0]) #
            

        #Brightness
        if self._brightness is not None:
          hexBrightnessArr = ['53','5a','30','30','00','00','00','00','00','20','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','83','00','00','00','00','00','00','00','00','00','04','83','03','01','cf','20']
          intBrightnessArr = []
          for x in hexBrightnessArr:
              intBrightnessArr.append(int(x, 16))
              
          maxBrightness = 100
          if self._hs_color is None:
              maxBrightness = 100
          brightnessval = int((((self._brightness  - 0) * (maxBrightness - 0)) / (255 - 0)) + 0)
          intBrightnessArr[41] = brightnessval
          _LOGGER.info(brightnessval)
          bytes = bytearray(intBrightnessArr)
          self._socket.send(bytes)
        
        #Colour
        if self._hs_color is not None:
            rgb = list(color_util.color_hs_to_RGB(*self._hs_color))
            hexColourArr = ['53','5a','30','30','00','00','00','00','00','23','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','5a','00','00','00','00','00','00','00','00','00','04','5a','06','02','ff','00','ff','00','04']
            intColourArr = []
            for x in hexColourArr:
                intColourArr.append(int(x, 16))
           
            intColourArr[42] = rgb[0] if rgb[0] < 255 else 255
            intColourArr[43] = rgb[1] if rgb[1] < 255 else 255
            intColourArr[44] = rgb[2] if rgb[2] < 255 else 255
            bytes = bytearray(intColourArr)
            self._socket.send(bytes)

        if self._effect is not None:
            effects_map = {
                EFFECT_SAVASANA: '535a3030000000000023000000000000000000000000000000004400000000000000000004440602ff04970400',
                EFFECT_SUNRISE: '535a3030000000000023000000000000000000000000000000004500000000000000000004450602ff01c10a00',
                EFFECT_UNICORNS: '535a3030000000000023000000000000000000000000000000004600000000000000000004460602ff049a0e00',
                EFFECT_PENSIVE: '535a3030000000000023000000000000000000000000000000004700000000000000000004470602ff04c40600',
                EFFECT_CIRCUS: '535a3030000000000023000000000000000000000000000000004800000000000000000004480602ff04810130',
                EFFECT_INSTAGRAMMER: '535a3030000000000023000000000000000000000000000000004900000000000000000004490602ff03bc0190',
                EFFECT_EIGHTIESCLUB: '535a3030000000000023000000000000000000000000000000004a000000000000000000044a0602ff049a0000',
                EFFECT_CHERRYBLOSSOMS: '535a3030000000000023000000000000000000000000000000004b000000000000000000044b0602ff04940800',
                EFFECT_COCKTAILPARADE: '535a3030000000000023000000000000000000000000000000004100000000000000000004410602ff05bd0690',
                EFFECT_CUSTOMIZED: '535a3030000000000023000000000000000000000000000000004c000000000000000000044c0602ff06940900',                
            }
            effectCommand = effects_map[self._effect]
            decode_hex = codecs.getdecoder("hex_codec")
            self._socket.send(decode_hex(effectCommand)[0])
       


    def turn_off(self, **kwargs):
        """Turn the device off."""
        self._socket.connect((IPADDR, PORTNUM))
        decode_hex = codecs.getdecoder("hex_codec")
        self._socket.send(decode_hex('535a3030000000000020000000000000000000000000000000003800000000000000000004380301ce1e')[0])
        self._isOn=False

Each integration now has its own directory with different platforms inside. Take a look in core/homeassistant/components at dev · home-assistant/core · GitHub

For example in the very first one, abode, there are files for alarm_control_panel.py, binary_sensor.py, camera.py and quite a few others. That is because abode provides all those types on entities.

You’ll also need a manifest.json and i think __init__.py is needed, although the latter doesn’t need to be more than a comment.

So, assuming you want to call your integration quantum you’ll have custom_components/quantum/__init__.py, custom_components/quantum/manifest.json and custom_components/quantum/light.py at a minimum.

1 Like

This was a great help! Thank you.
To get it working I had to

  • Create a folder called quantum
  • Create an init.py with just the comment in
  • Rename the main file light.py
  • Add the line DOMAIN=“quantum” in the light.py file
  • Replace Light with LightEntity in the imports section and constructor.

Here is the fixed light.py:

"""
Support for Quantum lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.Quantumlight/
"""
import logging

import voluptuous as vol

from homeassistant.components.light import (
    ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_EFFECT, ATTR_EFFECT, LightEntity,
    PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
import socket
import codecs
import colorsys

IPADDR = '192.168.1.240'
PORTNUM = 8900
DOMAIN='quantum'

_LOGGER = logging.getLogger(__name__)

CONF_IP = 'ip'

DEFAULT_NAME = 'Quantum Light'

SUPPORT_QUANTUM = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT 

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_IP, default="192.168.1.240"): cv.string,
    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})


EFFECT_SAVASANA = "Savasana"
EFFECT_SUNRISE = "Sunrise"
EFFECT_UNICORNS = "Unicorns"
EFFECT_PENSIVE = "Pensive"
EFFECT_CIRCUS = "The Circus"
EFFECT_INSTAGRAMMER = "Instagrammer"
EFFECT_EIGHTIESCLUB= "80's Club"
EFFECT_CHERRYBLOSSOMS = "Cherry Blossoms"
EFFECT_COCKTAILPARADE = "Cocktail Parade"
EFFECT_CUSTOMIZED = "Customized"

QUANTUM_EFFECT_LIST = [
    EFFECT_SAVASANA,
    EFFECT_SUNRISE,
    EFFECT_UNICORNS,
    EFFECT_PENSIVE,
    EFFECT_CIRCUS,
    EFFECT_INSTAGRAMMER,
    EFFECT_EIGHTIESCLUB,
    EFFECT_CHERRYBLOSSOMS,
    EFFECT_COCKTAILPARADE,
    EFFECT_CUSTOMIZED]


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up Quantum device specified by serial number."""

    name = config.get(CONF_NAME)
    serial = config.get(CONF_IP)


    stick = "hello"
    add_entities([QuantumLight(stick, name)], True)


class QuantumLight(LightEntity):
    """Representation of a Quantum light."""

    def __init__(self, stick, name):
        """Initialize the light."""
        self._name = name
        self._hs_color = None
        self._effect = None
        self._brightness = 255
        self._isOn = False
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)



    @property
    def should_poll(self):
        """Set up polling."""
        return True

    @property
    def name(self):
        """Return the name of the light."""
        return self._name

    @property
    def brightness(self):
        """Read back the brightness of the light."""
        return self._brightness

    @property
    def effect(self):
        """Read back the effect of the light."""
        return self._effect

    @property
    def hs_color(self):
        """Read back the color of the light."""
        return self._hs_color

    @property
    def is_on(self):
        """Return True if entity is on."""
        return self._isOn

    @property
    def supported_features(self):
        """Flag supported features."""
        return SUPPORT_QUANTUM

    @property
    def effect_list(self):
        """Return the list of supported effects."""
        return QUANTUM_EFFECT_LIST

    # def update(self):
    #     """Read back the device state."""
    #     //rgb_color = self._stick.get_color()
    #     hsv = color_util.color_RGB_to_hsv(*rgb_color)
    #     self._hs_color = hsv[:2]
    #     self._brightness = hsv[2]

    def turn_on(self, **kwargs):
        """Turn the device on."""
        self._socket.connect((IPADDR, PORTNUM))
        _LOGGER.info(kwargs)
        self._isOn=True
        if ATTR_HS_COLOR in kwargs:
            self._hs_color = kwargs[ATTR_HS_COLOR]
            self._effect = None
            
        if ATTR_BRIGHTNESS in kwargs:
            self._brightness = kwargs[ATTR_BRIGHTNESS]

        if ATTR_EFFECT in kwargs:      
            self._effect = kwargs[ATTR_EFFECT]
            self._hs_color = None
        else:
            decode_hex = codecs.getdecoder("hex_codec")
            self._socket.send(decode_hex('535a3030000000000020000000000000000000000000000000003900000000000000000004390301cf0f')[0]) #
            

        #Brightness
        if self._brightness is not None:
          hexBrightnessArr = ['53','5a','30','30','00','00','00','00','00','20','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','83','00','00','00','00','00','00','00','00','00','04','83','03','01','cf','20']
          intBrightnessArr = []
          for x in hexBrightnessArr:
              intBrightnessArr.append(int(x, 16))
              
          maxBrightness = 100
          if self._hs_color is None:
              maxBrightness = 100
          brightnessval = int((((self._brightness  - 0) * (maxBrightness - 0)) / (255 - 0)) + 0)
          intBrightnessArr[41] = brightnessval
          _LOGGER.info(brightnessval)
          bytes = bytearray(intBrightnessArr)
          self._socket.send(bytes)
        
        #Colour
        if self._hs_color is not None:
            rgb = list(color_util.color_hs_to_RGB(*self._hs_color))
            hexColourArr = ['53','5a','30','30','00','00','00','00','00','23','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','00','5a','00','00','00','00','00','00','00','00','00','04','5a','06','02','ff','00','ff','00','04']
            intColourArr = []
            for x in hexColourArr:
                intColourArr.append(int(x, 16))
           
            intColourArr[42] = rgb[0] if rgb[0] < 255 else 255
            intColourArr[43] = rgb[1] if rgb[1] < 255 else 255
            intColourArr[44] = rgb[2] if rgb[2] < 255 else 255
            bytes = bytearray(intColourArr)
            self._socket.send(bytes)

        if self._effect is not None:
            effects_map = {
                EFFECT_SAVASANA: '535a3030000000000023000000000000000000000000000000004400000000000000000004440602ff04970400',
                EFFECT_SUNRISE: '535a3030000000000023000000000000000000000000000000004500000000000000000004450602ff01c10a00',
                EFFECT_UNICORNS: '535a3030000000000023000000000000000000000000000000004600000000000000000004460602ff049a0e00',
                EFFECT_PENSIVE: '535a3030000000000023000000000000000000000000000000004700000000000000000004470602ff04c40600',
                EFFECT_CIRCUS: '535a3030000000000023000000000000000000000000000000004800000000000000000004480602ff04810130',
                EFFECT_INSTAGRAMMER: '535a3030000000000023000000000000000000000000000000004900000000000000000004490602ff03bc0190',
                EFFECT_EIGHTIESCLUB: '535a3030000000000023000000000000000000000000000000004a000000000000000000044a0602ff049a0000',
                EFFECT_CHERRYBLOSSOMS: '535a3030000000000023000000000000000000000000000000004b000000000000000000044b0602ff04940800',
                EFFECT_COCKTAILPARADE: '535a3030000000000023000000000000000000000000000000004100000000000000000004410602ff05bd0690',
                EFFECT_CUSTOMIZED: '535a3030000000000023000000000000000000000000000000004c000000000000000000044c0602ff06940900',                
            }
            effectCommand = effects_map[self._effect]
            decode_hex = codecs.getdecoder("hex_codec")
            self._socket.send(decode_hex(effectCommand)[0])
       


    def turn_off(self, **kwargs):
        """Turn the device off."""
        self._socket.connect((IPADDR, PORTNUM))
        decode_hex = codecs.getdecoder("hex_codec")
        self._socket.send(decode_hex('535a3030000000000020000000000000000000000000000000003800000000000000000004380301ce1e')[0])
        self._isOn=False

I hope this is useful to someone else. If someone feels like making a proper stab at this component using my code, go for it, credit nice but not required!