d03n3rfr1tz3 / hass-divoom

Divoom Integration for Home Assistant
MIT License
99 stars 9 forks source link

Ditoo is compatible ! Reverse engineered code to toggle Keyboard's light and more. #17

Closed Write closed 5 months ago

Write commented 10 months ago

Just for anyone passing by, Ditoo is compatible with almost 100% of commands.

OFC Keyboard lights controls is not available, since it wasn't reverse engineered as it was not something compatible either with the Timebox or Pixoo.

However, I managed to reverse engineer it, so you can actually toggle the lights (there's no turn on or turn off, only toggle, it seems) and pass to the next / previous effects.

Also, I've implemented a set_brightness and set_volume functions and a way to toggle between your three custom channels, basically show_design, but you can choose between the three. I was also lazy, so show_design It's actually implemented in 3 different functions.

I had no previous knowledge of hex/byte conversion, my code is ugly AF and I ain't ever going to make a PR with such ugly code, but I wanted to share it anyway, as it may hopefully help someone !

pixoo.py file : https://bin.socialspill.com/iwanukis.py

pixoo.py ```python """Provides class Pixoo that encapsulates the Pixoo communication.""" import logging, math, itertools, select, socket, time from PIL import Image def is_number(s): try: float(s) return True except ValueError: return False def clamp(value, minimum=0, maximum=255): if value > maximum: return maximum if value < minimum: return minimum return value class Pixoo: """Class Pixoo encapsulates the Pixoo communication.""" COMMANDS = { "set date time": 0x18, "set image": 0x44, "set view": 0x45, "set brightness": 0x74, "set volume": 0x08, "get settings": 0x46, "set animation frame": 0x49, "set keyboard": 0x23, "set design": 0xbd } logger = None socket = None socket_errno = 0 message_buf = [] host = None port = 1 def __init__(self, host=None, port=1, logger=None): self.type = "Pixoo" self.size = 16 self.host = host self.port = port if logger is None: logger = logging.getLogger(self.type) self.logger = logger def __exit__(self, type, value, traceback): self.close() def connect(self): """Open a connection to the Pixoo.""" self.socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) try: self.socket.connect((self.host, self.port)) self.socket.setblocking(0) self.socket_errno = 0 except socket.error as error: self.socket_errno = error.errno def close(self): """Closes the connection to the Pixoo.""" try: self.socket.shutdown(socket.SHUT_RDWR) except: pass self.socket.close() self.socket = None def reconnect(self): """Reconnects the connection to the Pixoo, if needed.""" try: self.send_ping() except socket.error as error: self.socket_errno = error.errno retries = 1 while self.socket_errno > 0 and retries <= 5: self.logger.warning("Pixoo connection lost (errno = {0}). Trying to reconnect for the {1} time.".format(self.socket_errno, retries)) if retries > 1: time.sleep(1 * retries) if not self.socket is None: self.close() self.connect() retries += 1 def receive(self, num_bytes=1024): """Receive n bytes of data from the Pixoo and put it in the input buffer. Returns the number of bytes received.""" ready = select.select([self.socket], [], [], 0.1) if ready[0]: data = self.socket.recv(num_bytes) self.message_buf += data return len(data) else: return 0 def send_raw(self, data): """Send raw data to the Pixoo.""" try: return self.socket.send(data) except socket.error as error: self.socket_errno = error.errno raise def send_payload(self, payload): """Send raw payload to the Pixoo. (Will be escaped, checksumed and messaged between 0x01 and 0x02.""" self.logger.warning("send_payload: {0}".format(payload)) msg = self.make_message(payload) try: self.logger.warning("whole payload: {0}".format(msg)) self.logger.warning("byte payload: {0}".format(bytes(msg))) self.logger.warning("byte payload: {0}".format(bytes(msg).hex())) return self.socket.send(bytes(msg)) except socket.error as error: self.socket_errno = error.errno raise def send_command(self, command, args=None): """Send command with optional arguments""" if args is None: args = [] if isinstance(command, str): command = self.COMMANDS[command] length = len(args)+3 payload = [] payload += length.to_bytes(2, byteorder='little') payload += [command] payload += args self.send_payload(payload) def drop_message_buffer(self): """Drop all dat currently in the message buffer,""" self.message_buf = [] def checksum(self, payload): """Compute the payload checksum. Returned as list with LSM, MSB""" length = sum(payload) csum = [] csum += length.to_bytes(2, byteorder='little') return csum def chunks(self, lst, n): """Yield successive n-sized chunks from lst.""" for i in range(0, len(lst), n): yield lst[i:i + n] def escape_payload(self, payload): """Escaping is not needed anymore as some smarter guys found out""" if self.type == "Pixoo": return payload """Escape the payload. It is not allowed to have occurrences of the codes 0x01, 0x02 and 0x03. They mut be escaped by a leading 0x03 followed by 0x04, 0x05 or 0x06 respectively""" escpayload = [] for payload_data in payload: escpayload += \ [0x03, payload_data + 0x03] if payload_data in range(0x01, 0x04) else [payload_data] return escpayload def make_message(self, payload): """Make a complete message from the paload data. Add leading 0x01 and trailing check sum and 0x02 and escape the payload""" cs_payload = payload + self.checksum(payload) escaped_payload = self.escape_payload(cs_payload) return [0x01] + escaped_payload + [0x02] def convert_color(self, color): return color[0].to_bytes(1, byteorder='big') + color[1].to_bytes(1, byteorder='big') + color[2].to_bytes(1, byteorder='big') def make_frame(self, frame): length = len(frame)+3 header = [0xAA] header += length.to_bytes(2, byteorder='little') return [header + frame, length] def make_framepart(self, lsum, index, framePart): header = [] header += lsum.to_bytes(2, byteorder='little') header += [index] return header + framePart def process_image(self, image): frames = [] with Image.open(image) as img: picture_frames = [] palette = img.getpalette() try: while True: try: if img.mode in ("L", "LA", "P", "PA") and not img.getpalette(): img.putpalette(palette) except ValueError as error: self.logger.warning("Pixoo encountered an error while trying to put palette into GIF frames. {0}".format(error)) duration = img.info['duration'] new_frame = Image.new('RGBA', img.size) new_frame.paste(img, (0, 0), img.convert('RGBA')) picture_frames.append([new_frame, duration]) img.seek(img.tell() + 1) except EOFError: pass for pair in picture_frames: picture_frame = pair[0] time = pair[1] colors = [] pixels = [None]*self.size*self.size if time is None: time = 0 for pos in itertools.product(range(self.size), range(self.size)): y, x = pos r, g, b, a = picture_frame.getpixel((x, y)) if [r, g, b] not in colors: colors.append([r, g, b]) color_index = colors.index([r, g, b]) pixels[x + self.size * y] = color_index colorCount = len(colors) if colorCount >= 256: colorCount = 0 timeCode = [0x00, 0x00] if len(picture_frames) > 1: timeCode = time.to_bytes(2, byteorder='little') frame = [] frame += timeCode frame += [0x00] frame += colorCount.to_bytes(1, byteorder='big') for color in colors: frame += self.convert_color(color) frame += self.process_pixels(pixels, colors) frames.append(frame) result = [] for frame in frames: result.append(self.make_frame(frame)) return result def process_pixels(self, pixels, colors): """Correctly transform each pixel information based on https://github.com/RomRider/node-divoom-timebox-evo/blob/master/PROTOCOL.md#pixel-string-pixel_data """ bitsPerPixel = math.ceil(math.log(len(colors)) / math.log(2)) if bitsPerPixel == 0: bitsPerPixel = 1 pixelString = "" for pixel in pixels: pixelBits = "{0:b}".format(pixel).zfill(8) pixelString += pixelBits[::-1][:bitsPerPixel:] chunkSize = 8 pixelChunks = [] for i in range(0, len(pixelString), chunkSize): pixelChunks += [pixelString[i:i+chunkSize]] result = [] for pixel in pixelChunks: result += [int(pixel[::-1], 2)] return result def send_ping(self): """Send a ping (actually it's requesting settings) to the Pixoo to check connectivity""" self.send_command("get settings") def show_clock(self, clock=1, weather=1, temp=1, calendar=1, color=None): """Show clock on the Pixoo in the color""" args = [0x00, 0x01] if clock >= 0: args += clock.to_bytes(1, byteorder='big') args += [0x01] else: args += [0x01, 0x00] args += weather.to_bytes(1, byteorder='big') args += temp.to_bytes(1, byteorder='big') args += calendar.to_bytes(1, byteorder='big') if not color is None: args += self.convert_color(color) self.send_command("set view", args) def send_raw_payload(self, payload): self.send_payload(payload) def set_brightness(self, number): self.logger.warning("Nombre is (number = {0})".format(number)) if (is_number(number)): number = int(number) else: number = 0 args = number.to_bytes(1, byteorder='big') self.send_command("set brightness", args) def set_volume(self, number): self.logger.warning("Nombre is (number = {0})".format(number)) if (is_number(number)): number = int(number) if (number >= 16): number = 16 elif (number <= 0): number = 0 else: number = 0 args = number.to_bytes(1, byteorder='big') self.send_command("set volume", args) def show_light(self, color, brightness, power): """Show light on the Pixoo in the color""" args = [0x01] if color is None: args += [0xFF, 0xFF, 0xFF] args += brightness.to_bytes(1, byteorder='big') args += [0x01] else: args += self.convert_color(color) args += brightness.to_bytes(1, byteorder='big') args += [0x00] args += [0x01 if power == True else 0x00, 0x00, 0x00, 0x00] self.send_command("set view", args) def show_cloud(self): """Show cloud channel on the Pixoo""" args = [0x02] self.send_command("set view", args) def show_effects(self, number): """Show effects on the Pixoo""" args = [0x03] args += number.to_bytes(1, byteorder='big') self.send_command("set view", args) def show_visualization(self, number): """Show visualization on the Pixoo""" args = [0x04] args += number.to_bytes(1, byteorder='big') self.send_command("set view", args) def keyboard_toggle_light(self): """Show visualization on the Pixoo""" args = [0x02] raw_value = 29 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set keyboard", args) def keyboard_previous_effect(self): """Show visualization on the Pixoo""" args = [0x00] raw_value = 27 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set keyboard", args) def keyboard_next_effect(self): """Show visualization on the Pixoo""" args = [0x01] raw_value = 28 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set keyboard", args) def show_design(self): """Show design on the Pixoo""" args = [0x05] self.send_command("set view", args) def show_design_1(self): args = [0x17] raw_value = 0 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set design", args) def show_design_2(self): args = [0x17] raw_value = 1 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set design", args) def show_design_3(self): args = [0x17] raw_value = 2 args += raw_value.to_bytes(1, byteorder='big') self.send_command("set design", args) def show_scoreboard(self, blue, red): """Show scoreboard on the Pixoo with specific score""" args = [0x06, 0x00] args += red.to_bytes(2, byteorder='little') args += blue.to_bytes(2, byteorder='little') self.send_command("set view", args) def show_image(self, file): """Show image or animation on the Pixoo""" frames = self.process_image(file) framesCount = len(frames) if framesCount > 1: """Sending as Animation""" frameParts = [] framePartsSize = 0 for pair in frames: frameParts += pair[0] framePartsSize += pair[1] index = 0 for framePart in self.chunks(frameParts, 200): frame = self.make_framepart(framePartsSize, index, framePart) self.send_command("set animation frame", frame) index += 1 elif framesCount == 1: """Sending as Image""" pair = frames[0] frame = [0x00, 0x0A, 0x0A, 0x04] + pair[0] self.send_command("set image", frame) def clear_input_buffer(self): """Read all input from Pixoo and remove from buffer. """ while self.receive() > 0: self.drop_message_buffer() def clear_input_buffer_quick(self): """Quickly read most input from Pixoo and remove from buffer. """ while self.receive(512) == 512: self.drop_message_buffer() ```

notify.py file : https://bin.socialspill.com/refeluko.py

notify.py ```python """Switching states and sending images or animations to a divoom device.""" import logging, os import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService ) from homeassistant.const import CONF_MAC _LOGGER = logging.getLogger(__name__) CONF_DEVICE_TYPE = 'device_type' CONF_MEDIA_DIR = 'media_directory' PARAM_MODE = 'mode' PARAM_BRIGHTNESS = 'brightness' PARAM_COLOR = 'color' PARAM_NUMBER = 'number' PARAM_CLOCK = 'clock' PARAM_WEATHER = 'weather' PARAM_TEMP = 'temp' PARAM_CALENDAR = 'calendar' PARAM_PAYLOAD = 'payload' PARAM_PLAYER1 = 'player1' PARAM_PLAYER2 = 'player2' PARAM_FILE = 'file' PARAM_RAW = 'raw' VALID_MODES = {'on', 'off', 'clock', 'brightness', 'volume', 'payload', 'keyboard', 'keyboard_next', 'keyboard_previous', 'light', 'effects', 'cloud', 'visualization', 'scoreboard', 'design', 'design_1', 'design_2', 'design_3', 'image'} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, vol.Required(CONF_DEVICE_TYPE): cv.string, vol.Required(CONF_MEDIA_DIR, default="pixelart"): cv.string, }) def get_service(hass, config, discovery_info=None): """Get the Divoom notification service.""" mac = config[CONF_MAC] device_type = config[CONF_DEVICE_TYPE] media_directory = hass.config.path(config[CONF_MEDIA_DIR]) return DivoomNotificationService(mac, device_type, media_directory) class DivoomNotificationService(BaseNotificationService): """Implement the notification service for Divoom.""" def __init__(self, mac, device_type, media_directory): if device_type == 'pixoo': from .devices.pixoo import Pixoo self._mac = mac self._media_directory = media_directory self._device = Pixoo(host=mac, logger=_LOGGER) self._device.connect() if self._device is None: _LOGGER.error("device_type {0} does not exist, divoom will not work".format(media_directory)) elif not os.path.isdir(media_directory): _LOGGER.error("media_directory {0} does not exist, divoom may not work properly".format(media_directory)) def send_message(self, message="", **kwargs): if kwargs.get(ATTR_DATA) is None: _LOGGER.error("Service call needs a message type") return False self._device.reconnect() data = kwargs.get(ATTR_DATA) mode = data.get(PARAM_MODE) if mode == False or mode == 'off': self._device.show_light(color=[0x01, 0x01, 0x01], brightness=0, power=False) elif mode == 'on': self._device.show_light(color=[0x01, 0x01, 0x01], brightness=100, power=True) elif mode == "payload": payload = data.get(PARAM_PAYLOAD) self._device.send_raw_payload(payload=payload) elif mode == "clock": clock = data.get(PARAM_CLOCK) weather = data.get(PARAM_WEATHER) temp = data.get(PARAM_TEMP) calendar = data.get(PARAM_CALENDAR) color = data.get(PARAM_COLOR) self._device.show_clock(clock=clock, weather=weather, temp=temp, calendar=calendar, color=color) elif mode == "brightness": number = data.get(PARAM_NUMBER) self._device.set_brightness(number=number) elif mode == "volume": number = data.get(PARAM_NUMBER) self._device.set_volume(number=number) elif mode == "light": brightness = data.get(PARAM_BRIGHTNESS) color = data.get(PARAM_COLOR) self._device.show_light(color=color, brightness=brightness, power=True) elif mode == "effects": number = data.get(PARAM_NUMBER) self._device.show_effects(number=number) elif mode == "cloud": self._device.show_cloud() elif mode == "visualization": number = data.get(PARAM_NUMBER) self._device.show_visualization(number=number) elif mode == "scoreboard": player1 = data.get(PARAM_PLAYER1) player2 = data.get(PARAM_PLAYER2) self._device.show_scoreboard(blue=player1, red=player2) elif mode == "keyboard": self._device.keyboard_toggle_light() elif mode == "keyboard_next": self._device.keyboard_next_effect() elif mode == "keyboard_previous": self._device.keyboard_previous_effect() elif mode == "design": self._device.show_design() elif mode == "design_1": self._device.show_design_1() elif mode == "design_2": self._device.show_design_2() elif mode == "design_3": self._device.show_design_3() elif mode == "image": image_file = data.get(PARAM_FILE) image_path = os.path.join(self._media_directory, image_file) self._device.show_image(image_path) else: _LOGGER.error("Invalid mode '{0}', must be one of 'on', 'off', 'clock', 'brightness', 'light', 'weather', 'temp', 'calendar', 'effects', 'visualization', 'scoreboard', 'design', 'keyboard', 'image'".format(mode)) return False return True ```

Bluetooth connection trough HA OS If you have HAOS, there's only the command bluetoothctl available, you can just type bluetoothctl scan on, wait for your device to be shown, and connect to it with bluetoothctl connect MAC_ADDRESS, and you'll hear a sound on your Ditoo when connected. And it does seem to reconnect just fine after each HA OS reboot. May I suggest adding a periodic "ping" notification to the Ditoo, through an automation every 5 minutes or so to the devices, so it stays connected.

Also, it may happen the device stop responding after one day or two. Haven't found a way around that right now, you have to reboot the device by long pressing the power button. Unfortunately the device happens to lose connection from time to time and do a HUGE sound that is not controlled through the volume control. I tried to open the Ditoo to disconnect the speakers to no avail.

d03n3rfr1tz3 commented 6 months ago

I added quite a few new modes, but unfortunately I cannot open/download your linked files anymore. Could you give me a short hint/documentation for the command to toggle the keyboard lights?

Write commented 6 months ago

I added quite a few new modes, but unfortunately I cannot open/download your linked files anymore. Could you give me a short hint/documentation for the command to toggle the keyboard lights?

I'm so sorry, seems my VM had an issue. Should be available now :) !

PS : The maddening thing is that I can't get the current state of the keyboard to be able to toggle it only in specific case (since there's no off / on). Maybe if I were able to retrieve the conf ? idk

