AdapterRequests icon indicating copy to clipboard operation
AdapterRequests copied to clipboard

Neakasa M1

Open luckyheiko opened this issue 2 months ago • 1 comments

Würde mich freuen wenn man einen Adapter für den Neakasa M1 in den iOBroker entwickeln könnte.

hab leider nur Infos für HA gefunden https://github.com/timniklas/hass-neakasa

hier die HP https://neakasa.de/products/neakasa-m1-cat-litter-box

Vielen Dank schon mal von mir und meinen Katzen

What kind of device or service would you like to see an adapter for? Add name and company of the device, including links to the device and any additional information[...]

Is the device connected to the internet or only available on a local network?

Is an official App or Website available? If yes, please add links

Is an official API including documentation available? If yes, please add links and information[...]

Are other libraries for an integration available? Ideally in JavaScript/npm, but also other programming languages are interesting, add links please

Is this device already integrated in other Smart Home systems? Add links please

Is this device already integrated in homebridge? Might the ham adapter in combination with the homebridge plugin be sufficient? Please try it and add results

Additional context Add any other context or screenshots about the feature request here. If the topic was discussed on the ioBroker forum, please include the link too.

After you create the issue, please vote for yourself in the first post of the issue using the "+1"/"Thumbs up" button

luckyheiko avatar Oct 23 '25 16:10 luckyheiko

ich habe leider gar keine ahnung davon. hoffe aber chatGPT hat mich verstanden ;)

kann man damit was anfangen?

