maximkulkin / esp-homekit-demo

Demo of Apple HomeKit accessory server library
MIT License
808 stars 233 forks source link

What type and characteristics #82

Closed supersjimmie closed 5 years ago

supersjimmie commented 6 years ago

I have a wallmount dimmer. It's a double dimmer with touch buttons. Those buttons are controlled over 433MHz. See my other "issue" where I am building communication with (e.g.) some other 433MHz devices.

Does anyone know what type/characteristics I could use for a dimmer with such buttons:

/---------------\
| [ 1 ]   [ 4 ] |
| [ 2 ]   [ 5 ] |
| [ 3 ]   [ 6 ] |
\---------------/
Dinner room:
1 = UP
2 = on/off
3 = DOWN

Main room:
4 = UP
5 = on/off
6 = DOWN

I have been experimenting with values from types.h and characteristics.h but still unable to find how to "create" such device.

maximkulkin commented 6 years ago

Obviously, you need a LIGHTBULB service with ON and BRIGHTNESS characteristics: ON will turn light bulb on/off and BRIGHTNESS will allow controlling.. well.. brightness. You can create two LIGHTBULB services in a single accessory.

supersjimmie commented 6 years ago

Thanks yes that sounds obvious. :) But the buttons are not like lightbulbs, because they are stateless(?) here. You tip it, instead of switching it (I'm not sure how to explain). Same for the brightness, you don't set brightness to a specific value like 60%, you can only tip on the up/down buttons without knowing the current/new value (also stateless).

I've tried to add a characteristic "stateless programmable switch, also called a button", under a type "homekit_accessory_category_programmable_switch", but then the iPhone says that it needs a home-app to use this accessory (not sure about the exact term because my ios is Dutch).

EDIT: looks like this: https://github.com/cflurin/homebridge-mqtt/issues/46#issuecomment-352287501 So I think I have to do what they suggest, use a switch and reset it after each push. Bu then, how to get 3 (of 6) buttons grouped so that is is clear the handle one light? And how to make 1 as on/off and 2 as brightness?

supersjimmie commented 6 years ago

Perhaps it's simple enough for me if you can tell me how to make the button on the iphone turn off after I tap it (now the button toggles light/dark but I need it to stay dark after each tap).

Then next will be finding a way to create some kind of grouping/layout for those 6 buttons.

renssies commented 6 years ago

Have a look at the HomeKit Specification. It is what this library is based on. You can download it here with a free developer account: https://developer.apple.com/homekit/specification/

It will explain what a programmable switch is and might give you an inspiration for the brightness up/down buttons.

supersjimmie commented 6 years ago

I guess this would turn the button to "dark" on the iPhone.

led_on.value.bool_value = false;
homekit_characteristic_notify(&led_on, switch_on.value);

@renssies thanks, I am not a developer, and for sure not an apple dev. Will look into it.

renssies commented 6 years ago

Correct, that will send out a notification to the iOS device if it's currently monitoring.

As for the developer account: You can just log in with your normal Apple ID, after accepting some terms it will be activated as a developer account as well :)

supersjimmie commented 6 years ago

I'm still a bit stuck on the part for turning off the button on the app. I use the following pieces:

homekit_characteristic_t dim1 = HOMEKIT_CHARACTERISTIC_(
    ON, false, .callback=HOMEKIT_CHARACTERISTIC_CALLBACK(dim1_callback)
);

This characteristic is added to the button, which works as expected. The dim1_callback function sends the http request, that goes fine. After sending the http request, the following code runs:

    vTaskDelay(3000 / portTICK_PERIOD_MS);
    value.bool_value = false;
    homekit_characteristic_notify(&dim1, value);

So it should take about 3 sec and then the notify should be executed. But nothing happens: in the log I only see the notify from the actual button-press (that has either true or false inside). In fact, the logs shows exact the same as for a different button without the delay+notify.

maximkulkin commented 6 years ago

@supersjimmie Are you sure you re-compiled and re-flashed it with the new code?