Write commented 6 months ago

Also show_design_1 and show_design_2, show_design_3, doesn't "save" the design (it doesn't act the same as show_design :( ) , at reboot it'll go back to the first one IIRC. If I do it in the app it works fine after a reboot, soo It's just certainly missing some params, but wanted to let you know.

d03n3rfr1tz3 commented 6 months ago

Nice, thank you! I'll add it (maybe a little bit refactored) into the component. Sadly I can't test it myself currently, but it' still a nice start. I'll try to challenge my googling skills. Maybe I can dig up some more details regarding additional parameters and we can refine the new modes. We'll see 🙂

Write commented 6 months ago

Nice, thank you! I'll add it (maybe a little bit refactored) into the component. Sadly I can't test it myself currently, but it' still a nice start. I'll try to challenge my googling skills. Maybe I can dig up some more details regarding additional parameters and we can refine the new modes. We'll see 🙂

Well if you add a way to actually show the "get settings" command value, I can always see what my device returns when keyboard is turned on or not ;)

Write commented 6 months ago

Added some info in my first post related to buetooth. Also added the file in a spoiler tag for archival purpose. Didn't know about spoiler on github back then.

d03n3rfr1tz3 commented 6 months ago

I've added your modes and some other things in a new branch here: https://github.com/d03n3rfr1tz3/hass-divoom/tree/feature/ditoo-modes

