matterbridge-dyson-robot
Version:
A Matterbridge plugin that connects Dyson robot vacuums and air treatment devices to the Matter smart home ecosystem via their local or cloud MQTT APIs.
106 lines • 4.11 kB
JavaScript
// Matterbridge plugin for Dyson robot vacuum and air treatment devices
// Copyright © 2025-2026 Alexander Thoukydides
import { PNG } from 'pngjs';
import { formatList, plural } from './utils.js';
// A persistent map or cleaned area bitmap (one octet per pixel)
export class DysonBitmapOctet {
width;
height;
buffer;
// Construct a new bitmap
constructor(width, height, buffer) {
this.width = width;
this.height = height;
this.buffer = buffer;
}
// Resample a rectangular region
resample(src, dest, filter) {
const destBuffer = Buffer.alloc(dest.width * dest.height);
const width = src.width / dest.width, height = src.height / dest.height;
for (let destY = 0; destY < dest.height; ++destY) {
for (let destX = 0; destX < dest.width; ++destX) {
const x = src.x + destX * width;
const y = src.y + destY * height;
const octets = this.pixels({ x, y, width, height });
destBuffer[destY * dest.width + destX] = filter(octets);
}
}
return new DysonBitmapOctet(dest.width, dest.height, destBuffer);
}
// Read a single pixel
read(x, y) {
const i = y * this.width + x;
return this.buffer.readUInt8(i);
}
// Write a single pixel
write(x, y, octet) {
const i = y * this.width + x;
this.buffer.writeUInt8(octet, i);
}
// Pixels within a rectangle
pixels({ x, y, width, height }) {
const startX = Math.max(Math.ceil(x), 0), endX = Math.min(Math.ceil(x + width), this.width);
const startY = Math.max(Math.ceil(y), 0), endY = Math.min(Math.ceil(y + height), this.height);
const octets = [];
for (let y = startY; y < endY; ++y) {
for (let x = startX; x < endX; ++x) {
octets.push(this.read(x, y));
}
}
return octets;
}
// Determine the bounding box of occupied pixels
boundingBox(filter) {
let minX = this.width, maxX = 0, minY = this.height, maxY = 0;
for (let y = 0; y < this.height; ++y) {
for (let x = 0; x < this.width; ++x) {
if (filter(this.read(x, y))) {
minX = Math.min(minX, x);
maxX = Math.max(maxX, x + 1);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y + 1);
}
}
}
if (maxX === 0)
return undefined;
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}
// Count the number of occupied pixels
occupied(filter) {
let count = 0;
for (let y = 0; y < this.height; ++y) {
for (let x = 0; x < this.width; ++x) {
if (filter(this.read(x, y)))
++count;
}
}
return count;
}
// Create a bitmap from a PNG, using a filter function from RGBA values
static fromPNG(png, filter) {
const { width, height, data } = PNG.sync.read(png);
const buffer = Buffer.alloc(width * height);
for (let i = 0; i < buffer.length; ++i) {
const rgba = data.readUInt32BE(i << 2);
const rawOctet = filter(rgba);
buffer[i] = Math.min(Math.max(Math.round(rawOctet), 0), 255);
}
return new DysonBitmapOctet(width, height, buffer);
}
// Create a bitmap from a PNG, using a map of RGBA values
static fromPNGMapped(png, map) {
const unmappedRGBA = new Set();
const filter = (rgba) => {
if (!map.has(rgba))
unmappedRGBA.add(`#${rgba.toString(16).toUpperCase().padStart(8, '0')}`);
return map.get(rgba) ?? 0;
};
const bitmap = DysonBitmapOctet.fromPNG(png, filter);
if (unmappedRGBA.size) {
throw new Error(`${plural(unmappedRGBA.size, 'unmapped bitmap colour')}: ${formatList([...unmappedRGBA.values()])}`);
}
return bitmap;
}
}
//# sourceMappingURL=dyson-bitmap-octet.js.map