As of code: it usually kind of a bad idea to do long tasks in callbacks (e.g. characteristic callback). Instead, you could use a separate task to execute those http requests, communicate with it via a queue and maybe even use timers for delays to ensure you do not block any worker loop.

But in general, it would be better if you provide your code: your descriptions are nice but there might be elephant in the room which you happen not to notice.

supersjimmie commented 6 years ago

You are right @maximkulkin , there was a mistake somewhere else. [edit] Here's my newest code:

#include <stdio.h>
#include <espressif/esp_wifi.h>
#include <espressif/esp_sta.h>
#include <esp/uart.h>
#include <esp8266.h>
#include <FreeRTOS.h>
#include <task.h>

#include <homekit/homekit.h>
#include <homekit/characteristics.h>
#include "wifi.h"

#include "http_get.h"

static void wifi_init() {
    struct sdk_station_config wifi_config = {
        .ssid = WIFI_SSID,
        .password = WIFI_PASSWORD,
    };
    sdk_wifi_set_opmode(STATION_MODE);
    sdk_wifi_station_set_config(&wifi_config);
    sdk_wifi_station_connect();
}

void dim3_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context);

homekit_characteristic_t dim3 = HOMEKIT_CHARACTERISTIC_(
    ON, false, .callback=HOMEKIT_CHARACTERISTIC_CALLBACK(dim3_callback)
);

void dim3_write() {
    http_get_task("?tx433=15286572");
}

void dim3_write_task() {
    dim3_write();
    vTaskDelete(NULL);
}

void dim3_notify_task() {
    homekit_value_t value;
    value.bool_value = false;
    homekit_characteristic_notify(&dim3, value);
    vTaskDelete(NULL);
}

void dim3_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context) {
    if (on.bool_value == true) {
        xTaskCreate(dim3_notify_task, "dim3 notify_task", 256, NULL, 2, NULL);
        xTaskCreate(dim3_write_task, "dim3 write_task", 512, NULL, 2, NULL);
    }
}

void dim_init() {
}

void dim_identify_task(void *_args) {
    // no identify possible
    vTaskDelete(NULL);
}

void dim_identify(homekit_value_t _value) {
    printf("dimmer identify\n");
    xTaskCreate(dim_identify_task, "dim identify", 128, NULL, 2, NULL);
}

homekit_accessory_t *accessories[] = {
    HOMEKIT_ACCESSORY(.id=1, .category=homekit_accessory_category_switch, .services=(homekit_service_t*[]){
        HOMEKIT_SERVICE(ACCESSORY_INFORMATION, .characteristics=(homekit_characteristic_t*[]){
            HOMEKIT_CHARACTERISTIC(NAME, "Dimmer"),
            HOMEKIT_CHARACTERISTIC(MANUFACTURER, "HaPK"),
            HOMEKIT_CHARACTERISTIC(SERIAL_NUMBER, "037A2BABF19D"),
            HOMEKIT_CHARACTERISTIC(MODEL, "supersjimmie"),
            HOMEKIT_CHARACTERISTIC(FIRMWARE_REVISION, "0.1"),
            HOMEKIT_CHARACTERISTIC(IDENTIFY, dim_identify),
            NULL
        }),
        HOMEKIT_SERVICE(SWITCH, .primary=true, .characteristics=(homekit_characteristic_t*[]){
            HOMEKIT_CHARACTERISTIC(NAME, "dimbtn3"),
            &dim3,
            NULL
        }),
        NULL
    }),

    NULL
};

homekit_server_config_t config = {
    .accessories = accessories,
    .password = "111-11-111"
};

void user_init(void) {
    uart_set_baud(0, 115200);

    wifi_init();
    dim_init();
    homekit_server_init(&config);
}

The only thing it that the notify calls the callback again, so I now have to check if the call is with "true" or "false" to prevent looping. If the callback is called with "true" (called by a real button-press) then it does it's job, and if it's called with "false" (called from the notify) then it does nothing.

As you can see I created 2 tasks for the http call and for the notify.