Can you have a look if it already works (obviously I can't test it myself)? If yes, I would merge it into main.

You also might activate debug mode for this component (if not already done) via your configuration.yaml, because I added some debug logging for payload (input and output). that way you can have a closer look into the answer of the keyboard-commands and maybe we can improve the toggling command. afaik the "get view" command (0x46) does not have that information or at least the known response does not mention it: http://docin.divoom-gz.com/web/#/5/288

Write commented 6 months ago

Thanks, I'll try it when I can, no promise on when though.

d03n3rfr1tz3 commented 6 months ago

FYI: I've ordered a Ditoo for myself, so I might be able to test and refine it myself. Maybe I can somehow improve the keyboard toggling and hopefully I can even expand on the audio commands more. We'll see.

Also in that branch there are a lot of new modes available now. What initially started with some small additions, turned out to be a lot bigger then expected. \ I found out, that even my old Pixoo does support far more then I thought. It can run the tools (noise meter, timer, countdown) as well as a few games. 3 games are in the app, but theoretically I can start 5, but the additional 2 games dont seem to work correctly. at least I got the codes from these (and other) commands and added them as additional modes or parameters. I've tested everything that my Pixoo supports and can hopefully test the rest with my new Ditoo in a few days.

Write commented 6 months ago

FYI: I've ordered a Ditoo for myself, so I might be able to test and refine it myself. Maybe I can somehow improve the keyboard toggling and hopefully I can even expand on the audio commands more. We'll see.

Also in that branch there are a lot of new modes available now. What initially started with some small additions, turned out to be a lot bigger then expected. \

I found out, that even my old Pixoo does support far more then I thought. It can run the tools (noise meter, timer, countdown) as well as a few games. 3 games are in the app, but theoretically I can start 5, but the additional 2 games dont seem to work correctly. at least I got the codes from these (and other) commands and added them as additional modes or parameters. I've tested everything that my Pixoo supports and can hopefully test the rest with my new Ditoo in a few days.

Well this indeed awesome ! I don't think I will be much help if you have your own Ditoo. You will be able to debug it way better than going back and forth with me.

This could be the start of a fully fledged HACS plugin.

That is, I'll link to you the API I used to download most of Divoom animations from their official app (because it's really impossible to find it elsewhere) for my devices, and then the CLI command I used them to properly convert them to 16x16 so I can upload them to my HA and be playable on my Ditoo. That would help a lot for future documentation.

