UNPKG

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
// 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