Now, since I am no good programmer... Is there a neat way to re-use functions here to create 6 buttons? Or do I really have to multiply a lot of dim3 functions to dim1, dim2, .... dim6? For instance, is it possible to call one main-control function from different places and recognise from where it was called?

supersjimmie commented 6 years ago

I now have added a total of 3 buttons, each calling the same callback function. The idea is to use the same callback function for all similar buttons to prevent a lot of nearly identical functions. Then, in that one function it would be great if I can determine from which button it was called...

I only managed to get this information visible in the callback function, by going through the *_ch var in the callback:

struct _homekit_characteristic {
    homekit_service_t *service;

    unsigned int id;
    const char *type;
    const char *description;
    homekit_format_t format;
    homekit_unit_t unit;
    homekit_permissions_t permissions;
    homekit_value_t value;
...

But none of those contain a usable value to determine which button has been pushed. The id differs per button, but it seems to be a bit randomly assigned (three buttons are 9, 12 and 15). The description contains "Name" for all three buttons (and value is either "true" or "false").

[EDIT] It took me hours to get the above, and only a couple of minutes more to find the name of the button that called the callback now. I'm pretty sure it can be done in less steps, but this is how:

        homekit_service_t *serv = _ch->service;
        homekit_characteristic_t **chr = serv->characteristics;
        homekit_characteristic_t *c = *chr;
        printf("\r\n____callback by name = %s\r\n", c->value.string_value);

Now I can create one callback function that is called by all similar buttons, determine inside that function which button called it, and then transmit the according code. :)

maximkulkin commented 6 years ago

@supersjimmie Yes, you should be able to deduce which button that is by inspecting data starting from a characteristic pointer provided. The easiest way in your case would be:

homekit_characteristic_t *button_name = homekit_service_characteristic_by_type(
    _ch->service, HOMEKIT_CHARACTERISTIC_NAME
);
printf("button name: %s\n", button_name->value.string_value);

The other way to do that is to use characteristic callback context. It wasn't exposed through macros yet and I just pushed a change to enable that.

// NOTICE "context" argument
void dim3_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context);

homekit_characteristic_t dim3 = HOMEKIT_CHARACTERISTIC_(
    ON, false, .callback=HOMEKIT_CHARACTERISTIC_CALLBACK(
        dim3_callback, .context=(void*)"button1"
    )
);

void dim3_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context) {
    printf("button %s callback", (char *)context);
}

Of course instead of just passing string as context you can pass any other type or structure pointer in case you need to pass more data.

maximkulkin commented 6 years ago

Regarding your code: there are a lot of inefficiencies there.

  1. If you do not need identify, there is no need to start an identify task. Just wipe out dim_identify. The other way would be to specify NULL here: HOMEKIT_CHARACTERISTIC(IDENTIFY, NULL).
  2. dim3_notify_task is not needed, this code is not blocking and can be executed in the callback itself thus saving resources (creating tasks is expensive).
  3. I'm not sure where http_get_task() comes from but it looks like it is already a task, there is no need to wrap it with another task.

As of reusing code for multiple buttons, when you define your characteristics, specify same callbacks, but provide different data for callback contexts.

supersjimmie commented 6 years ago

Thanks @maximkulkin I'm doing all what you suggested. Would I also be able to use that context to store the tx codes? If so, I don't need to store those apart and use a lot of if statements to find the code that belongs to a button. :)

Seems to working, until the *context part. To get the latest changes from your git, I do git submodule update --init --recursive which seems to do just nothing. No error, so it thinks all is fine. (and when I check, no files are updated)

me@comp:~/esp-homekit-demo$ git config --list
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.url=https://github.com/maximkulkin/esp-homekit-demo.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
submodule.components/WS2812FX.url=https://github.com/pcsaito/WS2812FX-rtos
submodule.components/cJSON.url=https://github.com/maximkulkin/esp-cjson
submodule.components/homekit.url=https://github.com/maximkulkin/esp-homekit
submodule.components/http-parser.url=https://github.com/maximkulkin/esp-http-parser.git
submodule.components/wifi_config.url=https://github.com/maximkulkin/esp-wifi-config
submodule.components/wolfssl.url=https://github.com/maximkulkin/esp-wolfssl
me@comp:~/esp-homekit-demo$ git submodule update --init --recursive
me@comp:~/esp-homekit-demo$
RavenSystem commented 6 years ago