Last thing I really wonder is if the Bluetooth connection could be more stable, even if it seems to already be okay, the device randomly stopping responding after 2 days or so is annoying. I don't have much hope as for removing the sound it does on disconnecting / reconnecting a Bluetooth device but who knows.

Write commented 6 months ago

Here's the API I use to download animations from Divoom Official servers : https://github.com/redphx/apixoo

Example script to download 30 pages of Gadget Category but only gifs that are 16X16. To download from account you follow just change it to GalleryCategory.FOLLOW

#!/opt/homebrew/bin/python3
from apixoo import APIxoo, GalleryCategory, GalleryDimension

# Divoom account
EMAIL = 'your divoom email'
MD5_PASSWORD = 'your password md5 hashed'

# Also accept password string with "password='password'"
api = APIxoo(EMAIL, md5_password=MD5_PASSWORD)
status = api.log_in()
if not status:
    print('Login error!')
else:

    for i in range(1, 30):
        print(f"Traitement de la page : {i}")
        files = api.get_category_files(
            GalleryCategory.GADGET,
            dimension=GalleryDimension.W16H16,
            page=i,
            per_page=20,
        )

        for info in files:
            print(info)
            pixel_bean = api.download(info)
            if pixel_bean:
                pixel_bean.save_to_gif(f'{info.gallery_id}-page_{i}.gif', scale=5)

