twocolors / esphome-native-api

MIT License
5 stars 6 forks source link

Question: seemingly irrational behavior on client disconnect, undesirable workaround #31

Open ghost opened 8 months ago

ghost commented 8 months ago

Please forgive my lack of understanding about this API and Javascript programming in general but im having pretty difficult time implementing this API in a real world project.

It was not clear in the beginning how to push state changes to the modules but then someone mentioned using switchCommandService rather than setState. So i got that to work by using an Emitter in the Client.on('newEntity' code block but this is where things get a little weird.

If the ESP client goes offline, resets or otherwise stops responding, then the problem is with the stagnant emitter that gets lost in the old client connection. I have tried many many ways to fix this issue but all attempts to use the removeEventListener function fail, even if i use global variables and functions. This leaves only one option and that is to use the removeAllListeners() function to kill all listeners (including those used by other APIs!!) and then disconnect all ESP client connections so that the listeners will be recreated and reconnect to all ESP devices. This is really undesirable because important updates or commands could be lost during this process.

I hate this workaround but i dont see any other options, ive been at this for days, im ready to give up...

     esp: function () {  // currently testing for outgoing signaling 
            espInit();                                                              // start the init sequence
            function espInit() {                                                    // create a new client for every ESP device in the local object
                for (let x = 0; x < cfg.esp.length; x++) {
                    espClient.push(clientNew(x));
                    state.esp.entities.push([]);                                    // create entity array for each ESP device
                    clientConnect(x);
                }
            }
            function clientConnect(x) {                                             // client connection function, ran for each ESP device
                espClient[x].connect();
                espClient[x].on('newEntity', entity => {
                    let exist = 0;
                    for (let e = 0; e < state.esp.entities[x].length; e++) {        // scan for this entity in the entity list
                        if (state.esp.entities[x][e].id == entity.id) exist++;
                    }
                    if (exist == 0)                                                 // dont add this entity if its already in the list 
                        state.esp.entities[x].push({ name: entity.config.objectId, type: entity.type, id: entity.id });
                    log("new esp connected, " + entity.config.objectId, 2);
                    if (entity.type === "Switch") {                                 // if this is a switch, register the emitter
                        //  log("setting entity: " + id + " to " + state, 0, 0)
                        em.on(entity.config.objectId, function (id, state) {        // emitter for this connection 
                            entity.connection.switchCommandService({ key: id, state: state });
                        });
                    }
                    entity.on('state', (data) => {
                        for (let y = 0; y < state.esp.entities[x].length; y++) {
                            if (state.esp.entities[x][y].id == data.key) {          // identify this entity request with local object
                                //     log("esp data: " + state.esp.entities[x][y].name + " state: " + data.state, 2, 0)
                                for (let a = 0; a < state.udp.length; a++) {        // scan all the UDP clients and find which one cares about this entity
                                    for (let b = 0; b < state.udp[a].esp.length; b++) {                 // scan each UDP clients registered ESP entity list
                                        if (state.udp[a].esp[b] == state.esp.entities[x][y].name) {     // if there's a match, send UDP client the data
                                            udp.send("E" + JSON.stringify({ id: state.esp.entities[x][y].name, state: data.state }), state.udp[a].port);
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    });
                });
                espClient[x].on('error', (error) => {
                    console.log(error);
                    log("ESP module went offline, resetting ESP system", 2, 2);
                    reset();                                                        // if there's a connection problem, start reset sequence
                });
            }
            function clientNew(x) {                                                 // create new object for every ESP device 
                return new Client({
                    host: cfg.esp[x].ip,
                    port: 6053,
                    encryptionKey: cfg.esp[x].key,
                    reconnect: true,
                    reconnectInterval: 2000,
                    pingInterval: 3000,
                    pingAttempts: 3
                })
            }
            function reset() {
                em.emit('ws-disconnect');                                               // close and reset websocket service (because removeAllListeners)
                em.removeAllListeners();                                                // remove all event listeners 
                for (let c = 0; c < espClient.length; c++) espClient[c].disconnect();   // disconnect every client 
                espClient = [];                                                         // delete all client objects 
                setTimeout(() => { espInit(); }, 1000);                                 // reconnect to every device
            }

        },
ghost commented 8 months ago

Update: this API does a number of things i dont like; i have quarantined this it into its own worker thread which gives me an acceptable result.

I can just call em.removeAllListeners() and reestablish at will without effecting other APIs because its within its own worker thread environment.

richardhopton commented 8 months ago

You should either elevate your event handler to an instanced function if you are using classes, or capture a reference to your event handler in a const, and importantly pass the SAME reference into on and off functions.

one way you could do this in your example is:-

const newEntityHandler = (entity) => {
    let exist = 0;
    ...
espClient[x].on('newEntity', newEntityHandler);
const errorHandler = (error) => {
    console.log(error);
    log("ESP module went offline, resetting ESP system", 2, 2);
    espClient[x].off('newEntity', newEntityHandler);
    espClient[x].off('error', errorHandler);
    reset();                                                        // if there's a connection problem, start reset sequence
});
espClient[x].on('error', errorHandler);

You'll probably want to keep track of all the entity 'state' handlers you on as well... e.g.

in espInit after adding the client to espClient you could:

handlers[x] = [];

where handlers is an empty correctly sized array (for the number of clients) and then when subscribing to state changes on an entity you can do something like this:

const stateHandler = ....
entity.on('state', stateHandler);
handlers[x].push(() => entity.off('state', stateHandler));

Then in your error handler you can do something like this:

handlers[x].forEach(f=>f());
richardhopton commented 8 months ago

Additionally to all that... if a client errors and disconnects, you don't need to make a new one... you can just call connect() on the existing instance. The Client, Connection, and FrameHelper only get reset if YOU explicitly call disconnect on the Client or Connection.