Try this from root of repo: git submodule update --recursive --remote

maximkulkin commented 6 years ago

Yes, submodule needs to be updated every time, but that would just result in lots of commits in demo repository. So it is updated less often and if you want to use latest and greatest, you need to update components manually either by doing cd components/homekit && git pull origin master or by doing git submodule update --remote components/homekit

supersjimmie commented 6 years ago

All changes seem to be working, now using that *context for the API details. :) Next step in the process is reading data over the API calls, like temperature and humidity. I don't think that will be difficult with what is build until now.

I'm not sure where http_get_task() comes from but it looks like it is already a task, there is no need to wrap it with another task.

Based on this: https://github.com/SuperHouse/esp-open-rtos/tree/master/examples/http_get (removed the loop and some obsolete code, made it using inputs and a return value)

supersjimmie commented 6 years ago

I'm trying to add CO2 measurement, but somehow the "optional" part is not visible. Is this a known issue?

homekit_characteristic_t co2detect    = HOMEKIT_CHARACTERISTIC_(CARBON_DIOXIDE_DETECTED, 0);
homekit_characteristic_t co2value     = HOMEKIT_CHARACTERISTIC_(CARBON_DIOXIDE_LEVEL, 0);
...
    HOMEKIT_ACCESSORY(.id=3, .category=homekit_accessory_category_sensor, .services=(homekit_service_t*[]) {
        HOMEKIT_SERVICE(ACCESSORY_INFORMATION, .characteristics=(homekit_characteristic_t*[]) {
            HOMEKIT_CHARACTERISTIC(NAME, "Sensors"),
            HOMEKIT_CHARACTERISTIC(MANUFACTURER, "HaPK"),
            HOMEKIT_CHARACTERISTIC(SERIAL_NUMBER, "0012345"),
            HOMEKIT_CHARACTERISTIC(MODEL, "Sensors"),
            HOMEKIT_CHARACTERISTIC(FIRMWARE_REVISION, "0.1"),
            HOMEKIT_CHARACTERISTIC(IDENTIFY, NULL),
            NULL
        }),
        HOMEKIT_SERVICE(CARBON_DIOXIDE_SENSOR, .characteristics=(homekit_characteristic_t*[]) {
            HOMEKIT_CHARACTERISTIC(NAME, "CO2detect"),
            &co2detect,
            &co2value,
            NULL

I can only see the co2detect, the co2value is missing. (a periodic notify to update the value does seem to work, it's just not showing in the app)

maximkulkin commented 6 years ago

Could be that those are specifics of Apple Home.app UI. The other thing could be is that you need to have additional characteristics. E.g. with lightbulbs you need to have both HUE and SATURATION characteristics for proper UI, if you have only one of them, UI won't show up.

supersjimmie commented 6 years ago

I found that the value is only visible "under" the Detect button. So when I click it and look at the Details.

maximkulkin commented 6 years ago

I see. You can probably also reference that sensor as a trigger in automations.

supersjimmie commented 6 years ago

I now notice a new "glitch". As explained, I turn the button off, right after clicking it. But when I re-open the app on the iphone, the button is on. This is what I see in the log at the moment I open the app (so it's a refresh):

>>> client_send: [Client 4] Sending payload: c5\x0D\x0A{"characteristics":[{"aid":1,"iid":10,"value":true},{"aid":1,"iid":13,"value":false},

That "value":true is what's wrong. Is seems that I have to set another "value" to "false", beside doing the homekit_characteristic_notify(_ch, value)?

I think that after the

        homekit_characteristic_t *button_name = homekit_service_characteristic_by_type(
            _ch->service, HOMEKIT_CHARACTERISTIC_NAME 
        );

I need to do something like this?

        button_name->value.bool_value = false;      

(which was just a though, it does not fix it)

maximkulkin commented 6 years ago

You should not update "name" characteristic.

As always, please send full code listing for troubleshooting. The problem might be in place you did not expect.

supersjimmie commented 6 years ago

Sure think that I should not update the name, but I was unable to figure out how to update the correct characteristic/value. I thought that the homekit_characteristic_notify() would handle all places where a value should be changed.

So what happens is when I push the button on the iPhone, the button goes on/off immediately and the http call is performed. All looks normal here. But when I close the Home-app and re-open it, the app refreshes it's states by doing a call to the esp8266 and then the button on the phone it displayed as "on". In the logs I see the "true":

>>> client_send: [Client 4] Sending payload: c5\x0D\x0A{"characteristics":[{"aid":1,"iid":10,"value":true},{"aid":1,"iid":13,"value":false},

(iid:10 is turned on now, while the other button iid:11 is still off because I've never clicked it)

To me it looks like there is some other place where the state is stored, which is recalled when it refreshes.

Here's the full code (except the http part, which is not relavant):

#include <stdio.h>
#include <espressif/esp_wifi.h>
#include <espressif/esp_sta.h>
#include <esp/uart.h>
#include <esp8266.h>
#include <FreeRTOS.h>
#include <task.h>
#include <string.h>

#include <homekit/homekit.h>
#include <homekit/characteristics.h>
#include "wifi.h"

#include "http_get.h"

#define API_TX433 "tx433="

void dimbtn_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context);

homekit_characteristic_t dimbtn3 = HOMEKIT_CHARACTERISTIC_(
    ON, false, .callback=HOMEKIT_CHARACTERISTIC_CALLBACK(
        dimbtn_callback, .context=(void*)(API_TX433"215286572")
    )
);
homekit_characteristic_t dimbtn4 = HOMEKIT_CHARACTERISTIC_(
    ON, false, .callback=HOMEKIT_CHARACTERISTIC_CALLBACK(
        dimbtn_callback, .context=(void*)(API_TX433"215286562")
    )
);

homekit_characteristic_t temperature   = HOMEKIT_CHARACTERISTIC_(CURRENT_TEMPERATURE, 0);

void dimbtn_write_task(void *code) {
//  printf("Send code: %s\r\n", code);
    char ret[16];
    int r = http_get_task(code, ret, 15);
    if (r > 0) printf("\r\r____http return: %s\r\n", ret);
    vTaskDelete(NULL);
}

void dimbtn_callback(homekit_characteristic_t *_ch, homekit_value_t on, void *context) {
    if (on.bool_value == true) {
        homekit_characteristic_t *button_name = homekit_service_characteristic_by_type(
            _ch->service, HOMEKIT_CHARACTERISTIC_NAME 
        );
        printf("\r\n____Callback %s\r\n", button_name->value.string_value);

        homekit_value_t value;
        value.bool_value = false;
        homekit_characteristic_notify(_ch, value);

        xTaskCreate(dimbtn_write_task, "dimbtn write_task", 768, context, 2, NULL);
    }
}

void temperature_sensor_task(void *_args) {
    float temperature_value;
    while (1) {
        char ret[8];
        int success = http_get_task("envget=temp", ret, 8);
        if (success > 0) {
            printf("____ Read temperature: %s\r\n",ret);
            temperature_value = atof(ret);
            temperature.value.float_value = temperature_value;
            homekit_characteristic_notify(&temperature, HOMEKIT_FLOAT(temperature_value));
        } else {
            printf("Couldnt read environment.\r\n");
        }

        vTaskDelay(60000 / portTICK_PERIOD_MS);
    }
}

void dimmer_init() {
}

void temperature_sensor_init() {
    xTaskCreate(temperature_sensor_task, "Temperatore Sensor", 1536, NULL, 2, NULL);
}

homekit_accessory_t *accessories[] = {
    HOMEKIT_ACCESSORY(.id=1, .category=homekit_accessory_category_switch, .services=(homekit_service_t*[]){
        HOMEKIT_SERVICE(ACCESSORY_INFORMATION, .characteristics=(homekit_characteristic_t*[]){
            HOMEKIT_CHARACTERISTIC(NAME, "Dimmer"),
            HOMEKIT_CHARACTERISTIC(MANUFACTURER, "HaPK"),
            HOMEKIT_CHARACTERISTIC(SERIAL_NUMBER, "037A2BABF19D"),
            HOMEKIT_CHARACTERISTIC(MODEL, "supersjimmie"),
            HOMEKIT_CHARACTERISTIC(FIRMWARE_REVISION, "0.1"),
            HOMEKIT_CHARACTERISTIC(IDENTIFY, NULL),
            NULL
        }),
        HOMEKIT_SERVICE(LIGHTBULB, .primary=false, .characteristics=(homekit_characteristic_t*[]){
            HOMEKIT_CHARACTERISTIC(NAME, "Eetkmr"),
            &dimbtn3,
            NULL
        }),
        HOMEKIT_SERVICE(LIGHTBULB, .primary=true, .characteristics=(homekit_characteristic_t*[]){
            HOMEKIT_CHARACTERISTIC(NAME, "Woonkmr"),
            &dimbtn4,
            NULL
        }),
        NULL
    }),

    HOMEKIT_ACCESSORY(.id=2, .category=homekit_accessory_category_thermostat, .services=(homekit_service_t*[]) {
        HOMEKIT_SERVICE(ACCESSORY_INFORMATION, .characteristics=(homekit_characteristic_t*[]) {
            HOMEKIT_CHARACTERISTIC(NAME, "Thermostat"),
            HOMEKIT_CHARACTERISTIC(MANUFACTURER, "HaPK"),
            HOMEKIT_CHARACTERISTIC(SERIAL_NUMBER, "0012346"),
            HOMEKIT_CHARACTERISTIC(MODEL, "Sensors"),
            HOMEKIT_CHARACTERISTIC(FIRMWARE_REVISION, "0.1"),
            HOMEKIT_CHARACTERISTIC(IDENTIFY, NULL),
            NULL
        }),

    NULL
};

static void wifi_init() {
    struct sdk_station_config wifi_config = {
        .ssid = WIFI_SSID,
        .password = WIFI_PASSWORD,
    };
    sdk_wifi_set_opmode(STATION_MODE);
    sdk_wifi_station_set_config(&wifi_config);
    sdk_wifi_station_connect();
}

homekit_server_config_t config = {
    .accessories = accessories,
    .password = "111-11-111"
};

void user_init(void) {
    uart_set_baud(0, 115200);

    wifi_init();
    homekit_server_init(&config);
    dimmer_init();
    temperature_sensor_init();
}
supersjimmie commented 6 years ago

Should I add this to the dimbtn_callback?

        _ch->value.bool_value = false;
maximkulkin commented 6 years ago

Ok, not sure I understand what is happening in dimbtn_callback: this callback is executed when button state is changed, it gets it's new state as parameter (on argument). If the value is true, then you execute another notification with "false" value. Thing is that this second notification has no effect on your internal state, but changes state of controllers (iPhones). So, controllers think that the state is "false", but in reality it is "true". When you re-open Home.app, it re-reads value directly -> thus change of state.

If you want to update internal state, you need to update _ch->value: _ch->value = HOEMKIT_BOOL(false); The other thing that will probably work better for you is: if you really need to control the value of your characteristic, instead of using notification callback you need to define custom setter (and in some cases - getter) for your characteristic. Support for custom getters and setters was originally designed to allow pulling/pushing data from/to external sources.

supersjimmie commented 6 years ago

Yep, that _ch->value = HOMEKIT_BOOL(false); did the trick. I don't think that the .setter (or getter) would be very useful, I can now use one function for all similar buttons (same functionality with only different properties) but with .setter I would have to create separate functions for each button. That is because with the callback I can check from which button the function was called while the .setter doesn't have that ability. (or does it?)