Then to resize them to a proper sized gif that is uploadable trough hass-divoom I have this batch script :

#!/bin/bash

SUB="16x16"

for d in *.gif ; do
  echo "$d"
  if [[ "$d" =~ .*"$SUB".* ]]; then
    echo $d " is ignored"
  else
    gifsicle --resize 16x16 -i $d -o $d-16x16.gif
  fi
done

The render on the Ditoo seems great almost all the time.

d03n3rfr1tz3 commented 6 months ago

Interesting... that might actually be worth a mention in the Readme, to give people another way of preparing the GIFs.

FYI: the Ditoo I ordered arrived today and I already have a few ideas on how to improve my component. Problem is, I also recognized (and partially remembered), that multiple BT classic connections are not supported and hass-divoom basically has a perma-connection to the Divoom device configured. If thats the only HA integration using BT classic connections (scanning is fine tho), then its not a problem. But trying to manage two Divoom devices through HA is a bit tricky... besides the fact, that my Ditoo did not accept any commands from my component yet, but I hope its just a pairing or port/channel problem.

Long story short: I'm working on supporting multiple devices. That obviously only works through connection-switching. After that, I will have a closer look at the Ditoo specific commands.

I've just got both Divoom devices to work without any connection-switching. All it needed was a configuration parameter for using a different port. So now the fun can begin 😎