`/**

  • ioBroker Neakasa Adapter
  • Adapter skeleton implementing Neakasa Cloud API connectivity
  • Extracted structure and partial API flow based on hass-neakasa integration */

const utils = require('@iobroker/adapter-core'); const axios = require('axios');

class Neakasa extends utils.Adapter { constructor(options) { super({ ...options, name: 'neakasa' }); this.devices = {}; this.pollInterval = null; this.token = null; this.baseUrl = 'https://api.neakasa.com'; }

async onReady() {
    this.log.info('Starting Neakasa adapter...');
    const username = this.config.username;
    const password = this.config.password;
    this.baseUrl = this.config.cloud_base_url || this.baseUrl;
    this.pollInterval = this.config.poll_interval || 60;

    try {
        await this.login(username, password);
        await this.loadDevices();
        await this.pollAll();

        this.pollTimer = this.setInterval(() => this.pollAll(), this.pollInterval * 1000);
    } catch (err) {
        this.log.error('Initialization failed: ' + err);
    }
}

async login(username, password) {
    try {
        const res = await axios.post(`${this.baseUrl}/v1/login`, { username, password });
        if (res.data && res.data.token) {
            this.token = res.data.token;
            this.log.info('Successfully authenticated with Neakasa cloud');
        } else {
            throw new Error('No token received');
        }
    } catch (err) {
        this.log.error('Login failed: ' + err);
        throw err;
    }
}

async loadDevices() {
    try {
        const res = await axios.get(`${this.baseUrl}/v1/devices`, { headers: { Authorization: `Bearer ${this.token}` } });
        if (!res.data.devices) throw new Error('No devices found');

        for (const dev of res.data.devices) {
            const id = dev.id || dev.deviceId;
            this.devices[id] = dev;

            await this.setObjectNotExistsAsync(`devices.${id}`, { type: 'channel', common: { name: dev.name }, native: {} });
            await this.createDeviceObjects(id, dev);
        }
    } catch (err) {
        this.log.error('Loading devices failed: ' + err);
    }
}

async createDeviceObjects(id, dev) {
    const sensors = [
        'litter_level', 'wifi_rssi', 'last_stay_time', 'last_usage', 'device_status', 'cat_litter_state', 'bin_state'
    ];
    const switches = [
        'kitten_mode', 'child_lock', 'automatic_cover', 'automatic_leveling', 'silent_mode', 'automatic_recovery', 'unstoppable_cycle', 'auto_clean'
    ];
    const buttons = ['clean', 'level'];

    for (const s of sensors) {
        await this.setObjectNotExistsAsync(`devices.${id}.${s}`, { type: 'state', common: { name: s, type: 'string', role: 'value', read: true, write: false }, native: {} });
    }
    await this.setObjectNotExistsAsync(`devices.${id}.garbage_can_full`, { type: 'state', common: { name: 'garbage_can_full', type: 'boolean', role: 'indicator', read: true, write: false }, native: {} });

    for (const s of switches) {
        await this.setObjectNotExistsAsync(`devices.${id}.${s}`, { type: 'state', common: { name: s, type: 'boolean', role: 'switch', read: true, write: true }, native: {} });
    }
    for (const b of buttons) {
        await this.setObjectNotExistsAsync(`devices.${id}.${b}`, { type: 'state', common: { name: b, type: 'boolean', role: 'button', read: false, write: true }, native: {} });
    }

    await this.subscribeStatesAsync(`devices.${id}.*`);
}

async pollAll() {
    for (const id of Object.keys(this.devices)) {
        try {
            const res = await axios.get(`${this.baseUrl}/v1/devices/${id}/status`, { headers: { Authorization: `Bearer ${this.token}` } });
            const data = res.data;
            await this.setStateAsync(`devices.${id}.raw`, JSON.stringify(data), true);

            const fields = {
                litter_level: this._coalesce(data.litterLevel, data.litter_level, data.litterPercent),
                wifi_rssi: this._coalesce(data.wifiRssi, data.signal),
                last_stay_time: data.lastStayTime,
                last_usage: data.lastUsage,
                device_status: data.deviceStatus || data.status,
                cat_litter_state: data.catLitterState,
                bin_state: data.binState
            };

            for (const [k, v] of Object.entries(fields)) {
                if (v !== undefined) await this.setStateAsync(`devices.${id}.${k}`, { val: v, ack: true });
            }

            await this.setStateAsync(`devices.${id}.garbage_can_full`, { val: data.binState === 'full', ack: true });

            if (data.cats) {
                for (const cat of data.cats) {
                    const cname = this._slugify(cat.name || 'cat');
                    await this.setObjectNotExistsAsync(`devices.${id}.cat_${cname}_kg`, { type: 'state', common: { name: `cat_${cname}_kg`, type: 'number', role: 'value.weight', read: true, write: false }, native: {} });
                    await this.setStateAsync(`devices.${id}.cat_${cname}_kg`, { val: cat.weight, ack: true });
                }
            }

            if (data.switches) {
                for (const sw of Object.keys(data.switches)) {
                    const val = !!data.switches[sw];
                    if (await this.getObjectAsync(`devices.${id}.${sw}`)) {
                        await this.setStateAsync(`devices.${id}.${sw}`, { val, ack: true });
                    }
                }
            }

        } catch (err) {
            this.log.error(`Polling device ${id} failed: ${err}`);
        }
    }
}

async onStateChange(id, state) {
    if (!state || state.ack) return;

    const [_, devId, stateName] = id.split('.').slice(-3);
    if (!this.devices[devId]) return;

    if (['clean', 'level'].includes(stateName)) {
        await this.triggerDeviceAction(devId, stateName);
        await this.setStateAsync(id, { val: false, ack: true });
    } else {
        await this.toggleDeviceSetting(devId, stateName, state.val);
    }
}

async triggerDeviceAction(devId, action) {
    try {
        await axios.post(`${this.baseUrl}/v1/devices/${devId}/action`, { action }, { headers: { Authorization: `Bearer ${this.token}` } });
        this.log.info(`Triggered ${action} for device ${devId}`);
    } catch (err) {
        this.log.error(`Action ${action} failed: ${err}`);
    }
}

async toggleDeviceSetting(devId, setting, value) {
    try {
        await axios.patch(`${this.baseUrl}/v1/devices/${devId}/settings`, { [setting]: value }, { headers: { Authorization: `Bearer ${this.token}` } });
        this.log.info(`Toggled ${setting} for ${devId} to ${value}`);
    } catch (err) {
        this.log.error(`Toggle ${setting} failed: ${err}`);
    }
}

_slugify(str) {
    return (str || '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
}

_coalesce(...args) {
    for (const a of args) if (a !== undefined && a !== null) return a;
    return undefined;
}

onUnload(callback) {
    try {
        if (this.pollTimer) clearInterval(this.pollTimer);
        callback();
    } catch (e) {
        callback();
    }
}

}

if (require.main !== module) { module.exports = (options) => new Neakasa(options); } else { new Neakasa(); }

/* ADMIN README / Notes: This adapter connects ioBroker to the Neakasa Cloud API. It creates full state trees per device, polls periodically, allows commands and switch toggles. TODO (once raw Python endpoints available):

  • Replace placeholder endpoints with real ones from hass-neakasa/api.py
  • Implement token refresh (if available)
  • Add support for websockets (if Neakasa supports push updates) */ `

luckyheiko avatar Oct 24 '25 16:10 luckyheiko