Write commented 6 months ago

Thanks a lot for your work ! Eager to learn your findings

d03n3rfr1tz3 commented 6 months ago

Good news is, I could refine some commands and found out, that the scoreboard has a different command. On my Pixoo you can access it through "set view" with number 6. But on the Ditoo "set view" with number 6 is the lyrics channel and the scoreboard is on "set tool" with number 1. Kinda strange IMO.

Bad news is, the keyboard commands don't have any feedback (no payload comes back from the Ditoo) and also don't change their values. I couldn't find any state information about it, wether it's on/off or the current effect. Therefore we have to live with the current state and hope for some changes or a lucky shot on something we might have missed.

As everything seems to work with the Ditoo, I'll probably merge the PR soon. Just letting it run maybe one or two days in my Home Assistant if daily business also works basically.

Write commented 6 months ago

Thanks, that's already awesome :)

Write commented 6 months ago

I can't keep my Ditoo properly connected since a while...

Lately HAOS updated really made a mess with Bluetooth I guess because I haven't changed a thing.

Is there any bluetoothctl command you may suggest enhancing the connexion ? It just keeps disconnecting / reconnecting the Ditoo (I know because of the sound).

After some test -> I just send a command to the ditoo and it disconnect after ~15s. It wasn't doing that like few weeks ago. Now I can't make it so HA keep connected ... damn so annoying.

So everytime I issue any command to the ditoo it has to make the connect and disconnected sound ._.

Hmm.. I can't even connect anymore to my Ditoo trough bluetoothctl command. But it does see the device 🤷

Wondering if the co/disconnect issue is just because HA can't use the "main" bluetooth device, it use the Bluetooth Proxy to issue the command and disconnect immediately (for power saving reason, proxy won't let an active connexion I guess).

d03n3rfr1tz3 commented 6 months ago

I had a similar experience, but I'm on a HA version from 2023. Problem in my case was, that I was also using the Ditoo with my phone sometimes and also that my HA is connected to my Pixoo and my Ditoo at the same time. The Pixoo never had a problem with that, but the Ditoo is a bit more finicky in that case.

That's why I added the 'disconnect' mode (and the 'connect' mode, if someone actually needs that). In my automations it just does a few commands and then at the end just disconnects. It works very well and does not interfere with my phone usage.

Another problem I recognized is, that the range of my Raspberry Pi bluetooth is quite low. Placing the Ditoo on the other side of my living room already is too far away for a stable connection. Not only is the bluetooth controller on the Raspberry Pi not strong enough, there are also typical interferences (like USB3, yes I'm serious) that can make things far worse.

If it just disconnects in your case every now and then, but is otherwise quick and stable, it might be just another integration using bluetooth classic. the older bluetooth classic (which is what Divoom uses) has more restrictions then BLE. So maybe also check for recent changes in that regard. Btw, afaik the bluetooth proxies for HA only support BLE, but I might be wrong. That would be a reason, why it does not work with that.

Write commented 6 months ago

Well honestly I don't know. I usually just connected to the device with bluetoothctl, and then issued notify commands. Device would keep the BT connection with HA for at least 2 days after which it would need a reboot to accept BT commands again.

Weirdly, now, I just cannot connect with Bluetoothctl, but when issuing notify commands trough HACS it does connect , issue the command, and disconnect. Which wouldn't even be a problem if there wasn't this HUGE sound when connecting.

Also I liked having connection kept because the connexion was faster, but that wouldn't be a requirement (unless my neighbor figures out he can connect to it when no device is connected lol)

I tried to ripe appart the Ditoo to disconnect the Speaker but god I can't manage to move it even a little and I don't want it to be scratched every where.

Write commented 5 months ago

EDIT : Ok I feel kinda dumb, you added HACS without me noticing it. Guess it's time for me to finally upgrade and move my old code / automations. And I'll try to see if reloading integration make the bug go away.

Hello, the BT issue somewhat improved for me with new HA release.

I know can press "Restart HA", which won't reboot the whole system but just HA and the BT connection is now KEPT ! That wasn't the case before. So the Ditoo doesn't make any sound on restart, which is awesome.

However, the same issue remains : after few hours, the Ditoo won't respond to any command. The notify command pass as if the BT packet was sent successfully. Unlike when the BT connection is dropped or / Ditoo is OFF which would throw an error.

If I restart HA, the issue is solved, and the Ditoo does respond again, without me having to disconnect / reconnect BT.

I would love to be able to reload this integration, but I can't.

My workaround right now I've made so HA restart itself every 6 hours with

  - service: homeassistant.restart
    data: {}

and it does work flawlessly.

If you have any idea on how I may be able to fix it, would love to hear it.

Thanks a lot :)

Write commented 5 months ago

After migrating to the official version of this plugin trough HACS, I can confirm that everything works fine. Bluetooth's connection is rock solid and the device accept commands without me restarting HA.

So thanks a lot @d03n3rfr1tz3, love my Divoom and can finally use it to its full potential ;)

d03n3rfr1tz3 commented 4 months ago

Thanks again for the nice words! And also thanks for you contribution regarding the Ditoo.

Just a small update: If you ever plan on moving your Ditoo further from your HA, you might want to keep an eye on the ESP32 firmware I'm currently working on. It works as a Proxy (via TCP) for my HA integration and even supports auto-discovery. It's partially untested currently and heavily WIP, but the important parts already work quite well. I'm also planning on adding MQTT, so that it could be used standalone, without my HA integration.

The ESP32 firmware (PlatformIO Project): https://github.com/d03n3rfr1tz3/esp32-divoom The branch on HASS-Divoom with support for it: https://github.com/d03n3rfr1tz3/hass-divoom/tree/feature/esp32-divoom

Write commented 4 months ago

Thanks again for the nice words! And also thanks for you contribution regarding the Ditoo.

Just a small update: If you ever plan on moving your Ditoo further from your HA, you might want to keep an eye on the ESP32 firmware I'm currently working on. It works as a Proxy (via TCP) for my HA integration and even supports auto-discovery. It's partially untested currently and heavily WIP, but the important parts already work quite well. I'm also planning on adding MQTT, so that it could be used standalone, without my HA integration.

The ESP32 firmware (PlatformIO Project): https://github.com/d03n3rfr1tz3/esp32-divoom

The branch on HASS-Divoom with support for it: https://github.com/d03n3rfr1tz3/hass-divoom/tree/feature/esp32-divoom

That's quite awesome. I have one or two esp32 laying around but I wouldn't really have use case so I'll keep it like that for now.

But now, with this plug-in and your further dev w/ esp32, there's a good chance I consider buying Divoom devices again.

I'd say my true concern now is just for the battery, as it's plugged 24.7 it'll die rather quickly. And I'm afraid it won't work if battery die.

d03n3rfr1tz3 commented 4 months ago

Don't worry about the battery that much. I have my Pixoo nearly 4 years now, constantly connected... and it's still working. If that ever changes, I probably just open it and replace the LiPo battery somehow. The case doesnt look like it would be impossible.

Of course it can reduce the capacity of a LiPo battery, if you have it constantly charging especially near 100%, but it's not as bad as some make it. Reduced capacity doesnt mean it's dead. It heavily depends on the charging circuit. A bad charger can damage a LiPo far more, by for example just charging a bit more then 100% (more then 4.2V) or by charging it in temperatures near or below zero. What is even worse is discharging it below a certain point, because over-discharging and over-charging damage those batteries the most.... to the point, that it actually can be dead. And even then 'dead' just means, that a good circuit will not charge it anymore, because a over-discharged LiPo, that gets then charged, can turn into fireworks quite fast.