diff --git a/.gitignore b/.gitignore index 77483869..a985112e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ .vscode/launch.json /test/remote/settings.json /test/remote/espmh.env +lib/Environment/wifi_credentials.h \ No newline at end of file diff --git a/README.md b/README.md index 5a451753..7748eb8f 100644 --- a/README.md +++ b/README.md @@ -125,13 +125,12 @@ You can configure aliases or labels for a given _(Device Type, Device ID, Group ## REST API -The REST API is specified using the [OpenAPI v3](https://swagger.io/docs/specification/about/) specification. +Generated API documentation is available here: -[openapi.yaml](docs/openapi.yaml) contains the raw spec. +* [latest version](https://sidoh.github.io/esp8266_milight_hub/branches/latest) +* [all versions](https://sidoh.github.io/esp8266_milight_hub) -[You can view generated documentation for the master branch here.](https://sidoh.github.io/esp8266_milight_hub/branches/latest) - -[Docs for other branches can be found here](https://sidoh.github.io/esp8266_milight_hub) +API documentation is generated from the [OpenAPI spec](docs/openapi.yaml) using redoc. ## MQTT diff --git a/dist/index.html.gz.h b/dist/index.html.gz.h index 43c6cec5..2face512 100644 --- a/dist/index.html.gz.h +++ b/dist/index.html.gz.h @@ -1,2 +1,2 @@ -#define index_html_gz_len 12910 -static const char index_html_gz[] PROGMEM = {31,139,8,0,0,0,0,0,0,19,237,125,107,123,219,54,178,240,247,247,87,32,76,55,150,106,138,146,175,113,100,83,57,242,37,137,91,219,113,98,167,217,174,215,71,15,37,65,18,99,138,212,146,148,101,215,213,127,127,103,6,0,9,94,100,43,217,236,158,158,158,77,159,90,36,1,2,51,131,193,220,48,0,247,158,245,131,94,124,63,225,108,20,143,189,214,30,254,101,158,227,15,109,131,251,6,220,115,167,223,218,27,243,216,97,189,145,19,70,60,182,141,105,60,168,237,64,89,236,198,30,111,157,186,39,238,112,20,179,119,211,238,94,93,60,218,243,92,255,134,133,220,179,141,40,190,247,120,52,226,60,54,216,40,228,3,219,24,197,241,36,106,214,235,99,231,174,215,247,173,110,16,196,81,28,58,19,188,233,5,227,122,242,160,190,97,109,88,47,235,189,40,74,159,89,99,23,106,69,145,193,168,39,219,24,59,46,130,73,29,102,219,31,186,49,54,9,63,163,105,215,114,131,180,145,90,28,12,135,30,175,175,91,240,95,182,125,89,148,118,147,71,162,180,43,232,231,75,100,245,188,96,218,31,120,78,200,9,15,231,139,115,87,247,220,174,222,122,228,185,125,30,214,95,89,47,173,70,174,99,81,244,125,59,142,184,199,123,177,251,27,183,190,68,245,134,181,182,110,109,83,175,233,243,164,255,141,127,25,202,212,87,125,13,123,223,204,227,76,101,143,245,76,140,231,59,99,24,233,91,151,207,38,65,8,124,212,11,252,152,251,192,136,51,183,31,143,236,62,191,117,123,188,70,55,166,235,187,177,235,120,181,168,231,0,123,172,65,19,207,106,181,43,119,192,188,152,29,31,177,87,215,173,255,199,224,223,94,212,11,221,73,204,162,176,183,52,78,56,53,182,162,145,123,11,140,249,210,218,72,239,129,186,208,79,93,52,137,237,239,61,187,226,126,223,29,92,215,106,173,61,194,167,101,225,68,226,97,45,12,102,15,221,32,196,203,110,16,199,193,184,185,54,185,99,81,0,163,207,158,247,122,189,185,231,116,185,247,208,119,163,137,231,220,55,187,94,208,187,153,91,161,211,119,131,90,48,137,221,192,127,152,56,253,190,235,15,155,13,182,53,185,219,237,77,195,40,8,155,147,192,5,162,132,115,132,121,236,248,253,90,119,10,173,251,209,131,231,70,113,141,96,104,250,129,207,119,199,78,56,116,253,102,99,55,105,166,240,14,243,220,4,0,215,135,193,231,53,130,67,190,91,11,113,190,55,215,248,120,55,184,229,225,192,11,102,77,103,26,7,115,107,16,132,227,26,140,76,120,255,160,186,97,13,182,222,0,12,27,122,41,19,215,56,142,97,224,61,208,200,53,215,27,124,156,169,148,165,132,14,136,32,18,224,19,87,44,30,134,65,88,101,226,183,230,250,131,32,121,5,241,157,235,5,189,192,3,82,133,188,191,59,128,174,107,17,204,128,166,245,18,187,77,43,53,187,28,96,224,15,146,201,154,43,149,149,76,177,51,0,50,167,165,85,40,149,67,219,141,125,133,54,162,60,183,134,78,204,103,206,125,13,8,77,133,64,41,39,110,18,245,230,207,101,11,204,234,135,193,164,31,204,96,92,131,200,197,1,110,74,30,206,81,183,248,70,109,204,253,169,62,88,130,56,207,97,110,13,6,110,175,22,249,238,96,144,165,198,115,122,198,251,53,89,7,0,190,171,141,56,13,232,86,67,27,209,218,189,28,83,213,217,99,195,97,137,187,5,133,179,145,27,243,90,204,199,147,218,196,237,221,0,245,100,135,235,208,95,215,233,221,12,195,96,234,247,155,248,142,19,214,134,200,236,128,103,37,14,24,209,202,124,238,108,247,215,6,3,214,48,159,15,224,103,171,241,23,188,112,26,141,6,91,107,52,254,82,221,45,101,86,197,223,27,130,251,70,83,94,210,189,96,61,108,101,55,55,229,210,250,48,238,126,217,91,32,97,122,21,124,149,213,216,6,31,47,0,35,59,63,151,194,23,24,84,224,10,248,109,91,219,219,219,47,1,225,6,220,109,128,70,132,127,116,39,233,208,104,12,216,118,82,105,0,119,59,170,18,54,67,244,17,200,220,58,30,252,149,48,74,25,164,11,31,32,167,154,224,30,31,196,77,107,43,193,20,113,214,208,47,31,230,137,55,141,106,32,202,225,47,34,56,121,200,176,175,78,233,158,7,120,55,65,252,141,242,68,207,183,193,132,68,106,14,220,16,164,88,48,168,161,165,146,149,159,66,26,213,144,136,211,8,196,154,44,140,131,73,190,68,76,63,196,109,217,110,61,103,97,175,216,76,105,167,217,130,167,58,178,200,140,81,109,75,26,33,195,170,38,133,200,66,205,160,158,8,89,78,131,150,240,56,40,2,166,254,127,9,74,33,17,242,52,64,114,232,214,172,205,87,101,68,24,120,252,110,215,241,220,161,95,131,137,58,142,154,61,46,84,201,192,229,94,31,222,246,38,229,234,32,21,162,107,214,58,114,75,158,125,64,4,122,247,147,145,11,146,203,138,38,48,141,0,214,7,199,119,199,14,201,57,124,196,214,34,6,114,21,69,30,103,98,70,236,214,102,188,123,227,198,181,108,205,245,146,170,115,212,193,169,224,4,251,1,234,223,130,146,3,40,20,214,192,113,243,110,208,87,42,73,169,221,245,6,202,232,1,216,34,218,220,166,103,255,53,230,125,215,97,129,239,221,51,208,233,156,251,12,148,35,171,96,163,52,66,172,185,129,2,190,250,80,214,236,22,54,177,76,27,155,59,11,219,104,44,219,198,203,237,157,5,109,172,109,237,44,217,198,171,87,235,139,218,88,219,198,54,172,113,208,7,139,10,43,48,43,66,195,49,240,107,25,190,45,90,50,156,115,197,156,170,212,2,209,33,245,15,75,249,162,70,210,72,114,249,246,96,187,164,70,4,211,74,213,24,108,111,207,167,158,229,8,32,22,217,56,154,105,147,173,185,200,178,41,169,71,214,133,20,60,189,145,235,245,171,15,5,246,254,175,27,126,63,8,193,58,141,24,114,232,195,32,12,198,15,160,89,253,8,205,152,38,217,160,149,181,42,11,131,24,76,129,74,163,58,143,131,71,202,55,182,27,125,62,172,194,168,169,25,144,109,127,93,116,160,10,211,134,50,237,47,44,78,154,183,18,251,191,214,135,171,152,103,44,19,171,87,75,203,81,32,164,182,28,152,124,115,176,115,201,160,213,157,130,190,19,59,77,152,171,67,94,191,171,225,152,129,154,139,248,246,166,217,110,183,247,219,237,163,246,17,252,197,223,131,246,126,176,255,161,221,126,51,132,219,3,252,211,254,128,127,142,219,170,92,253,59,106,103,255,101,239,235,27,59,103,237,253,195,213,131,217,97,253,203,135,168,253,106,242,233,125,123,255,227,180,14,247,239,62,204,218,171,195,243,176,125,190,121,218,109,183,255,182,51,216,105,31,220,117,95,193,253,219,155,217,193,231,122,59,104,31,254,250,101,181,125,190,245,234,224,195,225,203,55,237,155,246,65,189,238,183,127,250,245,231,250,135,246,135,250,108,179,221,27,222,214,219,231,47,215,14,102,7,23,135,231,59,237,213,246,205,175,112,31,2,26,103,245,213,70,187,126,119,212,107,159,175,133,7,179,246,105,61,184,105,31,14,86,223,181,143,235,47,1,165,147,250,86,212,62,60,127,181,213,110,55,62,1,60,155,245,33,220,255,53,92,5,74,188,175,15,15,189,131,58,180,119,219,56,107,183,55,234,71,179,253,205,35,128,111,103,22,190,108,159,55,142,57,180,255,242,124,179,221,29,236,156,180,219,211,159,234,179,131,95,14,111,161,254,168,247,182,125,240,110,245,112,184,255,91,125,22,1,126,47,235,237,253,203,213,191,125,104,159,30,220,54,218,206,208,175,183,127,186,172,31,126,56,92,61,58,199,254,95,70,237,247,131,250,97,251,176,126,120,7,240,14,215,97,36,54,207,234,195,253,70,125,56,107,31,254,237,55,120,255,4,232,5,240,239,255,218,94,157,253,227,180,253,159,127,255,249,247,159,127,255,199,255,129,156,203,252,251,20,193,159,213,179,89,187,253,246,183,227,228,233,241,151,70,251,231,81,187,253,110,180,35,159,4,248,231,140,46,247,103,248,183,14,255,107,111,208,191,113,27,69,254,97,244,69,123,118,113,50,91,0,202,254,169,127,150,125,18,158,30,206,126,57,249,152,213,5,135,135,159,250,139,176,249,233,80,171,123,240,105,19,127,126,62,204,162,248,230,0,235,244,240,114,91,60,161,119,254,42,129,64,157,244,249,100,216,118,218,41,156,39,63,131,214,122,255,1,219,217,204,2,179,127,115,158,92,159,111,1,13,14,199,127,131,247,118,110,128,78,39,183,0,231,193,96,21,222,253,173,14,226,246,188,142,52,26,172,194,159,119,245,161,188,7,109,3,245,253,250,141,172,127,120,127,9,247,91,227,158,108,175,109,203,48,25,170,89,131,161,103,2,215,154,234,197,72,148,8,218,162,193,214,42,13,116,97,76,203,26,6,193,16,28,177,137,27,229,226,92,95,254,49,229,225,125,125,195,90,179,214,228,13,133,232,50,81,46,213,174,8,21,199,252,46,174,127,113,110,29,241,212,104,13,166,62,25,84,44,154,78,48,104,23,117,40,90,214,1,91,46,4,88,43,213,7,12,18,133,60,158,134,190,1,22,152,227,93,136,18,3,92,130,153,235,247,131,217,139,23,254,212,243,158,217,182,184,181,244,90,243,158,19,247,70,149,184,42,155,120,182,54,159,39,93,250,124,118,57,226,99,126,18,56,125,222,135,174,146,146,24,110,110,157,144,197,246,15,21,227,121,55,246,223,145,221,111,84,49,252,88,49,70,242,206,140,236,216,10,57,88,137,61,94,49,38,119,134,105,24,213,93,99,29,236,100,227,153,205,95,188,80,151,241,139,23,156,254,86,184,29,155,208,230,34,7,47,215,131,201,171,80,123,197,245,39,211,248,74,68,55,35,48,209,166,145,113,189,82,77,99,178,151,20,139,174,24,125,14,183,193,189,81,44,82,142,75,52,175,86,205,181,134,109,59,175,201,185,62,70,7,14,28,254,74,80,109,58,171,171,115,68,154,63,129,116,96,71,60,78,94,140,205,173,70,163,106,58,118,99,254,131,53,228,241,79,23,239,207,42,9,3,17,28,51,28,3,193,59,19,23,216,229,75,4,220,103,42,98,227,224,136,110,99,43,198,225,136,160,7,132,128,110,106,104,178,3,81,57,6,252,142,28,24,203,244,61,160,14,189,233,216,43,96,100,182,246,28,105,101,62,55,88,15,60,242,200,54,122,35,224,58,46,44,126,10,130,145,157,106,176,105,232,217,43,171,49,34,118,208,247,87,141,150,1,55,72,223,85,99,175,238,0,243,66,115,198,110,96,57,147,9,247,251,21,167,58,135,255,118,177,175,132,77,37,131,218,139,248,118,215,29,84,242,149,37,83,33,94,182,206,165,2,239,93,99,234,247,57,184,173,188,143,44,3,19,54,24,136,202,192,55,48,97,193,99,226,192,220,195,138,113,128,104,129,223,34,74,89,28,48,128,31,47,137,91,208,222,190,210,87,61,144,87,156,56,14,97,88,128,62,134,89,236,186,3,4,1,110,230,241,165,59,230,193,52,78,137,92,125,208,134,2,3,123,192,14,136,105,197,160,169,195,154,73,215,115,115,141,111,84,205,252,164,170,206,191,124,64,201,80,209,199,59,174,24,40,116,160,45,184,55,122,158,219,187,129,185,99,45,24,46,83,131,70,114,74,37,30,185,145,132,164,186,27,63,9,34,80,38,67,65,81,2,8,11,226,113,49,184,129,109,192,168,219,198,190,154,61,172,114,200,7,206,212,139,171,134,109,219,252,245,119,90,147,106,42,248,197,168,0,241,81,150,228,152,5,6,189,56,80,54,95,48,122,118,144,195,240,211,199,19,201,30,136,63,148,198,75,112,70,80,28,63,228,252,130,48,207,40,137,101,105,241,37,79,138,82,61,145,105,122,217,165,185,47,11,86,230,158,238,225,137,165,28,156,145,97,212,11,66,90,25,91,179,118,172,13,237,89,237,187,244,177,96,213,175,116,197,239,159,238,172,108,165,15,174,64,171,248,125,199,11,124,174,213,248,222,216,101,22,248,190,44,88,223,251,38,227,33,28,118,47,131,119,145,87,113,76,110,134,213,7,167,110,175,111,109,153,92,252,132,244,35,196,183,25,155,61,251,212,137,71,22,176,173,172,110,142,228,19,215,87,79,124,187,210,91,29,85,235,235,40,198,123,182,61,170,130,170,183,27,187,220,139,56,201,160,174,221,171,141,118,163,153,75,230,133,237,183,172,173,215,221,122,101,189,6,143,171,77,184,194,215,205,94,245,161,231,68,156,57,205,200,174,240,90,88,173,119,87,43,124,47,124,189,221,108,84,119,187,33,119,110,118,169,2,199,10,97,205,193,10,235,122,65,136,5,78,141,99,193,230,60,170,219,219,115,97,201,60,140,154,145,25,53,99,211,107,250,154,77,243,241,237,126,28,188,187,248,69,81,98,13,228,150,19,14,167,32,77,227,200,242,184,63,140,71,100,134,56,214,208,12,225,111,23,84,183,99,133,85,69,158,2,113,122,5,226,140,236,184,214,3,18,129,25,97,199,175,27,205,81,61,54,187,118,92,71,34,43,138,72,196,123,0,126,67,199,7,41,1,132,88,29,253,152,208,193,68,172,126,28,229,201,1,212,88,93,255,113,84,82,138,52,1,146,172,110,38,165,89,154,248,230,109,179,59,159,47,203,69,63,100,181,93,63,232,17,181,164,110,34,149,4,202,169,57,112,193,60,43,170,162,31,132,40,7,194,113,11,237,161,106,98,16,214,255,254,247,250,208,52,234,134,246,200,250,241,239,245,58,89,137,32,187,67,119,56,228,32,123,177,101,49,3,12,243,42,190,6,129,107,234,96,0,226,253,251,156,70,22,208,8,16,245,215,147,90,220,140,149,113,36,33,180,38,48,57,129,9,192,248,36,123,82,216,157,208,6,88,28,125,104,17,233,2,106,200,181,227,93,39,97,20,135,112,114,73,11,72,11,232,211,217,241,101,231,188,253,177,125,122,97,63,0,87,156,186,33,239,71,205,181,173,13,19,184,70,222,109,188,108,224,221,62,69,10,125,30,69,77,96,142,185,249,233,184,115,217,222,191,176,175,30,98,103,216,52,98,167,91,155,185,3,23,224,14,93,48,180,188,251,166,241,25,239,231,102,90,1,148,52,128,169,213,184,160,7,122,21,15,44,38,173,194,201,209,97,166,152,22,197,245,10,31,233,129,94,101,252,143,56,214,107,156,126,184,188,204,84,160,32,41,45,30,68,122,189,75,237,241,252,26,241,123,115,124,116,114,152,96,232,244,129,66,157,105,196,67,180,46,245,55,219,88,194,210,18,92,61,105,26,159,228,61,3,107,151,129,30,39,43,207,245,193,72,193,49,100,51,222,157,160,239,99,162,117,216,4,95,32,132,114,184,115,186,26,49,21,216,162,239,9,24,194,179,32,204,16,232,60,121,38,122,85,247,223,163,215,81,16,197,121,92,223,37,207,68,127,23,220,27,212,96,78,128,185,195,251,76,189,129,166,88,4,111,48,212,67,67,112,238,226,17,59,60,120,119,14,110,44,248,150,224,5,44,213,63,222,116,208,73,114,123,29,55,203,55,244,144,29,159,179,118,191,31,2,79,38,224,36,5,142,40,96,21,240,141,110,57,235,130,196,184,65,176,96,152,216,225,187,131,243,234,183,192,208,241,121,60,118,162,155,114,88,206,84,161,128,69,222,170,78,137,8,73,229,111,234,93,38,23,148,247,254,86,20,230,41,162,145,66,2,226,68,76,54,196,102,35,92,28,74,161,98,46,174,58,112,183,231,14,112,30,46,13,226,56,232,243,236,204,127,227,178,83,122,40,160,184,12,239,161,111,228,197,183,12,43,51,119,192,238,131,233,74,200,217,200,185,197,231,128,101,215,245,220,248,158,77,194,160,235,241,113,164,250,23,41,48,29,185,76,3,150,45,221,71,205,7,0,103,223,48,1,134,183,134,233,3,189,1,166,114,24,123,188,51,113,125,29,192,131,35,86,103,231,63,95,50,122,46,167,14,76,98,80,187,71,23,231,59,235,219,219,72,42,49,139,86,14,142,86,88,5,175,206,62,190,89,223,60,105,172,49,90,215,31,128,26,168,50,172,0,13,201,26,43,231,39,107,107,219,47,235,39,151,59,175,26,141,21,173,226,66,106,70,25,25,216,139,252,2,172,23,103,203,129,121,113,182,178,100,47,192,14,60,206,247,243,241,232,226,104,57,130,80,205,101,251,2,137,158,239,9,164,122,190,31,201,155,216,60,150,138,208,8,147,235,118,172,210,176,225,210,1,182,232,87,119,153,207,135,180,228,11,212,189,5,243,29,87,202,220,161,239,120,172,18,114,76,107,66,193,83,91,167,182,112,121,47,112,64,28,66,163,203,14,193,196,233,221,0,117,64,168,113,39,142,178,194,22,75,88,82,34,121,123,196,153,63,29,119,57,116,7,94,190,139,75,119,152,215,65,181,216,199,55,76,52,24,161,68,36,39,181,59,245,186,209,66,96,194,140,70,203,2,211,153,240,176,227,5,193,100,49,84,12,170,48,81,69,138,161,4,52,85,67,9,103,23,231,62,78,74,143,179,97,96,49,246,14,84,60,84,165,172,145,136,141,185,227,195,100,133,41,26,143,192,190,24,142,192,210,48,49,77,130,121,40,77,198,224,75,187,49,8,56,104,192,90,18,27,116,52,36,46,29,152,21,224,249,102,116,203,229,229,185,34,155,42,21,56,156,98,95,19,207,237,137,113,23,133,200,155,89,242,208,144,75,21,131,121,3,152,82,133,106,169,123,143,254,51,163,246,219,231,199,22,251,116,72,23,24,74,133,54,61,239,30,222,234,113,104,26,88,110,42,250,225,106,216,76,22,5,130,12,170,151,104,20,76,61,104,149,167,83,2,155,94,82,149,79,227,0,224,5,6,15,99,28,78,55,200,232,243,54,20,215,100,49,83,197,130,8,88,132,185,17,10,96,81,7,17,19,217,144,140,195,108,184,215,88,17,99,146,49,173,35,115,16,232,46,239,3,226,48,199,26,4,177,154,79,75,78,10,168,222,195,228,158,251,14,229,99,106,16,31,170,18,38,74,164,245,3,20,198,123,100,54,140,251,129,158,145,189,170,202,114,82,4,190,197,100,100,134,56,19,241,137,208,132,160,183,137,192,48,124,42,225,88,96,10,99,178,185,179,243,234,149,194,7,94,147,232,44,55,6,104,34,118,192,60,3,64,242,150,34,83,143,5,26,135,1,6,87,80,200,107,122,20,72,75,85,187,97,112,3,206,60,123,79,10,137,70,69,144,250,30,230,21,129,79,58,191,194,239,156,241,196,3,93,49,190,199,247,228,107,61,204,158,216,217,217,92,8,50,25,178,25,144,227,0,216,21,204,192,24,148,138,95,128,156,74,89,82,170,236,65,186,37,226,167,181,34,109,88,112,0,142,4,132,77,120,6,84,142,234,77,65,231,142,219,79,46,17,200,122,147,28,13,120,108,177,11,206,217,199,163,246,225,233,17,181,61,152,134,49,202,142,62,143,29,215,91,44,221,138,72,77,39,125,152,109,79,224,38,42,61,142,34,160,52,153,118,1,171,17,211,222,137,44,118,46,89,45,30,129,96,113,104,34,211,100,135,169,27,6,99,22,72,192,37,103,97,174,204,36,255,6,137,110,170,77,102,180,156,112,51,215,243,112,34,2,239,162,48,149,29,130,96,32,131,6,94,249,10,50,160,198,123,138,10,84,231,107,198,89,167,137,122,217,98,159,133,213,39,22,43,132,87,28,137,98,147,166,223,96,10,104,221,248,152,0,42,94,18,193,107,249,2,33,13,178,79,182,11,84,84,206,69,14,176,229,57,160,196,167,18,35,8,5,44,239,83,49,229,96,128,119,195,132,229,144,153,184,75,119,91,230,78,81,75,147,69,62,85,210,233,55,247,217,243,48,237,179,35,204,27,49,218,133,238,15,168,14,25,229,96,2,93,138,58,2,146,131,192,247,185,92,100,19,197,96,108,68,14,142,223,19,195,2,218,253,18,111,92,191,231,77,251,80,255,228,243,37,177,122,215,133,137,11,165,143,206,231,197,250,189,136,98,136,111,100,120,247,60,195,129,26,196,32,28,69,109,54,240,156,97,226,177,12,24,247,73,55,153,197,87,242,72,82,35,200,156,68,58,173,181,4,228,133,238,131,17,135,83,110,52,141,35,95,42,66,99,224,120,17,62,57,84,186,113,190,0,209,200,69,113,217,41,14,105,198,110,207,140,227,169,194,65,119,139,142,97,28,169,41,114,138,76,145,179,71,26,144,232,28,177,149,158,24,113,222,95,161,209,90,33,5,154,60,42,208,3,6,18,218,20,131,6,212,17,173,98,170,22,115,186,193,84,88,11,104,51,3,5,76,77,165,153,140,199,192,34,170,57,201,36,253,229,105,120,65,88,232,36,148,32,44,36,225,40,24,243,14,76,44,23,227,213,113,71,179,47,66,62,112,239,178,49,135,49,111,171,154,98,160,83,163,227,92,86,79,120,7,236,22,83,96,34,184,136,124,176,97,72,73,165,130,93,50,237,173,68,162,197,212,50,81,129,55,135,188,81,152,69,160,217,131,25,24,37,168,49,224,161,19,9,3,191,139,134,87,76,35,193,28,221,54,91,110,178,144,101,220,73,28,68,82,176,133,200,86,234,64,50,81,46,208,92,183,54,217,219,119,191,49,106,131,70,217,3,51,4,153,71,136,115,49,241,133,143,77,146,92,57,219,126,16,75,63,28,108,123,229,206,62,123,114,152,125,172,217,52,232,199,48,133,115,11,243,90,247,117,245,129,206,26,253,225,96,125,19,172,198,25,186,46,96,162,122,58,146,212,34,59,199,66,118,34,10,165,204,165,71,84,159,228,145,47,97,125,18,212,211,227,51,144,163,40,128,78,222,127,6,87,51,152,25,230,187,227,183,239,128,141,192,182,49,204,211,246,95,161,220,185,123,2,94,97,28,117,144,156,126,25,200,39,194,120,58,80,229,2,234,207,35,183,55,98,242,165,40,103,251,38,82,12,179,18,208,103,20,212,124,2,31,194,130,240,57,61,62,196,171,126,6,159,199,177,80,144,20,225,191,64,7,240,32,41,94,4,62,249,137,58,224,136,72,2,60,72,155,79,196,76,3,142,163,149,188,8,86,48,239,71,96,15,209,251,209,66,49,2,83,53,70,49,215,124,24,11,7,143,55,159,53,230,255,60,238,114,240,74,60,120,57,108,57,15,254,216,239,133,220,137,196,50,188,80,146,120,143,211,28,80,117,198,193,20,228,142,244,237,17,57,184,19,93,16,238,64,17,105,47,162,85,76,30,143,238,146,164,85,19,71,7,195,108,27,139,117,106,198,87,17,118,225,192,155,70,35,33,44,192,59,207,7,0,193,100,195,114,150,150,75,183,25,92,224,241,116,156,113,9,61,0,135,131,10,233,163,157,26,207,48,59,156,94,150,49,56,50,17,81,129,70,163,114,108,64,212,56,247,164,139,220,49,37,154,199,28,196,206,4,117,74,20,231,90,88,210,183,212,236,223,16,255,120,238,216,45,132,240,101,211,88,129,201,10,95,133,164,238,18,96,37,12,194,200,54,43,125,205,255,196,20,152,175,176,118,250,28,244,170,15,226,155,232,82,0,90,21,51,89,188,20,200,130,196,137,37,47,128,84,160,211,94,52,37,230,191,17,242,76,212,164,131,209,157,56,6,115,6,46,120,52,10,188,254,194,232,18,83,117,153,86,55,49,77,113,99,95,196,70,160,109,38,217,144,148,35,99,72,248,38,154,40,231,122,64,140,84,50,69,148,104,122,169,241,194,132,7,244,191,124,49,37,41,38,197,42,96,220,233,164,170,10,69,223,115,48,118,152,235,84,104,232,164,91,134,27,218,160,239,83,17,208,202,182,75,173,192,36,87,216,77,39,80,83,143,74,172,55,26,227,111,10,217,165,212,5,100,113,121,233,22,140,138,37,232,155,169,253,213,20,206,6,242,116,242,20,233,2,82,26,103,51,237,247,27,56,17,114,87,134,173,26,38,27,59,119,196,175,130,88,107,141,6,60,107,40,137,16,45,102,186,199,232,50,22,115,96,49,41,146,10,37,216,15,40,247,138,48,64,185,213,115,208,230,21,50,91,104,167,92,204,147,167,46,125,6,183,141,101,97,23,113,15,41,139,113,139,84,70,171,188,37,199,88,204,83,85,154,44,143,129,129,8,142,11,105,86,81,166,197,15,149,161,141,126,108,217,92,135,113,249,120,116,129,126,77,52,1,141,200,69,132,51,21,93,10,250,50,240,202,167,190,176,137,59,137,189,74,75,55,29,177,212,79,4,208,52,11,61,164,16,66,200,111,221,0,189,77,92,186,17,18,40,2,175,70,24,213,117,218,167,195,228,134,226,4,245,55,240,236,227,219,253,207,159,69,188,155,85,132,221,9,143,86,15,14,46,49,164,246,230,211,101,99,231,85,213,76,94,149,241,150,254,151,41,168,18,209,106,218,13,190,65,219,74,25,110,43,229,33,22,200,153,43,192,87,203,156,0,225,100,18,6,147,208,37,223,145,92,160,177,115,35,56,67,8,78,17,229,145,175,225,246,204,2,150,48,255,63,183,63,158,29,159,189,109,10,65,49,224,162,67,184,68,35,26,64,158,0,88,168,17,73,128,173,16,184,43,9,42,95,235,126,22,189,207,71,204,27,222,23,195,70,107,111,32,8,7,238,48,191,188,66,35,213,159,34,83,51,172,198,84,53,49,56,73,21,90,1,212,98,200,100,253,176,118,175,135,2,249,28,55,176,202,122,142,27,147,125,20,200,150,144,22,180,192,47,241,84,64,105,140,39,220,191,50,152,7,194,55,44,131,153,0,34,136,69,37,217,163,79,57,17,79,0,63,114,162,226,91,42,168,76,109,250,160,93,130,240,230,107,129,14,136,223,114,243,35,11,134,86,101,41,18,71,83,162,49,198,218,238,89,56,165,125,146,95,11,150,144,105,165,48,37,107,35,209,50,240,160,153,142,163,75,203,39,24,27,197,155,228,253,111,1,10,216,114,234,103,64,123,131,22,33,235,9,91,186,0,221,89,126,1,13,224,67,136,105,126,147,49,41,224,86,98,28,21,94,79,230,206,46,148,225,58,124,82,236,119,210,236,143,146,213,23,101,159,167,149,228,26,12,238,158,212,204,142,82,165,4,243,254,62,107,200,97,32,49,53,104,180,54,149,187,128,50,146,150,150,134,80,56,245,80,171,165,89,40,166,72,106,69,225,227,161,55,188,216,97,136,179,169,43,111,63,190,255,116,222,185,184,108,95,30,117,126,62,250,245,194,190,50,164,182,80,41,231,166,209,77,210,121,224,70,186,229,198,104,74,117,18,145,11,55,36,214,224,87,140,188,113,195,189,91,55,121,222,65,73,140,141,129,128,151,188,97,160,96,156,130,194,237,168,55,249,96,64,115,215,72,214,20,224,90,173,35,164,143,69,120,195,8,70,201,139,35,126,39,175,175,77,96,133,206,233,251,195,35,196,229,253,96,0,165,23,24,147,17,249,161,112,247,6,12,151,244,142,202,186,30,230,140,136,18,117,237,209,230,126,184,122,239,67,163,159,14,207,59,231,31,223,95,190,63,120,127,210,249,229,232,227,197,241,251,51,232,96,203,220,190,54,15,143,222,180,63,157,92,118,210,58,73,21,123,203,76,18,43,77,25,15,186,72,30,208,86,86,24,30,251,217,154,233,5,14,78,171,11,233,230,226,35,82,237,112,67,38,195,113,95,127,212,198,166,224,1,230,41,26,198,51,74,43,199,81,176,84,86,141,200,2,155,241,238,69,128,252,99,251,124,198,62,171,187,138,49,195,20,78,99,181,240,218,170,209,220,89,51,170,187,201,139,86,224,75,151,222,78,147,204,170,15,152,229,46,129,87,91,38,184,133,177,195,93,204,27,207,29,34,97,84,173,9,218,86,152,111,182,7,87,152,118,143,25,247,116,89,157,207,105,251,65,28,188,227,119,153,62,228,238,143,198,157,177,202,173,56,184,32,78,174,172,109,87,225,230,211,4,230,218,1,80,179,82,157,11,162,112,69,37,189,137,2,5,27,233,112,88,48,99,126,65,35,181,2,144,19,95,29,247,41,25,94,86,134,199,67,113,69,79,49,20,155,212,188,4,6,172,150,141,79,200,7,232,235,208,19,242,182,17,190,180,205,12,112,63,84,86,158,203,30,152,216,234,129,4,20,231,48,216,198,202,42,95,93,161,76,109,202,147,151,237,32,20,133,70,72,82,123,238,83,175,15,121,124,48,13,49,25,240,80,162,107,107,217,133,130,218,32,110,194,136,31,251,113,37,165,211,80,209,169,154,105,164,128,82,210,6,178,128,66,140,54,78,83,250,5,32,8,140,128,48,86,12,2,18,134,222,196,36,196,182,223,23,83,66,48,181,214,30,112,217,179,28,145,85,254,229,67,58,16,205,20,38,26,164,170,169,134,179,89,68,25,74,37,104,205,2,42,72,164,216,126,223,253,130,73,201,120,198,140,203,163,74,126,210,90,210,40,195,4,74,15,140,219,204,6,7,73,128,142,229,70,71,255,152,226,54,153,171,181,107,139,162,193,212,199,185,19,58,227,200,228,152,84,153,155,202,13,51,150,233,151,173,198,235,66,167,9,175,198,87,141,235,106,179,80,78,187,122,42,213,130,124,152,207,77,49,0,159,66,207,46,36,177,150,146,39,182,75,8,99,58,118,158,202,66,244,216,54,175,162,119,53,51,78,209,16,167,147,33,148,177,112,124,104,96,165,132,29,228,1,69,9,23,128,127,2,215,209,196,115,65,34,153,112,169,22,156,42,78,245,247,223,43,177,221,168,154,218,230,24,59,217,28,83,236,81,44,55,98,135,82,106,212,101,218,90,4,82,14,164,26,252,117,232,111,60,47,206,81,157,48,57,118,251,253,247,116,119,83,66,72,32,135,62,5,193,178,232,123,156,90,250,68,146,168,130,227,171,79,149,236,172,205,204,19,57,119,229,52,41,78,16,77,182,217,29,75,121,226,21,189,127,220,41,247,131,133,169,247,149,135,105,232,53,53,56,87,141,215,116,126,193,123,255,195,148,131,88,32,55,194,28,243,120,20,244,193,135,254,116,105,152,180,55,31,241,179,132,169,224,14,238,43,98,219,12,30,38,68,243,203,0,31,73,228,211,128,3,39,118,113,73,139,180,249,36,29,128,16,201,94,60,199,227,97,12,23,115,185,103,8,45,201,3,225,3,45,194,77,226,101,212,163,251,136,246,4,61,36,192,191,191,248,86,232,231,106,211,146,228,145,11,90,153,253,24,204,108,61,143,218,116,196,52,113,109,99,47,14,91,192,202,171,120,213,111,25,38,92,173,236,145,68,147,103,143,41,129,19,93,93,39,155,208,244,35,172,12,150,17,203,245,214,138,73,173,213,85,115,139,26,166,173,65,79,55,26,63,217,104,223,189,85,109,116,99,95,38,129,211,106,159,220,56,3,207,165,219,217,90,217,133,110,42,136,187,111,55,118,253,189,82,219,71,10,171,93,127,117,85,208,41,178,75,235,93,249,215,102,96,59,182,29,253,254,123,100,219,143,216,74,47,94,60,43,239,73,19,11,187,132,12,109,247,210,208,97,136,146,48,164,157,240,126,101,181,18,188,54,152,156,81,77,195,168,2,117,4,113,20,113,197,182,88,225,41,171,227,227,196,74,39,17,27,35,30,104,155,226,217,23,182,17,128,13,201,178,186,53,130,22,153,232,167,55,226,96,39,245,69,63,70,139,25,171,145,28,6,130,178,101,200,77,10,76,60,132,113,104,149,143,144,32,127,30,171,62,134,32,208,211,26,7,183,188,166,78,12,147,185,4,9,82,234,173,228,80,20,150,30,143,34,94,197,221,53,110,194,33,162,47,5,131,6,78,29,57,125,78,198,104,98,137,102,183,32,200,61,59,226,167,134,121,115,40,195,233,78,28,135,5,210,177,212,34,125,241,66,219,40,90,87,203,57,134,153,51,43,11,118,112,195,148,26,249,134,223,71,80,169,100,71,104,186,229,97,229,185,106,152,233,187,103,197,12,1,107,200,116,51,214,147,163,148,248,202,213,66,187,41,217,22,209,106,188,120,81,145,76,131,219,107,228,22,58,114,70,64,95,9,78,232,6,119,197,194,215,237,48,116,238,193,40,160,223,10,199,221,30,175,241,111,17,23,36,2,233,143,166,43,234,53,197,94,12,186,198,45,34,60,103,226,199,182,161,157,176,73,155,30,147,26,187,106,55,137,216,101,44,247,32,251,206,109,215,9,107,93,112,4,251,106,207,36,8,101,160,189,180,120,193,215,234,72,11,3,48,46,183,53,68,30,91,4,131,157,179,151,138,109,148,140,88,2,59,7,115,6,108,11,14,102,18,12,141,110,212,57,88,146,24,114,14,86,80,134,155,115,181,126,61,223,45,192,229,244,251,2,170,202,3,110,106,105,198,38,141,41,252,230,13,176,166,11,132,44,188,47,237,2,133,217,179,181,42,238,154,86,38,63,32,132,212,136,22,145,65,175,87,138,112,180,24,80,116,125,80,109,9,120,57,238,185,209,252,148,34,92,216,91,49,140,171,205,2,241,178,100,254,146,128,47,240,55,176,117,102,214,26,180,22,88,218,174,96,141,178,72,226,162,46,75,163,142,143,116,90,218,118,89,183,34,98,183,92,183,50,112,184,116,183,178,237,92,183,73,172,238,201,78,211,168,222,50,93,166,237,230,58,20,1,159,39,123,147,241,188,101,186,146,45,206,133,53,67,230,120,70,143,212,4,193,163,100,3,53,237,83,147,117,228,104,0,239,23,30,149,75,47,181,123,62,111,92,85,36,155,163,3,99,210,132,231,48,143,73,166,21,99,31,104,162,225,172,149,27,85,14,68,135,69,15,134,118,169,59,221,218,180,63,81,202,17,208,0,31,230,217,26,70,34,132,215,9,254,150,37,180,224,1,170,201,138,65,199,117,26,98,95,27,145,88,215,21,186,45,119,189,130,239,142,157,137,134,98,106,23,254,0,6,137,216,252,183,171,116,124,116,230,156,85,220,234,107,112,94,64,113,81,5,152,237,217,110,77,60,183,162,74,50,200,205,119,174,236,189,127,117,199,62,118,156,241,205,175,10,102,80,41,12,177,230,189,196,121,111,133,124,194,103,80,69,217,143,145,125,133,214,95,99,55,216,83,90,116,55,0,131,49,186,10,174,237,43,7,254,154,46,254,241,225,207,245,110,106,236,39,198,65,98,238,211,56,62,225,148,148,57,3,15,57,158,197,163,48,230,232,129,76,208,43,41,218,56,192,193,57,110,68,23,240,251,195,37,92,68,100,113,229,122,23,34,31,57,56,20,203,119,104,72,144,241,21,167,26,85,152,83,233,161,109,42,66,97,22,131,105,140,54,171,78,65,195,236,102,240,175,60,164,202,139,244,143,14,90,91,40,202,37,193,91,58,124,18,242,254,180,199,115,188,37,102,100,105,212,68,177,58,88,66,128,46,178,143,22,140,51,157,36,134,7,151,210,80,0,9,51,55,31,138,184,230,13,20,137,177,56,235,174,44,54,166,235,98,20,35,82,123,171,29,182,153,73,240,136,230,174,102,199,27,195,77,122,159,133,240,87,137,133,242,116,239,79,153,53,213,226,200,10,64,4,84,71,40,42,50,211,129,191,214,152,173,230,246,147,195,54,138,242,165,164,158,126,240,178,210,47,188,90,109,150,55,89,38,165,85,12,4,131,40,18,137,199,34,89,50,4,24,219,79,68,70,153,179,146,28,36,2,192,8,8,76,21,146,193,211,111,188,0,104,19,131,193,172,142,56,6,242,42,107,153,12,105,172,152,198,180,50,130,18,93,37,125,120,22,135,186,120,245,181,170,26,141,64,75,34,105,196,109,134,24,73,124,104,228,34,126,40,60,40,84,251,139,144,215,118,137,105,13,16,97,224,165,94,249,123,127,181,250,119,75,255,169,212,42,214,143,213,234,235,186,82,32,15,99,231,75,16,54,113,226,153,99,215,167,203,245,107,33,36,225,114,227,218,196,5,111,236,9,238,182,176,0,244,84,243,138,234,83,77,170,131,69,215,48,147,64,27,97,146,95,17,182,116,138,235,192,163,233,235,102,159,196,137,102,115,44,234,171,229,138,223,185,41,194,76,7,232,108,189,9,66,17,105,42,225,8,212,117,192,5,244,227,228,100,151,56,56,10,61,82,24,238,158,224,155,154,212,124,9,147,90,242,193,170,193,42,232,84,65,179,174,227,199,171,70,85,106,82,58,217,9,91,240,16,130,164,1,102,33,175,227,209,156,208,20,207,152,71,201,57,54,87,89,238,160,186,232,7,101,31,99,98,2,88,41,78,194,32,248,0,79,99,72,121,68,206,167,185,130,136,171,83,177,98,113,130,79,30,52,185,166,151,26,122,102,177,150,154,171,130,29,133,145,180,235,216,217,65,173,128,79,233,12,59,104,52,152,9,169,170,175,13,168,163,146,198,153,115,11,22,53,166,39,60,51,154,198,175,34,169,88,230,179,142,193,67,165,229,99,63,86,213,45,49,251,20,16,209,116,60,118,194,123,5,170,35,0,113,237,216,130,249,128,11,162,139,195,239,226,76,39,24,134,62,191,123,143,182,181,28,186,234,51,187,182,134,70,138,230,208,99,151,3,55,28,207,156,16,15,157,242,111,12,73,102,121,52,142,139,170,181,27,6,51,48,149,59,40,4,80,237,209,185,73,200,231,57,214,214,79,226,201,29,61,4,230,247,110,9,183,153,79,140,144,22,219,81,7,36,179,242,32,15,9,121,25,229,169,62,70,201,199,7,93,136,23,51,49,121,104,43,0,216,59,133,128,47,160,139,1,5,7,149,7,122,78,211,144,147,101,169,222,76,206,146,155,184,234,32,31,60,33,6,183,255,71,245,200,237,7,163,58,143,38,184,89,184,51,118,105,43,89,7,234,64,185,71,217,180,117,1,94,89,207,24,242,182,121,161,103,37,25,180,0,116,126,201,146,19,117,105,222,47,58,92,77,140,189,10,234,153,198,251,51,92,226,16,239,125,213,209,107,232,163,211,138,180,146,199,201,97,53,242,185,21,170,26,214,48,185,234,226,74,75,114,152,203,227,53,119,243,88,164,107,241,136,137,56,62,8,3,118,98,233,200,48,215,26,141,31,193,87,148,171,68,252,28,69,58,159,29,96,115,120,248,239,143,177,53,162,58,177,21,201,95,79,58,167,233,10,190,114,65,176,60,83,80,211,14,8,177,146,243,65,170,245,74,230,185,58,41,100,65,237,60,74,90,246,214,2,156,92,9,97,154,163,32,32,244,109,253,217,143,120,14,127,61,7,73,122,74,73,161,95,145,228,80,222,163,143,51,191,72,193,194,218,65,76,75,88,107,116,74,30,92,58,246,86,67,152,13,133,195,246,229,81,123,15,233,217,255,212,98,211,24,1,183,208,242,149,73,43,230,127,49,105,21,235,47,85,90,193,192,99,175,47,84,2,65,169,99,76,203,240,160,64,244,52,3,116,138,139,14,113,246,48,237,140,109,151,61,137,91,200,142,104,226,248,212,136,16,40,23,180,185,88,193,98,16,108,240,228,107,129,107,60,9,156,36,86,25,112,2,238,82,224,130,137,6,27,137,190,204,7,48,18,77,135,43,118,62,114,27,63,148,6,223,27,140,127,45,88,20,90,180,164,226,246,149,137,185,104,117,5,156,111,199,82,155,24,94,167,97,99,185,205,71,45,97,50,61,236,29,151,4,81,29,181,238,18,95,57,215,203,44,140,128,118,208,151,63,86,86,125,132,82,3,88,5,203,233,166,100,17,36,177,160,29,172,80,111,49,92,9,209,86,58,170,102,186,200,97,160,190,211,70,62,57,170,10,151,5,179,71,110,194,4,78,99,236,57,63,18,247,209,30,41,95,146,12,104,240,25,27,230,70,163,90,125,52,94,64,107,155,223,232,151,91,253,192,231,180,78,41,108,85,142,82,0,187,197,35,7,171,115,156,195,250,7,119,12,181,86,80,98,253,99,158,104,165,160,85,158,53,212,169,74,24,168,210,205,83,205,122,199,179,119,254,90,83,13,1,253,1,75,104,10,79,151,135,55,226,122,101,237,71,85,72,39,245,87,170,104,69,211,161,93,36,67,72,164,59,50,207,32,43,236,221,170,190,174,92,121,0,137,68,26,116,87,137,39,253,195,38,232,218,4,184,183,30,38,68,118,221,25,167,172,99,225,54,54,130,131,210,26,68,221,233,36,95,115,77,21,101,142,153,204,149,225,164,207,22,226,81,84,217,14,72,132,10,169,108,36,226,89,60,13,157,89,77,224,133,67,66,201,187,69,219,251,1,115,60,244,181,154,31,50,135,50,202,5,27,245,208,5,33,211,84,227,6,230,45,140,118,147,230,66,98,129,139,48,28,191,202,54,67,27,145,171,215,96,32,233,132,78,224,79,62,168,20,151,114,79,102,112,100,221,102,214,55,144,79,13,58,160,75,208,132,86,200,23,53,169,173,181,47,221,164,146,209,249,214,48,40,64,156,77,137,35,148,5,137,97,5,41,99,95,235,138,9,220,217,140,46,168,168,166,51,34,184,70,190,118,41,216,153,182,74,133,183,114,136,177,176,228,116,209,231,248,117,37,25,231,70,218,152,79,160,241,104,120,124,81,104,219,160,35,128,21,110,50,234,176,44,225,48,250,224,162,243,163,159,215,6,179,185,60,0,144,212,75,181,96,82,84,8,148,88,93,60,228,141,234,63,66,34,171,124,117,217,44,145,103,73,80,36,14,147,112,13,182,156,100,12,184,118,99,215,221,123,5,234,104,181,154,196,72,210,0,138,162,96,225,228,94,210,149,84,57,81,50,46,45,219,195,143,118,56,47,197,106,202,80,200,244,3,196,103,142,81,22,255,204,204,44,124,39,55,7,82,8,12,177,216,90,24,45,148,86,187,194,194,212,219,47,38,178,225,1,181,116,134,117,49,35,80,100,196,192,91,243,121,222,242,148,225,81,227,218,76,194,187,43,137,48,243,203,249,203,47,70,253,236,52,190,38,131,124,201,42,61,148,130,4,8,113,167,75,19,5,56,110,88,62,26,79,226,123,193,51,248,44,128,49,122,239,191,9,122,211,8,111,69,229,247,254,62,184,90,120,15,252,137,223,134,122,16,176,54,179,62,112,178,166,178,135,134,88,226,188,230,190,40,98,232,131,223,122,60,129,1,38,188,242,108,5,35,96,195,224,228,46,80,75,73,164,187,116,232,74,166,164,124,161,16,138,21,138,167,192,249,133,239,159,24,213,133,140,34,22,120,116,75,177,240,114,75,152,75,43,233,82,142,154,35,6,81,176,69,217,175,252,142,50,102,197,3,104,54,61,162,218,116,193,29,9,124,49,120,237,126,63,55,28,113,33,142,142,110,73,225,161,90,122,255,134,156,73,243,145,76,204,121,121,180,121,158,93,176,72,35,230,26,219,226,178,70,57,199,70,65,24,147,101,222,148,81,239,82,30,254,191,199,180,199,253,63,5,199,202,37,32,177,80,181,43,127,237,36,47,217,209,23,88,132,56,151,11,34,14,188,109,102,178,44,142,1,220,138,108,161,100,241,69,176,200,27,138,35,54,75,71,67,70,208,255,187,210,184,187,114,106,131,118,237,77,163,246,234,250,97,205,220,156,255,126,37,47,183,230,213,31,234,213,215,149,4,66,232,170,177,135,145,88,190,103,111,111,109,109,108,189,174,100,86,87,48,131,3,93,128,102,238,177,200,103,237,114,60,110,129,206,161,224,97,178,33,164,113,215,192,207,25,226,78,180,198,221,27,248,103,224,112,85,191,71,27,115,61,217,196,78,174,50,51,84,156,18,12,14,230,52,241,46,125,231,22,252,197,219,90,236,116,35,225,68,210,70,237,75,188,5,31,55,176,149,89,178,43,79,127,45,79,138,33,175,53,153,73,43,171,1,42,253,116,154,173,32,43,57,195,130,55,13,189,162,109,192,45,181,31,71,55,18,176,115,116,70,101,227,90,115,133,84,129,146,86,63,29,158,39,77,173,96,27,208,242,20,188,91,209,156,54,49,176,49,233,97,102,48,102,184,233,229,73,164,115,13,193,52,193,179,234,64,177,11,26,40,183,92,98,47,218,55,210,147,102,75,179,224,192,145,198,176,124,23,35,150,240,94,250,153,4,173,179,244,3,170,152,232,233,32,122,228,174,155,78,26,64,128,58,148,51,151,116,190,26,103,8,45,253,125,51,182,112,107,19,230,136,229,48,74,191,12,40,41,140,151,53,148,9,162,93,188,165,129,150,146,196,116,210,184,129,89,150,176,100,227,87,34,64,195,188,22,93,9,198,204,202,105,225,179,50,61,49,145,45,76,128,98,42,254,2,132,205,111,129,42,29,51,2,80,232,142,22,133,228,246,234,234,110,174,192,23,93,131,180,211,54,192,253,83,128,235,67,144,110,107,250,14,240,225,125,110,179,105,2,104,105,16,76,44,247,192,128,171,220,2,204,0,85,249,17,15,243,106,51,155,89,75,135,74,151,231,44,231,49,195,180,101,125,244,97,114,56,115,49,121,147,72,82,181,116,238,149,78,153,194,12,23,115,184,232,213,69,53,250,108,33,211,190,94,168,121,52,114,101,88,135,34,115,77,222,177,138,58,165,219,155,162,106,190,36,154,118,199,110,92,156,165,165,126,103,49,147,201,26,57,81,206,203,171,22,243,162,48,0,161,78,168,119,236,60,4,60,116,193,61,248,141,139,228,211,234,46,29,252,190,56,249,130,95,137,245,179,235,100,5,54,151,190,234,84,95,59,214,100,26,141,18,213,218,76,222,177,175,64,11,139,167,215,218,83,249,136,242,49,50,135,205,52,175,174,145,61,237,31,44,224,24,164,224,67,113,178,98,29,19,212,62,175,228,115,75,192,227,214,182,82,40,156,197,192,82,216,10,126,43,6,6,8,12,105,221,148,120,247,200,158,37,3,149,154,1,101,244,46,51,159,22,242,216,66,80,180,47,168,46,92,43,134,106,123,46,217,145,229,145,111,205,230,164,179,76,105,197,207,29,162,175,29,7,1,10,185,202,3,157,66,143,121,192,77,3,195,228,38,101,3,231,60,223,68,66,27,98,251,132,227,130,24,104,10,95,123,142,110,183,140,58,8,43,78,69,178,158,203,227,7,50,97,167,210,21,250,106,38,169,188,82,18,172,144,17,138,63,246,169,252,25,48,114,30,118,17,158,66,50,78,186,151,14,96,92,148,27,117,5,115,47,126,241,226,89,102,51,215,139,23,153,109,142,37,62,220,31,247,3,2,233,39,25,208,106,76,13,200,46,134,81,68,54,186,60,158,25,76,2,221,245,80,124,152,125,44,95,17,159,85,135,34,185,115,66,168,30,113,99,228,234,202,237,190,89,115,175,23,120,158,51,137,146,199,78,8,195,99,39,9,242,73,113,43,235,21,134,53,60,122,207,104,137,133,94,68,192,29,210,178,130,244,118,88,166,58,205,76,104,206,104,125,151,226,100,239,6,3,171,54,139,163,72,232,151,166,110,221,104,105,219,2,132,65,75,123,79,50,228,21,8,178,34,194,5,59,31,203,225,199,104,101,13,234,68,207,100,41,43,196,94,150,172,121,249,220,82,178,32,177,182,115,77,83,124,87,120,22,116,249,132,199,77,139,131,36,40,5,9,91,217,101,201,132,126,105,103,170,181,36,60,169,245,78,95,81,48,242,53,202,249,40,121,159,129,133,163,177,32,102,157,212,64,129,79,130,201,116,98,139,51,60,196,67,126,7,176,244,57,160,38,14,242,104,137,207,66,100,134,190,7,211,46,214,198,221,209,71,37,1,8,63,151,149,31,20,130,189,38,190,88,188,212,200,100,94,80,195,211,22,183,25,130,9,252,34,14,34,193,193,35,183,19,112,220,91,87,204,197,226,56,210,234,137,92,151,145,158,162,174,52,150,0,79,85,151,128,145,98,161,195,109,164,106,89,192,64,73,151,203,246,80,75,18,129,100,79,162,125,246,70,62,94,208,15,46,219,23,54,211,149,118,35,106,214,242,19,97,159,30,167,173,163,187,185,144,71,133,149,75,95,61,203,240,109,250,88,164,50,22,62,138,183,36,15,183,232,51,96,123,221,2,27,118,19,30,212,186,194,175,244,45,197,147,79,125,155,175,85,242,241,181,50,138,136,43,177,139,78,252,5,169,180,64,105,16,160,137,135,174,215,193,221,195,84,154,166,160,74,101,92,195,162,188,148,172,69,227,218,6,98,148,122,199,89,205,95,222,150,209,18,225,38,118,6,70,176,242,154,149,11,152,190,145,52,66,102,9,158,147,198,161,131,3,10,78,137,51,68,45,203,34,65,32,252,183,12,250,217,75,29,191,165,145,56,238,231,225,23,28,164,160,63,238,103,5,147,150,208,155,74,167,69,200,97,227,25,188,142,104,191,246,104,218,197,221,218,5,164,138,32,111,102,17,209,115,110,169,155,204,206,114,193,211,132,91,175,23,155,225,176,59,195,63,29,188,25,76,227,198,206,43,250,121,181,150,165,131,140,110,27,45,178,166,18,100,22,39,120,168,23,22,164,120,148,231,97,200,83,101,212,198,212,214,226,189,168,106,17,232,169,141,168,107,48,171,196,218,180,157,36,23,180,216,90,130,192,99,96,124,143,254,215,161,183,245,127,91,111,192,188,108,227,223,214,27,240,29,219,92,170,55,150,225,201,148,3,5,195,125,15,88,182,0,150,173,63,8,44,219,0,203,246,31,4,150,151,0,203,203,63,8,44,59,0,203,206,191,141,59,27,208,91,219,243,146,254,50,26,241,17,73,170,9,61,10,79,182,62,242,113,0,106,6,87,239,202,228,158,102,98,44,216,159,46,233,91,176,46,178,222,216,2,91,67,192,211,194,32,64,170,66,30,177,129,165,239,179,208,16,38,225,44,16,67,219,73,167,24,234,130,236,2,30,30,56,152,177,110,245,234,160,49,178,181,15,14,46,23,86,150,42,38,177,113,18,9,159,237,109,245,137,54,10,47,44,172,156,176,173,86,95,156,144,200,234,108,127,231,209,247,72,249,229,222,123,181,134,239,173,63,250,222,122,163,248,222,122,35,111,163,61,198,135,139,172,146,181,245,71,116,124,58,87,51,218,28,126,117,141,78,192,45,219,213,104,171,245,110,10,12,7,191,95,11,234,118,46,24,144,38,203,45,44,144,89,116,229,254,125,49,23,56,101,118,249,83,160,232,2,144,23,83,77,51,127,190,142,74,23,73,70,249,55,19,75,72,58,21,58,161,108,61,166,229,233,177,66,234,186,0,93,212,196,15,237,162,172,203,62,115,238,192,242,105,228,158,42,139,168,209,48,190,141,78,146,169,74,77,197,175,32,25,37,88,178,203,52,111,253,155,41,167,61,165,163,76,107,148,101,159,48,219,114,132,213,19,232,255,53,148,253,186,89,14,180,72,211,238,191,137,52,212,204,114,200,139,44,254,239,141,246,87,49,131,56,224,245,219,17,77,21,93,146,50,154,58,25,110,214,144,72,114,90,89,102,87,75,210,143,70,159,236,170,183,212,216,69,87,66,8,246,242,185,195,92,223,3,47,123,161,128,206,11,103,108,233,177,64,109,222,160,208,83,100,69,31,242,9,37,26,116,104,74,24,173,207,248,147,218,5,66,17,17,133,23,246,135,38,2,52,250,142,35,27,22,186,71,183,246,177,190,125,218,169,36,76,140,51,188,206,117,190,44,154,114,87,211,99,93,77,28,55,124,58,232,57,141,100,208,243,28,170,127,35,48,242,144,161,71,96,153,250,203,64,163,31,53,196,62,209,43,249,177,65,35,97,2,191,147,199,121,251,107,120,46,203,99,218,155,72,28,156,241,240,87,248,238,95,199,129,120,45,15,7,127,108,148,228,73,208,146,39,50,218,189,140,70,4,79,89,56,95,127,145,22,232,12,105,23,75,179,225,159,7,157,209,145,53,248,5,156,60,83,243,187,120,89,248,21,199,229,44,114,93,126,169,94,114,138,34,157,155,255,140,68,41,14,243,191,216,71,193,65,120,60,50,255,132,87,194,178,57,208,89,75,249,219,136,82,22,217,250,31,226,127,113,38,60,126,196,165,35,209,251,158,51,224,2,27,254,142,83,224,17,232,137,62,255,14,238,47,56,74,248,54,69,21,75,147,7,52,166,112,60,202,119,81,235,76,125,215,241,130,97,65,90,58,94,109,113,73,105,56,92,20,45,181,158,42,118,106,8,42,130,203,50,118,147,247,141,214,11,58,142,123,55,37,203,104,61,219,131,28,84,245,245,105,177,127,2,173,163,245,242,73,160,242,114,158,180,151,152,110,51,97,66,71,9,61,69,162,71,107,47,166,79,211,164,121,68,8,209,94,140,200,183,240,156,71,184,76,2,223,135,123,117,184,195,39,248,137,210,243,32,140,147,7,231,97,128,1,26,143,201,253,240,73,65,186,44,155,87,245,10,28,220,144,66,172,136,48,230,118,167,44,173,238,133,223,141,253,225,145,125,120,37,16,64,122,149,33,175,246,176,96,85,172,131,191,136,59,252,34,93,158,242,51,211,197,134,23,126,55,154,236,230,56,55,183,162,245,231,99,217,116,173,122,41,94,45,82,105,25,99,159,24,87,172,194,210,138,153,78,217,92,208,80,100,43,45,178,42,147,157,154,23,162,218,215,14,114,18,199,194,3,91,25,253,173,205,156,144,78,31,16,119,116,170,172,92,139,204,236,199,90,106,40,10,175,201,13,96,37,195,177,217,186,20,21,153,60,84,156,85,232,75,64,152,197,172,62,205,90,133,81,217,212,152,49,119,250,248,34,36,211,149,232,252,194,239,159,143,127,11,43,217,95,37,114,75,248,192,32,51,190,219,106,123,152,168,200,98,252,102,138,195,146,248,96,93,75,12,145,135,102,164,188,108,209,246,219,150,88,13,199,207,56,220,7,83,204,127,77,211,65,88,151,15,240,83,7,211,201,48,164,83,166,44,134,11,208,23,193,152,139,79,62,136,2,252,94,145,248,184,149,248,234,131,216,36,27,153,242,19,58,248,169,159,94,16,134,211,137,248,152,37,37,60,209,46,73,139,29,15,196,215,18,70,148,208,6,175,144,222,22,237,152,192,89,224,42,35,208,242,61,65,53,83,114,93,20,139,239,66,113,182,0,5,139,252,155,101,166,19,145,241,80,82,40,233,136,117,93,223,193,20,71,245,25,97,158,82,86,157,171,161,157,169,241,244,105,26,160,115,221,248,221,180,203,212,3,134,187,139,145,210,22,75,211,41,28,230,107,167,183,116,239,25,165,243,209,183,62,196,169,45,70,33,245,194,96,130,241,4,198,196,17,167,200,10,209,84,81,8,49,185,87,159,148,20,68,165,239,204,56,228,251,78,232,3,44,216,32,17,176,27,56,97,255,25,14,117,194,170,226,147,57,174,175,125,68,39,57,226,181,71,223,70,146,137,152,140,78,224,40,25,89,32,27,113,7,129,131,29,136,38,63,93,236,235,227,148,17,190,117,53,18,6,19,27,202,241,100,62,76,117,224,126,79,204,81,145,63,238,132,49,73,214,26,78,206,236,156,209,115,229,114,235,65,90,145,84,250,186,217,169,100,249,36,116,233,216,150,214,62,157,62,195,50,242,159,210,248,100,156,71,92,59,32,249,39,152,189,214,197,188,37,74,175,64,113,69,65,229,166,31,248,188,184,88,191,100,198,54,230,17,98,198,27,104,47,121,245,109,8,41,229,4,194,136,184,61,25,225,82,205,182,20,6,82,164,235,202,173,32,206,6,192,22,79,202,216,252,18,93,185,180,61,64,73,172,69,52,30,209,160,122,154,211,159,91,169,44,171,75,144,34,249,35,137,212,123,163,144,84,189,220,59,152,218,212,186,90,207,159,211,149,167,118,246,60,35,131,26,60,161,103,197,246,84,228,88,68,72,31,111,73,101,186,8,150,75,154,42,174,149,38,199,126,9,170,194,21,250,23,226,140,172,164,15,241,218,71,33,134,217,33,209,111,185,166,146,239,54,119,156,120,65,115,103,1,101,195,201,246,38,33,127,172,61,57,52,244,21,22,145,70,228,44,168,142,167,87,216,226,60,44,237,125,220,33,129,7,97,25,173,95,92,80,27,192,104,66,199,100,115,77,75,91,69,34,103,15,221,74,117,96,54,217,238,105,27,245,127,122,146,151,231,244,253,249,38,187,72,82,100,47,216,71,105,255,124,173,75,180,55,218,72,230,157,248,46,125,174,105,173,193,141,214,215,88,146,139,53,103,190,225,44,103,46,4,168,4,197,141,188,133,144,130,245,191,206,66,16,68,251,223,96,34,124,87,203,224,15,39,58,202,210,174,255,124,130,67,164,182,39,217,228,79,11,139,52,140,46,72,147,91,242,124,244,59,25,196,68,99,166,157,133,147,11,248,162,247,6,115,82,204,113,39,124,100,249,238,27,59,32,63,50,115,238,61,176,5,186,150,248,1,76,38,54,144,149,45,138,253,97,184,179,46,195,131,168,223,91,255,31,125,250,64,3,255,181,0,0}; \ No newline at end of file +#define index_html_gz_len 13204 +static const char index_html_gz[] PROGMEM = {31,139,8,0,0,0,0,0,0,10,237,125,105,119,219,70,178,232,247,123,206,251,15,109,36,99,145,17,8,82,171,101,74,160,47,181,216,214,196,139,108,201,201,100,20,61,29,144,108,146,176,64,128,3,128,90,162,240,191,223,170,234,5,141,133,18,237,241,204,205,201,29,231,68,4,122,173,174,174,174,173,171,27,123,79,6,81,63,189,155,114,54,78,39,65,103,15,255,178,192,11,71,174,197,67,11,222,185,55,232,236,77,120,234,177,254,216,139,19,158,186,214,44,29,54,118,32,47,245,211,128,119,222,250,111,252,209,56,101,175,103,189,189,166,72,218,11,252,240,138,197,60,112,173,36,189,11,120,50,230,60,181,216,56,230,67,215,26,167,233,52,105,55,155,19,239,182,63,8,157,94,20,165,73,26,123,83,124,233,71,147,166,78,104,110,56,27,206,179,102,63,73,178,52,103,226,67,169,36,177,24,245,228,90,19,207,71,48,169,195,124,251,35,63,197,38,225,103,60,235,57,126,148,53,210,72,163,209,40,224,205,117,7,254,203,183,47,179,178,110,138,131,168,236,10,250,249,156,56,253,32,154,13,134,129,23,115,26,135,247,217,187,109,6,126,207,108,61,9,252,1,143,155,207,157,103,78,171,208,177,200,250,182,29,39,60,224,253,212,255,141,59,159,147,102,203,89,91,119,182,169,215,44,93,247,191,241,47,27,50,245,213,92,195,222,55,139,99,166,188,135,122,38,194,11,189,9,204,244,181,207,111,166,81,12,116,212,143,194,148,135,64,136,55,254,32,29,187,3,126,237,247,121,131,94,108,63,244,83,223,11,26,73,223,3,242,88,131,38,158,52,26,231,254,144,5,41,59,62,98,207,47,58,255,239,191,24,252,219,75,250,177,63,77,89,18,247,151,30,20,174,141,173,100,236,95,3,101,62,115,54,178,119,64,47,116,212,20,77,82,7,123,79,206,121,56,240,135,23,141,70,103,143,70,212,113,112,41,241,184,17,71,55,247,189,40,198,199,94,148,166,209,164,189,54,189,101,73,4,243,207,190,235,247,251,243,192,235,241,224,126,224,39,211,192,187,107,247,130,168,127,53,119,98,111,224,71,141,104,154,250,81,120,63,245,6,3,63,28,181,91,108,107,122,187,219,159,197,73,20,183,167,145,15,104,137,231,8,244,196,11,7,141,222,12,90,15,147,251,192,79,210,6,193,208,14,163,144,239,78,188,120,228,135,237,214,174,110,166,84,135,5,190,6,192,15,97,250,121,131,224,144,117,27,49,174,248,246,26,159,236,70,215,60,30,6,209,77,219,155,165,209,220,25,70,241,164,1,115,19,223,221,171,110,88,139,173,183,96,132,45,51,151,137,103,156,201,56,10,238,105,238,218,235,45,62,201,21,202,99,194,4,68,32,9,198,147,214,28,30,199,81,92,103,226,183,225,135,195,72,87,193,241,206,205,140,126,20,0,170,98,62,216,29,66,215,141,4,214,64,219,121,134,221,102,133,218,61,14,48,240,123,73,102,237,149,218,74,46,219,27,2,154,179,220,58,228,202,169,237,165,161,26,54,14,121,238,140,188,148,223,120,119,13,64,52,101,2,166,188,180,77,216,155,127,39,91,96,206,32,142,166,131,232,6,230,53,74,124,156,224,182,164,226,2,118,203,53,26,19,30,206,204,201,18,200,249,14,86,215,112,232,247,27,73,232,15,135,121,108,124,71,105,124,208,144,101,0,224,219,198,152,211,132,110,181,140,25,109,220,201,57,85,157,61,52,29,142,120,91,144,121,51,246,83,222,72,249,100,218,152,250,253,43,192,158,236,112,29,250,235,121,253,171,81,28,205,194,65,27,235,120,113,99,132,196,14,227,172,165,17,35,92,217,223,121,219,131,181,225,144,181,236,239,134,240,179,213,250,11,62,120,173,86,139,173,181,90,127,169,239,86,18,171,162,239,13,65,125,227,25,175,232,94,144,30,182,178,91,88,114,89,121,152,247,176,170,22,240,152,126,13,171,178,6,219,224,147,5,96,228,215,231,82,227,5,2,21,99,133,241,109,59,219,219,219,207,96,192,45,120,219,0,153,8,255,232,77,226,161,213,26,178,109,93,104,8,111,59,170,16,54,67,248,17,131,185,246,2,248,43,97,148,60,200,100,62,128,78,181,192,3,62,76,219,206,150,30,41,142,217,24,126,245,52,79,131,89,210,0,102,14,127,113,128,211,251,28,249,154,152,238,7,48,238,54,176,191,113,17,233,197,54,152,224,72,237,161,31,3,23,139,134,13,212,85,242,252,83,112,163,6,34,113,150,0,91,147,153,105,52,45,230,136,229,135,99,91,182,219,192,91,216,43,54,83,217,105,62,227,177,142,28,82,100,84,219,18,71,72,176,170,73,193,178,80,50,168,20,193,203,105,210,52,141,131,32,96,234,255,103,32,20,52,147,167,9,146,83,183,230,108,62,175,66,194,48,224,183,187,94,224,143,194,6,44,212,73,210,238,115,33,74,134,62,15,6,80,59,152,86,139,131,140,137,174,57,235,72,45,69,242,1,22,24,220,77,199,62,112,46,39,153,194,50,2,88,239,189,208,159,120,196,231,48,137,173,37,12,248,42,178,60,206,196,138,216,109,220,240,222,149,159,54,242,37,215,43,138,206,81,8,103,140,19,52,8,40,127,13,66,14,160,80,163,6,138,155,247,162,129,18,73,74,236,174,183,144,71,15,65,27,49,214,54,165,253,247,132,15,124,143,69,97,112,199,64,168,115,30,50,16,142,172,134,141,210,12,177,246,6,50,248,250,125,85,179,91,216,196,50,109,108,238,44,108,163,181,108,27,207,182,119,22,180,177,182,181,179,100,27,207,159,175,47,106,99,109,27,219,112,38,209,0,116,42,44,192,156,4,85,199,40,108,228,232,182,172,201,112,206,21,113,170,92,7,88,135,148,63,44,163,139,6,113,35,73,229,219,195,237,138,18,9,44,43,85,98,184,189,61,159,5,142,39,128,88,164,227,24,170,77,190,228,34,205,166,162,28,105,23,146,241,244,199,126,48,168,223,151,200,251,191,175,248,221,48,6,253,52,97,72,161,247,195,56,154,220,131,100,13,19,84,99,218,164,133,214,214,234,44,142,82,80,5,106,173,250,60,141,30,200,223,216,110,13,248,168,14,179,166,86,64,190,253,117,209,129,202,204,26,202,181,191,48,91,55,239,104,11,160,49,128,167,148,231,52,19,167,223,200,242,145,33,100,186,28,168,124,115,80,116,73,161,53,205,130,129,151,122,109,88,171,35,222,188,109,224,156,129,152,75,248,246,166,221,237,118,247,187,221,163,238,17,252,197,223,131,238,126,180,255,161,219,125,57,130,215,3,252,211,253,128,127,142,187,42,95,253,59,234,230,255,229,223,155,27,59,239,186,251,135,171,7,55,135,205,207,31,146,238,243,233,167,247,221,253,143,179,38,188,191,254,112,211,93,29,157,196,221,147,205,183,189,110,247,239,59,195,157,238,193,109,239,57,188,191,186,186,57,248,185,217,141,186,135,191,124,94,237,158,108,61,63,248,112,248,236,101,247,170,123,208,108,134,221,191,254,242,99,243,67,247,67,243,102,179,219,31,93,55,187,39,207,214,14,110,14,78,15,79,118,186,171,221,171,95,224,61,134,97,188,107,174,182,186,205,219,163,126,247,100,45,62,184,233,190,109,70,87,221,195,225,234,235,238,113,243,25,12,233,77,115,43,233,30,158,60,223,234,118,91,159,0,158,205,230,8,222,255,22,175,2,38,222,55,71,135,193,65,19,218,187,110,189,235,118,55,154,71,55,251,155,71,0,223,206,77,252,172,123,210,58,230,208,254,179,147,205,110,111,184,243,166,219,157,253,181,121,115,240,211,225,53,148,31,247,95,117,15,94,175,30,142,246,127,107,222,36,48,190,103,205,238,254,217,234,223,63,116,223,30,92,183,186,222,40,108,118,255,122,214,60,252,112,184,122,116,130,253,63,75,186,239,135,205,195,238,97,243,240,22,224,29,173,195,76,108,190,107,142,246,91,205,209,77,247,240,239,191,65,253,55,128,47,128,127,255,151,238,234,205,63,222,118,255,243,239,63,255,254,243,239,255,248,63,224,115,185,127,159,18,248,179,250,238,166,219,125,245,219,177,78,61,254,220,234,254,56,238,118,95,143,119,100,74,132,127,222,209,227,254,13,254,109,194,255,70,13,250,55,233,34,203,63,76,62,27,105,167,111,110,22,128,178,255,54,124,151,79,137,223,30,222,252,244,230,99,94,22,28,30,126,26,44,26,205,95,15,141,178,7,159,54,241,231,199,195,252,16,95,30,96,153,62,62,110,139,20,170,243,55,9,4,202,164,159,223,140,186,94,55,131,243,205,143,32,181,222,127,192,118,54,243,192,236,95,157,232,231,147,45,192,193,225,228,239,80,111,231,10,240,244,230,26,224,60,24,174,66,221,223,154,192,110,79,154,136,163,225,42,252,121,221,28,201,119,144,54,80,62,108,94,201,242,135,119,103,240,190,53,233,203,246,186,174,116,148,161,152,181,24,90,38,240,108,136,94,116,69,9,183,45,42,108,157,74,79,23,58,181,156,81,20,141,192,16,155,250,73,193,209,245,249,31,51,30,223,53,55,156,53,103,77,190,144,147,46,231,230,82,237,10,103,113,202,111,211,230,103,239,218,19,169,86,103,56,11,73,161,98,201,108,138,110,187,228,146,220,101,151,160,203,197,0,107,173,126,143,78,162,152,167,179,56,180,64,3,243,130,83,145,99,129,73,112,227,135,131,232,230,233,211,112,22,4,79,92,87,188,58,102,169,121,223,75,251,227,90,90,151,77,60,89,155,207,117,151,33,191,57,27,243,9,127,19,121,3,62,128,174,116,78,10,47,215,94,204,82,247,251,154,245,93,47,13,95,147,222,111,213,209,1,89,179,198,242,205,78,220,212,137,57,104,137,125,94,179,166,183,150,109,89,245,93,107,29,244,100,235,137,203,159,62,85,143,233,211,167,156,254,214,184,155,218,208,230,34,3,175,208,131,205,235,80,122,197,15,167,179,244,92,248,55,19,80,209,102,137,117,177,82,207,188,178,103,228,141,174,89,3,14,175,209,157,85,206,82,134,75,50,175,215,237,181,150,235,122,47,200,184,62,70,3,14,12,254,90,84,111,123,171,171,115,28,52,127,100,208,145,155,240,84,87,76,237,173,86,171,110,123,110,107,254,189,51,226,233,95,79,223,191,171,105,2,34,56,110,112,14,4,237,76,125,32,151,207,9,80,159,173,144,141,147,35,186,77,157,20,167,35,129,30,16,2,122,105,160,202,14,72,229,232,240,59,242,96,46,179,122,128,29,170,233,185,43,160,100,118,246,60,169,101,126,103,177,62,88,228,137,107,245,199,64,117,92,104,252,228,4,35,61,213,98,179,56,112,87,86,83,28,216,193,32,92,181,58,22,188,32,126,87,173,189,166,7,196,11,205,89,187,145,227,77,167,60,28,212,188,250,28,254,219,197,190,52,153,74,2,117,23,209,237,174,63,172,21,11,75,162,194,113,185,38,149,138,113,239,90,179,112,192,193,108,229,3,36,25,88,176,209,80,20,6,186,129,5,11,22,19,7,226,30,213,172,3,28,22,216,45,34,151,165,17,3,248,241,145,168,5,245,237,115,115,223,3,105,197,75,211,24,166,5,240,99,217,229,174,47,1,33,64,205,60,61,243,39,60,154,165,25,146,235,247,198,84,160,99,15,200,1,71,90,179,104,233,176,182,238,122,110,175,241,141,186,93,92,84,245,249,231,15,200,25,106,230,124,167,53,11,153,14,180,5,239,86,63,240,251,87,176,118,156,5,211,101,27,208,72,74,169,165,99,63,145,144,212,119,211,71,65,4,204,228,48,40,114,96,192,2,121,92,76,110,228,90,48,235,174,181,175,86,15,171,29,242,161,55,11,210,186,229,186,46,127,241,141,118,165,218,10,126,49,43,128,124,228,37,5,98,129,73,47,79,148,203,23,204,158,27,21,70,248,233,227,27,73,30,56,126,200,77,151,160,140,168,60,127,72,249,37,102,158,19,18,203,226,226,115,17,21,149,114,34,215,244,178,155,115,159,23,236,205,61,222,195,35,123,57,184,34,227,164,31,197,180,55,182,230,236,56,27,70,90,227,155,244,177,96,223,175,114,207,239,159,238,172,106,175,15,158,64,170,132,3,47,136,66,110,148,248,214,163,203,109,241,125,94,176,195,247,85,202,67,60,234,157,69,175,147,160,230,217,220,142,235,247,94,211,93,223,218,178,185,248,137,233,71,176,111,59,181,251,238,91,47,29,59,64,182,178,184,61,150,41,126,168,82,66,183,214,95,29,215,155,235,200,198,251,174,59,174,131,168,119,91,187,60,72,56,241,160,158,219,111,140,119,147,27,159,212,11,55,236,56,91,47,122,205,218,122,3,146,235,109,120,194,234,118,191,126,223,247,18,206,188,118,226,214,120,35,174,55,123,171,53,190,23,191,216,110,183,234,187,189,152,123,87,187,84,128,99,129,184,225,97,129,117,51,35,198,12,175,193,49,99,115,158,52,221,237,185,208,100,238,199,237,196,78,218,169,29,180,67,67,167,249,248,106,63,141,94,159,254,164,48,177,6,124,203,139,71,51,224,166,105,226,4,60,28,165,99,82,67,60,103,100,199,240,183,7,162,219,115,226,186,66,79,9,57,253,18,114,198,110,218,232,3,138,64,141,112,211,23,173,246,184,153,218,61,55,109,34,146,21,70,228,192,251,0,126,203,28,15,98,2,16,177,58,254,65,227,193,198,81,253,48,46,162,3,176,177,186,254,195,184,34,23,113,2,40,89,221,212,185,121,156,132,246,117,187,55,159,47,75,69,223,231,165,221,32,234,19,182,164,108,34,145,4,194,169,61,244,65,61,43,139,162,239,5,43,7,196,113,7,245,161,186,86,8,155,191,254,218,28,217,86,211,50,146,156,31,126,109,54,73,75,4,222,29,251,163,17,7,222,139,45,139,21,96,217,231,233,5,48,92,219,4,3,6,62,184,43,72,100,1,141,0,209,172,174,75,113,59,85,202,145,132,208,153,194,226,4,34,0,229,147,244,73,161,119,66,27,160,113,12,160,69,196,11,136,33,223,77,119,61,77,40,30,141,201,39,41,32,53,160,79,239,142,207,46,79,186,31,187,111,79,221,123,160,138,183,126,204,7,73,123,109,107,195,6,170,145,111,27,207,90,248,182,79,158,194,144,39,73,27,136,99,110,127,58,190,60,235,238,159,186,231,247,169,55,106,91,169,215,107,220,248,67,31,224,142,125,80,180,130,187,182,245,51,190,207,237,172,0,8,105,0,211,40,113,74,9,102,145,0,52,38,163,192,155,163,195,92,54,109,138,155,5,62,82,130,89,100,242,143,52,53,75,188,253,112,118,150,43,64,78,82,218,60,72,204,114,103,70,242,252,2,199,247,242,248,232,205,161,30,161,55,0,12,93,206,18,30,163,118,105,214,236,98,14,203,114,112,247,164,109,125,146,239,12,180,93,6,114,156,180,60,63,4,37,5,231,144,221,240,222,20,109,31,27,181,195,54,216,2,49,228,195,155,215,51,144,169,192,22,125,79,65,17,190,137,226,28,130,78,116,154,232,85,189,127,139,94,199,81,146,22,199,250,90,167,137,254,78,121,48,108,192,154,0,117,135,15,152,170,129,170,88,2,53,24,202,161,17,24,119,233,152,29,30,188,62,1,51,22,108,75,176,2,150,234,31,95,46,209,72,242,251,151,126,158,110,40,145,29,159,176,238,96,16,3,77,106,112,116,134,39,50,88,13,108,163,107,206,122,192,49,174,16,44,152,38,118,248,250,224,164,254,53,48,92,134,60,157,120,201,85,53,44,239,84,166,128,69,190,170,78,9,9,186,240,87,245,46,131,11,170,123,127,37,50,139,24,49,80,33,1,241,18,38,27,98,55,99,220,28,202,160,98,62,238,58,112,191,239,15,113,29,46,13,226,36,26,240,252,202,127,233,179,183,148,40,160,56,139,239,160,111,164,197,87,12,11,51,127,200,238,162,217,74,204,217,216,187,198,116,24,101,207,15,252,244,142,77,227,168,23,240,73,162,250,23,33,48,151,114,155,6,52,91,122,79,218,247,0,206,190,101,3,12,175,44,59,4,124,3,76,213,48,246,249,229,212,15,77,0,15,142,88,147,157,252,120,198,40,93,46,29,88,196,32,118,143,78,79,118,214,183,183,17,85,98,21,173,28,28,173,176,26,62,189,251,248,114,125,243,77,107,141,209,190,254,16,196,64,157,97,1,104,72,150,88,57,121,179,182,182,253,172,249,230,108,231,121,171,181,98,20,92,136,205,36,199,3,251,73,88,130,245,244,221,114,96,158,190,91,89,178,23,32,7,158,22,251,249,120,116,122,180,28,66,168,228,178,125,1,71,47,246,4,92,189,216,143,164,77,108,30,115,133,107,132,201,125,59,86,107,185,240,232,1,89,12,234,187,44,228,35,218,242,5,236,94,131,250,142,59,101,254,40,244,2,86,139,57,134,53,33,227,105,172,83,91,184,189,23,121,192,14,161,209,101,167,96,234,245,175,0,59,192,212,184,151,38,121,102,139,57,76,231,72,218,30,115,22,206,38,61,14,221,129,149,239,227,214,29,198,117,80,41,246,241,37,19,13,38,200,17,201,72,237,205,130,94,178,16,152,56,39,209,242,192,92,78,121,124,25,68,209,116,49,84,12,138,48,81,68,178,33,13,154,42,161,152,179,143,107,31,23,101,192,217,40,114,24,123,13,34,30,138,82,212,72,194,38,220,11,97,177,194,18,77,199,160,95,140,198,160,105,216,24,38,193,2,228,38,19,176,165,253,20,24,28,52,224,44,57,26,52,52,228,88,46,97,85,128,229,155,147,45,103,103,39,10,109,42,87,140,225,45,246,53,13,252,190,152,119,145,137,180,153,71,15,77,185,20,49,24,55,128,33,85,40,150,122,119,104,63,51,106,191,123,114,236,176,79,135,244,128,174,84,104,51,8,238,160,86,159,67,211,64,114,51,209,15,87,211,102,179,36,18,104,80,189,36,227,104,22,64,171,60,91,18,216,244,146,162,124,150,70,0,47,16,120,156,226,116,250,81,78,158,119,33,187,33,179,153,202,22,72,192,44,140,141,80,0,139,50,56,48,17,15,201,56,172,134,59,131,20,209,39,153,210,62,50,7,134,238,243,1,12,28,214,88,139,32,86,235,105,201,69,1,197,251,24,220,115,119,73,17,153,6,196,135,42,135,137,28,169,253,0,134,241,29,137,13,253,126,32,103,100,175,170,176,92,20,81,232,48,233,153,33,202,196,241,36,168,66,80,109,66,48,76,159,10,57,22,35,133,57,217,220,217,121,254,92,141,7,170,201,225,44,55,7,168,34,94,130,122,6,128,20,53,69,166,146,197,48,14,35,116,174,32,147,55,228,40,160,150,138,246,226,232,10,140,121,246,158,4,18,205,138,64,245,29,172,43,2,159,100,126,141,223,122,147,105,0,178,98,114,135,245,100,181,62,70,79,236,236,108,46,4,153,20,217,28,200,105,4,228,10,106,96,10,66,37,44,65,78,185,76,231,42,125,144,94,9,249,89,169,196,152,22,156,128,35,1,97,27,210,0,203,73,179,45,240,124,233,15,244,35,2,217,108,147,161,1,201,14,59,229,156,125,60,234,30,190,61,162,182,135,179,56,69,222,49,224,169,231,7,139,185,91,121,80,179,233,0,86,219,35,99,19,133,30,30,34,12,105,58,235,193,168,198,204,168,147,56,236,68,146,90,58,6,198,226,209,66,166,197,14,75,55,142,38,44,146,128,75,202,194,88,153,105,177,6,177,110,42,77,106,180,92,112,55,126,16,224,66,4,218,69,102,42,59,4,198,64,10,13,84,249,2,52,160,196,123,12,11,84,230,75,230,217,196,137,170,236,176,159,133,214,39,54,43,132,85,156,136,108,155,150,223,112,6,195,186,10,49,0,84,84,18,206,107,89,129,6,13,188,79,182,11,88,84,198,69,1,176,229,41,160,194,166,18,51,8,25,172,104,83,49,101,96,128,117,195,132,230,144,91,184,75,119,91,101,78,81,75,211,69,54,149,238,244,171,251,236,7,24,246,121,41,212,27,49,219,165,238,15,168,12,41,229,160,2,157,137,50,2,146,131,40,12,185,220,100,19,217,160,108,36,30,206,223,35,211,2,210,253,12,95,252,176,31,204,6,80,254,205,207,103,68,234,61,31,22,46,228,62,184,158,23,203,247,242,16,99,172,145,163,221,147,28,5,26,16,3,115,20,165,217,48,240,70,218,98,25,50,30,146,108,178,203,85,138,131,164,70,144,56,9,117,70,107,26,228,133,230,131,149,198,51,110,181,173,163,80,10,66,107,232,5,9,166,28,42,217,56,95,48,208,196,71,118,121,89,158,210,156,222,158,155,199,183,106,12,166,89,116,12,243,72,77,145,81,100,139,152,61,146,128,132,231,132,173,244,197,140,243,193,10,205,214,10,9,80,157,84,194,7,76,36,180,41,38,13,176,35,90,197,80,45,230,245,162,153,208,22,80,103,6,12,216,134,72,179,25,79,129,68,84,115,146,72,6,203,227,240,148,70,97,162,80,130,176,16,133,227,104,194,47,97,97,249,232,175,78,47,13,253,34,230,67,255,54,239,115,152,240,174,42,41,38,58,83,58,78,100,113,77,59,160,183,216,98,36,130,138,200,6,27,197,20,84,42,200,37,215,222,74,34,90,204,52,19,229,120,243,200,26,133,85,4,146,61,186,1,165,4,37,6,36,122,137,80,240,123,168,120,165,52,19,204,51,117,179,229,22,11,105,198,151,218,64,36,1,91,242,108,101,6,36,19,249,98,152,235,206,38,123,245,250,55,70,109,208,44,7,160,134,32,241,8,118,46,22,190,176,177,137,147,43,99,59,140,82,105,135,131,110,175,204,217,39,143,78,115,136,37,219,22,253,88,182,48,110,97,93,155,182,174,57,209,121,165,63,30,174,111,130,214,120,131,166,11,168,168,129,57,72,106,145,157,96,38,123,35,50,37,207,165,36,42,79,252,40,148,176,62,10,234,219,227,119,192,71,145,1,189,121,255,51,152,154,209,141,101,191,62,126,245,26,200,8,116,27,203,126,219,253,27,228,123,183,143,192,43,148,163,75,68,103,88,5,242,27,161,60,29,168,124,1,245,207,99,191,63,102,178,82,82,208,125,53,23,195,168,4,180,25,5,54,31,25,15,141,130,198,243,246,248,16,159,6,185,241,60,60,10,5,73,25,254,83,52,0,15,116,246,34,240,201,78,52,1,199,129,104,224,129,219,124,34,98,26,114,156,45,93,17,180,96,62,72,64,31,162,250,201,66,54,2,75,53,69,54,215,190,159,8,3,143,183,159,180,230,255,252,216,229,228,85,88,240,114,218,10,22,252,113,216,143,185,151,136,109,120,33,36,241,29,151,57,12,213,155,68,51,224,59,210,182,199,193,193,155,232,130,198,14,24,145,250,34,106,197,100,241,152,38,73,86,84,27,58,232,102,219,88,44,83,115,182,138,208,11,135,193,44,25,11,102,1,214,121,209,1,8,42,27,230,179,44,95,154,205,96,2,79,102,147,156,73,24,0,56,28,68,200,0,245,212,244,6,163,195,169,178,244,193,145,138,136,2,52,25,87,143,6,88,141,119,71,178,200,159,80,160,121,202,129,237,76,81,166,36,105,161,133,37,109,75,67,255,141,241,79,224,79,252,146,11,95,54,141,5,152,44,240,69,131,52,77,2,44,132,78,24,217,102,109,96,216,159,24,2,243,5,218,206,128,131,92,13,129,125,19,94,74,64,171,108,38,179,151,2,89,160,88,107,242,2,72,5,58,157,69,83,108,254,43,33,207,121,77,46,209,187,147,166,160,206,192,3,79,198,81,48,88,232,93,98,170,44,51,202,106,213,20,15,246,37,108,12,210,102,154,119,73,121,210,135,132,53,81,69,57,49,29,98,36,146,201,163,68,203,75,205,23,6,60,160,253,21,138,37,73,62,41,86,3,229,206,68,85,93,8,250,190,135,190,195,66,167,66,66,235,110,25,30,104,131,190,223,10,135,86,190,93,106,5,22,185,26,221,108,10,37,77,175,196,122,171,53,249,42,151,93,134,93,24,44,110,47,93,131,82,177,4,126,115,165,191,24,195,121,71,158,137,158,50,94,128,75,227,106,166,243,126,67,47,65,234,202,145,85,203,102,19,239,150,232,85,32,107,173,213,130,180,150,226,8,201,98,162,123,8,47,19,177,6,22,163,66,23,168,24,253,144,98,175,104,4,200,183,250,30,234,188,130,103,11,233,84,240,121,242,204,164,207,141,109,99,89,216,133,223,67,242,98,60,34,149,147,42,175,200,48,22,235,84,229,234,237,49,80,16,193,112,33,201,42,242,12,255,161,82,180,209,142,173,90,235,48,47,31,143,78,209,174,73,166,32,17,185,240,112,102,172,75,65,95,5,94,245,210,23,58,241,165,214,87,105,235,230,82,108,245,19,2,12,201,66,137,228,66,136,249,181,31,161,181,137,91,55,130,3,37,96,213,8,165,186,73,231,116,152,60,80,172,135,254,18,210,62,190,218,255,249,103,225,239,102,53,161,119,66,210,234,193,193,25,186,212,94,126,58,107,237,60,175,219,186,170,244,183,12,62,207,64,148,136,86,179,110,176,6,29,43,101,120,172,148,199,152,33,87,174,0,95,109,115,2,132,211,105,28,77,99,159,108,71,50,129,38,222,149,160,12,193,56,133,151,71,86,195,227,153,165,81,194,250,255,185,251,241,221,241,187,87,109,193,40,134,92,116,8,143,168,68,3,200,83,0,11,37,34,49,176,21,2,119,69,15,229,75,205,207,178,245,249,128,122,195,7,98,218,104,239,13,24,225,208,31,21,183,87,104,166,6,51,36,106,134,197,152,42,38,38,71,23,161,29,64,195,135,76,218,15,235,246,251,200,144,79,240,0,171,44,231,249,41,233,71,145,108,9,113,65,27,252,114,156,10,40,131,240,132,249,87,5,243,80,216,134,85,48,19,64,4,177,40,36,123,12,41,38,226,17,224,199,94,82,174,165,156,202,212,102,8,210,37,138,175,190,20,232,136,232,173,176,62,242,96,24,69,150,66,113,50,35,28,163,175,237,142,197,51,58,39,249,165,96,9,158,86,9,147,222,27,73,150,129,7,213,116,156,93,218,62,65,223,40,190,232,250,95,3,20,144,229,44,204,129,246,18,53,66,214,23,186,116,9,186,119,197,13,52,128,15,33,166,245,77,202,164,128,91,177,113,20,120,125,25,59,187,144,135,155,240,73,182,127,153,69,127,84,236,190,40,253,60,43,36,247,96,240,244,164,161,118,84,10,37,88,247,119,121,69,14,29,137,153,66,99,180,169,204,5,228,145,180,181,52,130,204,89,128,82,45,139,66,177,69,80,43,50,159,0,173,225,197,6,67,154,15,93,121,245,241,253,167,147,203,211,179,238,217,209,229,143,71,191,156,186,231,150,148,22,42,228,220,182,122,58,156,7,94,164,89,110,141,103,84,70,179,92,120,33,182,6,191,98,230,173,43,30,92,251,58,253,18,57,49,54,6,12,94,210,134,133,140,113,6,2,247,82,213,228,195,33,173,93,75,239,41,192,179,218,71,200,146,133,123,195,138,198,186,226,152,223,234,103,209,29,117,113,97,3,93,92,190,125,127,120,132,3,123,63,28,66,246,41,58,104,68,176,40,188,189,4,45,38,123,163,188,94,128,1,36,34,71,61,7,116,210,31,158,222,135,208,232,167,195,147,203,147,143,239,207,222,31,188,127,115,249,211,209,199,211,227,247,239,160,131,45,123,251,194,62,60,122,217,253,244,230,236,50,43,163,139,184,91,182,142,178,180,165,115,232,84,39,208,185,86,152,43,247,201,154,29,68,30,174,177,83,105,243,98,18,201,121,120,33,253,225,120,96,38,117,177,41,72,192,160,69,203,122,66,49,230,56,37,142,10,177,17,33,97,55,188,119,26,33,49,185,33,191,97,63,171,183,154,117,131,241,156,214,106,169,218,170,213,222,89,179,234,187,186,162,19,133,210,190,119,179,136,179,250,61,134,188,75,224,213,249,9,238,160,35,113,23,131,200,11,55,74,88,117,103,138,138,22,6,159,237,193,19,198,224,99,248,61,61,214,231,115,58,139,144,70,175,249,109,174,15,121,20,164,117,107,173,114,39,141,78,137,172,107,107,219,117,120,249,52,133,133,119,0,216,172,213,231,2,41,92,97,201,108,162,132,193,86,54,29,14,44,159,159,80,99,173,1,228,68,100,199,3,138,140,151,133,33,121,36,158,40,21,253,178,186,228,25,80,99,189,106,126,98,62,68,195,135,82,200,244,70,248,178,54,115,192,125,95,91,249,78,246,192,196,185,15,68,160,184,148,193,181,86,86,249,234,10,133,109,83,208,188,108,7,161,40,53,66,108,59,240,31,171,62,226,233,193,44,198,200,192,67,57,92,215,8,53,20,216,6,222,19,39,252,56,76,107,25,158,70,10,79,245,92,35,165,33,233,54,144,4,212,192,232,20,53,197,98,192,0,129,16,16,198,154,69,64,194,212,219,24,145,216,13,7,98,73,8,162,54,218,3,42,123,82,64,178,10,198,188,207,38,162,157,193,68,147,84,183,213,116,182,203,67,134,92,9,90,187,52,20,68,82,234,190,239,125,198,8,101,188,112,198,231,73,173,184,104,29,169,161,97,52,101,0,154,110,238,180,131,68,192,165,227,39,71,255,152,225,153,153,243,181,11,135,92,195,212,199,137,23,123,147,196,230,24,97,89,88,202,45,59,149,177,152,157,214,139,82,167,154,86,211,243,214,69,189,93,202,167,35,62,181,122,137,63,204,231,182,152,128,79,113,224,150,34,90,43,209,147,186,21,136,177,61,183,136,101,193,122,92,151,215,209,212,186,177,222,162,86,78,215,68,40,205,225,248,208,194,66,154,28,228,109,69,154,10,192,88,129,231,100,26,248,192,145,108,120,84,187,79,53,175,254,251,239,181,212,109,213,109,227,164,140,171,79,202,148,123,20,123,143,216,161,228,26,77,25,195,150,0,151,3,174,6,127,61,250,155,206,203,107,212,68,76,129,220,126,255,61,59,234,164,17,9,232,48,151,32,168,25,131,128,83,75,159,136,19,213,112,126,205,165,146,95,181,185,117,34,215,174,92,38,229,5,98,240,54,247,210,81,102,121,205,236,31,143,205,125,239,96,28,126,237,126,22,7,109,3,206,85,235,5,93,102,240,62,252,48,227,192,22,200,166,176,39,60,29,71,3,48,168,63,157,89,54,29,212,199,241,57,66,111,240,135,119,53,113,134,6,111,22,162,245,101,129,193,36,130,107,192,154,19,71,186,164,122,218,126,20,15,128,8,125,48,207,11,120,156,194,195,92,30,32,66,181,242,64,24,68,139,198,38,199,101,53,147,187,132,14,8,221,107,224,223,159,126,45,244,115,117,130,73,210,200,41,109,211,126,140,110,92,51,168,218,246,196,50,241,93,107,47,141,59,64,202,171,248,52,232,88,54,60,173,236,17,71,147,87,145,41,134,147,156,95,232,19,105,230,125,86,22,203,177,229,102,103,197,166,214,154,170,185,69,13,211,57,161,199,27,77,31,109,116,224,95,171,54,122,105,40,35,194,105,235,79,158,162,129,116,105,131,118,86,118,161,155,26,142,61,116,91,187,225,94,165,238,35,153,213,110,184,186,42,240,148,184,149,229,206,195,11,59,114,61,215,77,126,255,61,113,221,7,116,165,167,79,159,84,247,100,176,133,93,26,12,157,253,50,134,195,112,72,66,171,246,226,187,149,213,90,244,194,98,114,69,181,45,171,14,216,17,200,81,200,21,103,100,133,217,172,110,147,19,219,158,132,108,116,127,160,162,138,23,97,184,86,4,58,36,203,203,214,4,90,100,162,159,254,152,131,158,52,16,253,88,29,102,173,38,114,26,8,202,142,37,79,44,48,145,8,243,208,169,158,33,129,254,226,168,6,232,143,64,179,107,18,93,243,134,186,62,76,6,22,232,65,169,90,250,134,20,150,221,149,34,170,226,81,27,95,83,136,232,75,193,96,128,211,68,74,159,147,50,218,21,50,166,130,111,161,46,121,18,71,19,48,100,106,249,99,8,196,44,97,181,74,249,228,244,252,194,241,79,69,88,158,155,42,198,255,107,139,206,34,156,95,216,161,187,6,228,180,185,231,101,180,229,110,213,125,103,58,75,198,181,123,127,208,246,144,152,168,109,124,92,93,187,176,13,235,128,146,214,117,146,40,190,186,113,97,43,99,130,222,55,47,64,244,202,113,112,117,220,129,198,171,52,111,80,71,190,122,232,226,72,108,83,109,92,229,199,110,32,181,86,199,243,124,97,214,4,240,26,94,187,215,27,94,169,178,21,218,222,92,158,200,200,3,233,230,15,137,200,83,85,226,167,129,145,141,40,88,233,77,92,88,6,34,171,210,76,120,250,180,122,232,37,0,121,166,230,43,40,65,39,224,142,132,19,229,124,201,130,105,229,206,41,34,67,181,165,114,117,197,239,18,64,73,249,164,47,207,142,178,172,124,167,58,98,230,169,104,173,216,2,205,152,213,60,165,143,173,156,47,84,129,245,113,151,78,235,233,211,154,92,255,120,108,74,30,141,36,35,19,84,15,177,168,123,209,109,57,243,69,55,142,189,59,208,239,232,23,212,49,126,81,127,129,127,43,199,226,147,42,208,246,69,185,182,56,99,67,207,120,244,39,45,88,107,32,105,140,187,83,233,188,173,46,177,171,78,9,137,211,227,174,79,103,203,67,239,186,231,197,141,30,24,248,3,117,22,22,104,26,38,163,90,67,20,161,136,9,234,115,149,240,150,106,121,131,129,168,83,187,199,83,67,109,57,225,54,161,88,191,193,218,226,142,63,176,139,138,110,219,212,210,149,237,68,139,53,83,209,117,50,212,87,154,185,180,188,32,9,245,135,122,201,112,118,164,10,167,134,243,100,13,145,169,219,73,96,114,147,69,3,55,203,85,226,32,89,56,120,50,81,81,189,144,163,39,216,146,7,128,194,222,202,190,119,49,217,33,146,184,168,44,41,187,194,75,15,196,187,27,230,23,178,69,27,184,149,237,226,188,163,17,81,118,255,46,234,178,210,85,252,64,167,149,109,87,117,43,220,172,203,117,43,189,189,75,119,43,219,46,116,171,29,172,143,118,154,185,98,151,233,50,107,183,208,161,240,210,61,218,155,116,194,46,211,149,108,113,46,180,41,50,155,114,242,190,33,16,158,232,83,239,86,125,55,85,87,138,202,217,0,218,47,37,85,147,185,186,242,160,168,4,215,36,153,163,161,105,115,176,95,225,207,186,96,88,101,31,21,202,37,92,245,242,116,209,129,232,176,108,105,210,213,2,94,175,49,27,76,149,18,3,195,0,91,243,201,26,122,140,132,119,0,236,98,71,104,43,7,168,206,212,44,186,99,213,18,135,17,73,36,152,130,192,212,185,47,86,176,238,196,155,154,66,89,235,239,223,131,120,21,39,54,149,228,247,147,119,222,59,224,148,47,192,200,4,49,69,5,96,181,231,187,181,241,178,17,96,222,115,20,53,133,206,149,94,254,175,238,152,8,43,231,67,57,47,169,171,149,48,164,134,149,153,22,173,74,178,221,159,24,234,88,130,234,87,4,202,126,148,41,95,17,40,246,201,121,116,225,158,123,240,215,246,241,79,8,127,46,118,51,163,76,235,57,218,44,163,121,124,196,120,172,50,218,238,11,52,139,247,151,204,209,82,156,162,245,88,86,123,184,208,167,12,106,68,83,253,219,195,37,76,121,36,113,229,34,41,121,168,10,112,40,146,191,164,41,65,194,87,148,106,213,97,77,101,55,237,41,79,82,206,153,160,84,83,135,230,10,102,42,55,254,218,125,38,188,72,254,152,160,85,104,173,15,128,39,53,49,17,3,240,128,151,43,230,131,89,63,175,236,102,4,158,150,124,91,90,183,22,90,251,185,183,186,102,203,94,108,223,240,158,234,151,227,1,60,74,193,143,12,103,110,159,95,136,51,214,121,138,54,80,243,57,242,67,97,55,204,235,198,203,46,86,18,196,187,138,9,171,169,100,28,168,180,191,4,195,249,16,253,211,158,226,123,150,56,230,77,153,240,84,59,199,167,253,32,234,213,206,211,11,251,62,45,17,73,212,79,57,222,41,25,115,111,98,205,235,23,182,101,154,57,32,236,53,253,229,204,159,140,4,35,60,217,74,68,230,217,211,56,66,7,10,130,212,126,178,150,163,76,226,170,115,91,220,195,88,229,170,53,85,14,228,150,82,73,81,167,191,115,107,253,1,5,165,158,39,107,244,126,154,125,150,188,177,185,83,240,249,94,96,190,22,81,16,40,187,32,164,212,113,34,98,122,5,52,161,87,14,181,71,141,40,1,69,133,147,169,66,65,205,97,128,63,174,42,146,71,74,107,159,71,200,112,115,76,133,191,48,150,108,195,31,232,123,102,202,92,186,162,156,121,231,184,146,210,188,94,111,87,55,89,37,235,148,199,15,93,134,18,230,135,252,182,210,225,157,186,143,236,3,48,111,69,223,161,3,192,8,8,108,229,128,196,139,159,130,8,208,6,230,184,190,221,27,104,71,212,72,105,168,120,229,43,207,60,184,185,197,137,54,168,73,21,139,29,187,188,254,66,21,77,198,160,107,32,106,196,107,14,25,218,27,58,246,113,124,56,101,180,49,241,147,144,122,110,149,89,58,33,55,99,179,246,235,96,181,254,171,99,254,212,26,53,231,135,122,253,69,83,137,225,251,137,247,57,138,219,232,156,183,39,126,72,143,235,23,66,212,192,227,198,133,141,177,30,216,19,188,109,97,6,72,251,246,57,149,167,146,84,6,179,46,96,161,130,76,199,248,214,50,108,217,197,12,38,240,72,165,126,62,37,213,250,129,231,80,95,29,95,252,206,109,225,84,61,64,123,20,88,152,240,171,86,80,4,106,12,64,5,244,227,21,36,128,184,51,13,76,125,156,238,190,160,155,134,212,31,52,145,58,50,97,213,98,53,220,111,131,102,125,47,76,87,173,186,212,71,232,82,51,108,33,64,8,116,3,204,65,90,199,91,105,161,41,158,83,50,245,21,78,231,121,234,160,178,23,182,87,96,37,24,147,3,76,220,211,4,130,9,120,17,73,70,35,114,61,205,21,68,92,93,8,151,138,203,171,138,160,201,237,236,76,93,182,203,165,212,90,21,228,40,36,198,174,231,230,39,21,244,254,212,27,93,162,234,101,107,84,213,95,88,80,70,157,151,96,222,53,216,37,200,228,158,88,109,235,23,17,79,47,67,185,39,192,249,41,114,34,76,85,113,71,172,62,5,68,50,155,76,188,248,78,129,234,9,64,80,192,194,122,192,88,128,197,155,77,226,58,51,152,134,1,191,125,63,172,233,169,171,63,113,27,107,168,234,25,62,15,236,114,232,199,147,27,47,198,251,214,194,43,75,162,89,222,10,229,163,130,210,139,163,27,48,56,46,145,9,160,242,64,87,134,33,157,23,72,219,116,238,20,110,221,2,35,102,183,130,218,236,71,102,200,240,100,170,187,193,89,181,75,147,120,186,244,105,214,31,194,228,195,147,46,216,139,33,145,240,20,12,136,236,146,228,129,225,226,125,126,30,10,73,180,63,103,49,39,253,92,213,212,215,40,78,125,117,135,21,94,142,132,55,95,36,205,196,31,68,227,38,79,166,120,78,254,114,226,211,41,202,75,40,3,249,1,5,146,55,5,120,85,61,227,6,143,203,75,61,43,206,96,108,183,20,55,232,57,97,151,214,253,162,123,5,197,220,43,23,182,109,189,127,135,27,122,162,222,23,221,58,8,138,155,67,65,24,138,31,235,123,154,100,186,19,171,18,206,72,63,245,112,95,81,223,99,244,112,201,221,226,40,178,48,20,28,137,184,57,11,61,161,98,163,212,178,215,90,173,31,60,39,145,123,162,252,4,89,58,191,57,192,230,240,222,235,31,82,103,76,101,64,135,149,191,1,153,248,178,75,10,94,81,122,46,230,231,50,26,198,221,56,142,190,26,167,222,172,229,210,213,37,57,11,74,23,135,100,4,46,46,24,147,47,33,204,194,115,148,255,193,76,251,1,63,65,209,44,64,146,93,208,83,234,87,196,247,84,247,24,226,202,47,99,176,180,83,150,210,134,237,26,93,16,9,143,158,187,213,18,106,67,233,59,19,242,150,201,251,236,179,23,212,98,219,26,3,181,208,102,173,77,241,33,127,177,105,207,246,47,117,218,175,195,27,223,79,85,184,76,165,123,129,130,78,64,128,152,65,53,232,90,40,187,21,242,247,200,231,116,187,252,37,244,130,119,36,83,47,164,70,4,67,57,165,115,245,10,22,139,96,131,148,47,5,174,245,40,112,18,89,85,192,9,184,43,129,139,166,6,108,196,250,114,223,126,209,146,14,247,167,67,164,54,126,40,21,190,151,232,69,92,176,5,186,104,3,209,31,40,21,115,209,94,162,29,186,158,163,206,239,188,200,60,235,242,132,155,218,176,103,143,237,12,120,106,151,49,61,247,46,150,217,6,4,233,96,110,246,173,172,134,8,165,1,176,185,159,80,181,229,167,53,104,15,11,52,59,12,247,253,140,125,189,186,157,109,233,89,40,239,140,153,215,183,180,225,38,120,254,182,89,116,206,107,145,92,176,198,241,8,249,145,10,60,33,5,26,76,239,150,189,209,170,215,31,244,186,208,78,254,87,122,55,156,65,20,114,218,149,23,186,42,71,46,128,221,226,109,155,245,57,174,97,243,91,83,150,218,78,169,208,254,49,68,186,86,146,42,79,90,234,66,49,116,247,153,234,169,161,189,227,181,83,127,107,168,134,0,255,48,74,104,10,63,172,0,53,210,102,109,237,7,149,73,31,169,168,213,81,139,166,251,234,136,135,16,75,247,100,84,77,158,217,251,117,51,138,162,118,15,28,137,36,232,174,98,79,230,55,125,208,180,137,240,90,9,88,16,249,40,11,92,178,158,131,39,56,9,14,10,226,17,101,103,211,98,201,53,149,149,187,97,181,144,135,139,62,159,137,183,176,229,59,32,22,42,184,178,165,217,179,72,141,189,155,134,24,23,78,9,197,173,151,117,239,123,140,104,50,183,179,190,207,221,71,42,247,180,84,162,15,76,166,173,230,13,212,91,152,237,54,173,5,173,129,11,103,38,63,207,55,67,103,240,235,23,160,32,153,136,214,240,235,111,137,165,149,212,147,155,28,89,182,157,183,13,100,170,69,59,161,2,39,20,15,178,168,73,35,178,100,233,38,21,143,46,182,134,78,1,162,108,10,147,162,0,96,116,157,72,30,251,194,20,76,96,206,230,100,65,77,53,157,99,193,13,178,181,43,193,206,181,85,201,188,149,65,140,153,21,23,235,126,135,31,22,147,187,5,136,27,251,145,97,60,184,201,176,104,131,192,162,219,175,213,216,164,215,97,89,196,161,247,193,71,227,199,188,170,16,86,115,181,3,64,151,203,164,160,206,42,57,74,208,223,54,16,229,31,64,145,83,29,75,97,87,240,51,237,20,73,99,237,174,193,150,117,124,140,239,182,118,253,189,231,32,142,86,235,218,71,146,57,80,20,6,75,151,86,147,172,164,194,90,200,248,20,164,2,63,198,189,212,228,171,169,26,66,174,31,64,62,243,172,42,47,114,110,101,97,157,194,26,200,32,192,101,96,87,204,22,114,171,93,161,97,154,237,151,195,54,241,110,102,186,190,189,28,255,42,226,191,160,214,124,94,212,60,165,143,208,186,176,181,147,124,69,51,179,176,154,190,194,178,147,207,205,252,107,210,9,174,195,31,32,23,56,64,140,135,188,218,200,192,241,172,254,209,100,154,222,9,154,193,180,8,230,232,125,248,50,234,207,18,124,21,133,223,135,251,96,106,225,59,208,39,126,22,237,94,192,218,206,219,192,122,103,106,15,21,49,109,188,22,62,166,99,153,147,223,121,56,92,7,22,188,178,108,5,33,96,195,96,228,46,16,75,153,187,189,106,234,42,150,164,172,80,242,244,10,193,83,162,252,210,167,127,208,165,188,160,55,177,77,102,106,138,165,202,29,161,46,173,100,27,98,218,29,79,24,236,80,172,55,191,165,248,112,145,0,205,102,183,179,219,62,152,35,81,40,38,175,59,24,20,166,163,188,31,81,177,69,241,251,239,247,95,29,32,108,63,16,118,92,177,23,67,238,230,220,166,79,230,142,55,136,22,183,134,170,233,53,137,226,148,244,242,182,116,181,87,82,240,255,61,146,61,30,252,41,232,85,133,194,209,220,238,202,95,87,199,224,123,230,238,141,96,230,114,167,195,131,218,118,46,82,229,24,192,173,201,22,42,118,118,4,137,188,36,47,98,187,114,54,164,255,252,255,215,90,183,231,94,99,216,109,188,108,53,158,95,220,175,217,155,243,223,207,229,227,214,188,254,125,179,254,162,166,33,132,174,90,123,232,135,229,123,238,246,214,214,198,214,139,90,110,111,133,66,115,192,0,104,23,146,69,236,118,143,227,61,35,116,1,11,143,245,73,168,214,109,11,191,227,137,71,48,91,183,47,225,159,133,211,85,255,22,109,204,205,128,29,87,63,229,86,168,184,30,27,204,203,153,182,45,67,239,26,172,197,235,70,234,245,18,97,66,210,13,5,103,248,10,22,110,228,42,165,100,87,94,123,92,29,113,65,54,171,94,73,43,171,17,138,252,108,153,173,32,41,121,163,146,45,13,189,162,102,192,29,117,16,205,84,17,176,115,52,69,101,227,70,115,165,112,139,138,86,63,29,158,232,166,86,176,13,104,121,6,182,173,104,206,88,24,216,152,180,47,115,35,102,184,195,249,232,160,11,13,193,50,193,75,26,65,172,11,28,40,163,92,142,94,180,111,101,87,44,87,124,16,132,204,104,116,202,247,208,95,9,245,178,239,131,24,157,101,95,14,198,160,102,15,135,71,198,186,237,101,238,3,40,67,17,212,186,243,213,52,135,104,105,237,99,176,30,15,166,79,159,214,188,194,136,178,79,98,74,12,227,99,3,121,130,104,23,95,105,162,37,39,177,189,204,107,96,87,5,125,185,248,121,20,16,48,47,68,87,130,48,243,124,90,88,172,204,140,247,100,11,131,200,152,242,190,0,98,139,103,255,170,163,0,17,64,33,59,58,228,144,219,107,170,183,185,2,95,116,13,220,206,56,249,249,79,1,110,78,65,118,132,239,27,192,135,239,133,83,214,26,208,74,23,152,216,236,129,9,87,241,25,169,14,121,5,213,97,94,111,231,163,200,233,54,245,234,248,252,226,200,48,68,223,156,125,88,28,222,92,44,94,237,71,170,87,174,189,202,37,83,90,225,98,13,151,109,186,164,65,223,235,100,198,103,59,13,123,70,238,11,155,80,228,158,201,54,86,62,167,236,40,95,82,47,230,36,179,222,196,79,203,171,180,210,234,44,71,131,57,99,47,41,216,120,245,114,108,25,186,31,212,167,25,60,183,8,1,143,125,48,14,126,227,34,58,183,190,75,95,60,40,71,176,40,110,193,207,197,238,217,133,222,127,45,196,247,122,245,23,158,136,100,81,162,181,173,235,184,231,158,10,109,185,48,82,85,180,11,159,219,247,185,91,150,218,231,23,72,158,238,247,14,80,12,98,240,190,188,88,177,12,198,73,243,90,241,24,26,216,219,198,177,33,53,102,49,177,228,180,130,223,154,133,238,1,75,106,55,21,182,61,146,103,197,68,101,106,64,21,190,171,212,167,133,52,182,16,20,227,211,193,11,119,138,161,216,158,79,122,100,181,223,219,208,57,233,18,95,218,239,243,71,104,105,167,81,132,76,174,118,79,159,95,192,64,233,182,133,78,114,155,194,165,11,118,175,230,208,150,56,42,228,249,192,6,218,194,210,158,163,209,45,125,14,66,139,83,126,172,239,228,189,27,57,167,83,229,254,124,61,23,171,95,171,112,85,72,255,196,31,251,115,20,57,48,10,246,117,25,158,82,228,77,118,110,244,177,240,160,167,79,245,49,79,106,255,233,211,220,145,222,90,217,134,251,227,126,57,35,251,22,9,106,141,153,2,217,67,39,138,8,215,151,247,146,131,74,96,154,30,138,14,243,201,178,10,126,190,143,178,228,41,33,33,122,196,139,85,40,43,143,182,231,213,189,126,20,4,222,52,209,201,94,12,211,227,234,19,4,58,187,147,183,10,227,6,222,57,105,117,196,54,47,14,192,31,209,166,130,180,118,88,174,56,173,76,104,206,234,124,147,108,125,78,137,129,86,155,31,163,56,241,32,85,221,166,213,49,206,77,8,133,150,206,89,229,208,43,6,200,202,3,46,233,249,152,15,63,86,39,175,80,107,57,147,199,172,96,123,121,180,22,249,115,71,241,2,173,109,23,154,38,239,174,176,44,232,241,17,139,155,182,6,137,81,10,20,118,242,155,146,26,127,89,103,170,53,237,156,52,122,167,207,135,88,197,18,213,116,164,235,51,208,112,12,18,196,152,147,6,8,240,105,52,157,77,93,113,121,141,72,228,183,0,203,128,195,208,196,13,54,29,241,61,148,220,212,247,97,217,165,198,188,123,230,172,104,128,240,59,113,197,73,33,216,27,226,83,221,75,205,76,174,130,154,158,174,120,205,33,76,140,47,225,192,18,60,188,107,94,131,227,95,251,98,45,150,231,145,246,78,228,174,140,180,20,77,161,177,4,120,170,184,4,140,4,11,221,234,36,69,203,2,2,210,93,46,219,67,67,135,1,201,158,68,251,236,165,76,94,208,15,110,218,151,14,142,86,118,35,74,54,138,11,97,159,146,179,214,209,220,92,72,163,66,203,165,207,253,229,232,54,75,22,129,140,165,175,65,46,73,195,29,250,254,221,94,175,68,134,61,77,131,70,87,248,121,202,165,104,242,177,143,82,118,42,190,58,88,133,17,241,36,78,140,138,191,192,149,22,8,13,2,84,91,232,102,25,60,41,79,185,89,0,170,20,198,13,204,42,114,201,70,50,105,108,224,136,50,235,56,47,249,171,219,178,58,194,221,196,222,129,18,172,172,102,101,2,102,53,116,35,164,150,224,5,129,28,58,56,32,231,148,184,60,215,113,28,98,4,194,126,203,13,63,255,104,142,111,233,65,28,15,138,240,11,10,82,208,31,15,242,140,201,8,231,205,184,211,162,193,97,227,185,113,29,209,221,4,227,89,15,111,38,40,13,170,12,242,102,126,32,102,196,45,117,147,187,69,65,208,52,141,173,223,79,237,120,212,187,193,63,151,248,50,156,165,173,157,231,244,243,124,45,143,7,233,220,182,58,164,77,233,193,44,14,239,80,21,22,4,120,84,71,97,200,235,148,212,33,236,206,226,115,215,106,11,232,177,67,215,107,176,170,196,206,180,171,67,11,58,108,77,15,224,33,48,190,69,255,235,208,219,250,191,173,55,32,94,182,241,111,235,13,232,142,109,46,213,27,203,209,100,70,129,130,224,190,5,44,91,0,203,214,31,4,150,109,128,101,251,15,2,203,51,128,229,217,31,4,150,29,128,101,231,223,70,157,45,232,173,27,4,186,191,156,68,124,128,147,26,76,143,220,147,157,143,124,18,129,152,193,205,187,42,190,103,168,24,11,238,98,144,248,45,105,23,121,107,108,129,174,33,224,233,160,19,32,19,33,15,232,192,210,246,89,168,8,19,115,22,3,67,221,201,196,24,202,130,252,6,30,222,180,153,211,110,205,226,32,49,242,165,15,14,206,22,22,150,34,70,235,56,154,195,231,123,91,125,164,141,82,133,133,133,53,217,26,229,197,213,160,172,201,246,119,30,172,71,194,175,80,239,249,26,214,91,127,176,222,122,171,92,111,189,85,212,209,30,162,195,69,90,201,218,250,3,50,62,91,171,57,105,14,191,166,68,39,224,150,237,106,188,213,121,61,3,130,131,223,47,5,117,187,224,12,200,66,229,22,102,200,24,186,106,251,190,28,9,156,17,187,252,41,97,116,1,200,139,177,102,168,63,95,134,165,83,29,79,254,213,200,18,156,78,185,78,40,86,143,25,81,122,172,20,184,46,64,23,37,241,11,211,200,235,242,105,222,45,104,62,173,66,170,210,136,90,45,235,235,240,36,137,170,82,85,252,2,148,81,120,37,59,203,162,214,191,26,115,70,42,221,225,219,160,24,123,77,108,203,33,214,12,159,255,215,96,246,203,86,57,224,34,11,186,255,42,212,80,51,203,13,94,196,240,127,235,97,127,17,49,136,155,141,191,126,160,153,160,211,1,163,153,145,225,231,21,9,29,209,202,114,103,90,116,63,6,126,242,187,222,82,98,151,77,9,193,216,171,215,14,243,195,0,172,236,133,12,186,200,156,177,165,135,28,181,69,133,194,12,144,21,125,200,20,10,52,184,164,37,97,117,126,198,159,76,47,16,130,136,48,188,176,63,84,17,160,209,215,28,201,176,212,61,154,181,15,245,29,210,57,37,161,98,188,195,231,66,231,203,14,83,158,105,122,168,171,169,231,199,143,59,61,103,137,116,122,158,64,241,175,4,70,94,168,245,0,44,179,112,25,104,204,107,181,216,39,170,82,156,27,84,18,166,240,59,125,152,182,191,132,230,242,52,102,212,68,228,224,138,135,191,194,118,255,50,10,196,103,121,43,254,67,179,36,175,64,151,52,145,147,238,85,56,34,120,170,220,249,102,69,218,160,179,164,94,44,213,134,127,30,116,70,215,254,224,167,159,138,68,205,111,211,101,225,87,20,87,208,200,77,254,165,122,41,8,138,108,109,254,51,28,165,60,205,255,98,27,5,39,225,97,207,252,35,86,9,203,71,64,231,53,229,175,67,74,149,103,235,127,137,254,197,199,16,240,235,69,151,114,120,223,114,5,156,98,195,223,112,9,60,0,61,225,231,223,65,253,37,67,9,107,147,87,177,50,120,192,32,10,47,160,120,23,181,207,52,240,189,32,26,149,184,165,23,52,22,231,84,186,195,69,214,82,251,169,226,156,134,192,34,152,44,19,95,215,183,58,79,233,30,250,221,12,45,227,245,124,15,114,82,213,103,215,197,233,9,212,142,214,171,23,129,138,203,121,84,95,98,166,206,132,1,29,21,248,20,129,30,157,189,148,190,201,148,197,17,33,68,123,41,14,190,131,119,154,194,163,118,124,31,238,53,225,13,83,240,219,188,39,81,156,234,132,147,56,66,7,77,192,228,105,120,157,145,109,203,22,69,189,2,7,143,163,16,41,34,140,133,179,41,75,139,123,97,119,99,127,120,61,37,62,137,1,32,190,170,6,175,78,176,96,81,44,131,191,56,118,248,69,188,60,102,103,102,155,13,79,195,94,50,221,45,80,110,97,71,235,207,71,178,217,94,245,82,180,90,198,210,50,202,62,17,174,216,133,165,29,51,19,179,5,167,161,136,86,90,164,85,234,115,154,167,162,216,151,78,178,246,99,225,229,196,140,254,54,110,188,152,238,30,16,111,116,131,178,220,139,204,157,198,90,106,42,74,213,228,241,175,138,233,216,236,156,137,130,76,94,160,207,106,244,9,44,140,98,86,223,36,174,195,172,108,26,196,88,184,105,127,209,32,179,157,232,226,198,239,159,143,126,75,59,217,95,196,114,43,232,192,34,53,190,215,233,6,24,168,200,82,252,88,144,199,180,127,176,105,4,134,200,43,51,50,90,118,232,240,109,71,236,134,227,247,75,238,162,25,198,191,102,225,32,172,199,135,248,141,143,217,116,20,211,77,93,14,195,13,232,211,104,194,197,183,78,68,6,126,168,75,124,213,77,124,238,68,28,145,77,108,249,237,40,252,198,85,63,138,227,217,84,124,197,149,2,158,232,140,164,195,142,135,226,51,33,99,10,104,131,42,36,183,69,59,54,80,22,152,202,8,180,172,39,176,102,75,170,75,82,241,65,52,206,22,12,193,33,251,102,153,229,68,104,60,148,24,210,29,177,158,31,122,24,226,168,190,159,205,51,204,170,91,53,140,27,53,30,191,75,3,100,174,159,190,158,245,152,74,96,120,182,24,49,237,176,44,156,194,163,171,124,213,61,36,189,59,70,225,124,244,145,27,113,103,139,85,10,189,176,152,32,60,49,98,162,136,183,72,10,201,76,97,8,71,114,167,190,165,42,144,74,31,88,242,200,246,157,210,151,135,176,65,66,96,47,242,226,193,19,156,106,77,170,226,91,81,126,104,124,61,74,223,129,219,167,143,130,201,64,76,70,247,111,84,204,44,160,141,168,131,192,193,14,68,147,159,78,247,205,121,202,49,223,166,154,9,139,137,227,228,174,184,169,140,241,176,47,214,168,136,31,247,226,148,56,107,3,23,103,126,205,152,177,114,133,253,32,35,75,10,125,83,237,84,188,124,26,251,116,105,75,103,159,238,158,97,57,254,79,97,124,210,207,35,158,61,224,252,83,140,94,195,11,214,24,133,87,32,187,34,167,114,59,140,66,94,222,172,95,50,98,27,227,8,49,226,13,164,151,124,250,186,1,41,225,4,204,136,168,93,207,112,165,100,91,106,4,146,165,155,194,173,196,206,134,64,22,143,242,216,226,22,93,53,183,61,64,78,108,120,52,30,144,160,102,152,211,159,91,168,44,43,75,16,35,197,11,137,84,189,113,76,162,94,30,29,204,116,106,83,172,23,111,233,42,98,59,127,155,145,69,13,190,161,180,114,123,202,115,44,60,164,15,183,164,34,93,4,201,233,166,202,123,165,250,210,47,129,85,120,66,251,66,220,144,165,251,16,213,62,10,54,204,14,9,127,203,53,165,63,88,126,233,165,11,154,123,23,81,52,156,108,111,26,243,135,218,147,83,67,95,28,18,97,68,222,130,226,120,119,133,43,110,195,50,234,227,9,9,188,6,203,234,252,228,131,216,0,66,19,50,38,31,107,90,217,42,34,57,127,229,86,38,3,243,193,118,143,235,168,255,219,139,188,58,166,239,207,183,216,69,144,34,123,202,62,74,253,231,75,77,162,189,241,134,94,119,156,228,108,161,105,163,193,141,108,167,185,169,195,43,181,30,9,154,206,100,220,16,233,66,220,45,20,157,197,150,243,164,185,16,162,138,49,110,20,85,4,5,215,127,20,132,127,149,130,240,77,245,130,63,28,227,168,10,186,254,243,177,13,17,216,174,99,201,31,103,21,153,19,93,160,166,176,225,249,224,23,97,136,136,38,204,184,7,167,224,238,69,219,13,150,164,88,224,94,252,192,230,221,87,118,64,86,100,238,203,1,64,22,104,88,226,119,95,153,56,62,86,181,37,246,135,161,206,166,116,14,162,116,239,252,15,10,16,24,71,248,184,0,0}; \ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 281e6808..9d182112 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -28,6 +28,9 @@ x-tagGroups: tags: - System - Settings + - name: Aliases + tags: + - Aliases - name: Devices tags: - Device Control @@ -38,6 +41,125 @@ x-tagGroups: - Transitions paths: + /aliases: + post: + tags: + - Aliases + summary: Create a new alias + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Alias' + responses: + '201': + description: Alias created successfully + content: + application/json: + # will only respond with the created ID + schema: + type: object + properties: + id: + type: integer + get: + tags: + - Aliases + summary: Get all aliases + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + aliases: + type: array + items: + $ref: '#/components/schemas/Alias' + page: + type: integer + count: + type: integer + num_pages: + type: integer + /aliases/{id}: + put: + tags: + - Aliases + summary: Update an alias by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Alias' + responses: + '200': + description: Alias updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanResponse' + delete: + tags: + - Aliases + summary: Delete an alias by ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Alias deleted successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanResponse' + /aliases.bin: + get: + tags: + - Aliases + summary: Download a backup of all aliases in a binary format + responses: + '200': + description: Successful operation + content: + application/octet-stream: + schema: + type: string + format: binary + post: + tags: + - Aliases + summary: Upload a backup of all aliases in CSV format + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + '201': + description: Backup uploaded and aliases restored successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanResponse' /about: get: tags: @@ -50,6 +172,45 @@ paths: application/json: schema: $ref: '#/components/schemas/About' + /backup: + post: + tags: + - System + summary: Restore a backup + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + 400: + description: error + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanResponse' + 201: + description: Backup uploaded and settings restored successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanResponse' + get: + tags: + - System + summary: Download a backup of all settings + responses: + 200: + description: success + content: + application/octet-stream: + schema: + type: string + format: binary /remote_configs: get: tags: @@ -491,6 +652,19 @@ components: $ref: '#/components/schemas/RemoteType' required: true schemas: + Alias: + type: object + properties: + alias: + type: string + id: + type: integer + device_id: + type: integer + group_id: + type: integer + device_type: + type: string State: description: "On/Off state" type: string @@ -997,7 +1171,11 @@ components: $ref: '#/components/schemas/GroupStateField' group_id_aliases: type: object - description: Keys are aliases, values are 3-long arrays with same schema as items in `device_ids`. + description: | + DEPRECATED (use /aliases routes instead) + + Keys are aliases, values are 3-long arrays with same schema as items in `device_ids`. + deprecated: true example: alias1: [1234, 'rgb_cct', 1] alias2: [1234, 'rgb_cct', 2] diff --git a/lib/DataStructures/LinkedList.h b/lib/DataStructures/LinkedList.h index ff702b30..3761fccc 100644 --- a/lib/DataStructures/LinkedList.h +++ b/lib/DataStructures/LinkedList.h @@ -45,7 +45,7 @@ class LinkedList { Unlink and link the LinkedList correcly; Increment _size */ - virtual bool add(int index, T); + virtual bool add(size_t index, T); /* Adds a T object in the end of the LinkedList; Increment _size; @@ -60,13 +60,13 @@ class LinkedList { Set the object at index, with T; Increment _size; */ - virtual bool set(int index, T); + virtual bool set(size_t index, T); /* Remove object at index; If index is not reachable, returns false; else, decrement _size */ - virtual T remove(int index); + virtual T remove(size_t index); virtual void remove(ListNode* node); /* Remove last object; @@ -81,14 +81,14 @@ class LinkedList { Return Element if accessible, else, return false; */ - virtual T get(int index); + virtual T get(size_t index); /* Clear the entire array */ virtual void clear(); - ListNode* getNode(int index); + ListNode* getNode(size_t index); virtual void spliceToFront(ListNode* node); ListNode* getHead() { return root; } T getLast() const { return last == NULL ? T() : last->data; } @@ -145,9 +145,9 @@ LinkedList::~LinkedList() */ template -ListNode* LinkedList::getNode(int index){ +ListNode* LinkedList::getNode(size_t index){ - int _pos = 0; + size_t _pos = 0; ListNode* current = root; while(_pos < index && current){ @@ -164,7 +164,7 @@ size_t LinkedList::size() const{ } template -bool LinkedList::add(int index, T _t){ +bool LinkedList::add(size_t index, T _t){ if(index >= _size) return add(_t); @@ -226,7 +226,7 @@ bool LinkedList::unshift(T _t){ } template -bool LinkedList::set(int index, T _t){ +bool LinkedList::set(size_t index, T _t){ // Check if index position is in bounds if(index < 0 || index >= _size) return false; @@ -299,7 +299,7 @@ void LinkedList::remove(ListNode* node){ } template -T LinkedList::remove(int index){ +T LinkedList::remove(size_t index){ if (index < 0 || index >= _size) { return T(); @@ -325,7 +325,7 @@ T LinkedList::remove(int index){ template -T LinkedList::get(int index){ +T LinkedList::get(size_t index){ ListNode *tmp = getNode(index); return (tmp ? tmp->data : T()); diff --git a/lib/Environment/ProjectFS.h b/lib/Environment/ProjectFS.h new file mode 100644 index 00000000..b131b1fb --- /dev/null +++ b/lib/Environment/ProjectFS.h @@ -0,0 +1,16 @@ +// +// Created by chris on 9/14/2023. +// + +#ifndef ESP8266_MILIGHT_HUB_PROJECTFS_H +#define ESP8266_MILIGHT_HUB_PROJECTFS_H + +#ifdef MILIGHT_USE_LITTLE_FS +#include +#define ProjectFS LittleFS +#else +#include +#define ProjectFS SPIFFS +#endif + +#endif //ESP8266_MILIGHT_HUB_PROJECTFS_H diff --git a/lib/Environment/ProjectWifi.h b/lib/Environment/ProjectWifi.h new file mode 100644 index 00000000..ed0725c3 --- /dev/null +++ b/lib/Environment/ProjectWifi.h @@ -0,0 +1,36 @@ +// +// Created by chris on 9/17/2023. +// + +#ifndef ESP8266_MILIGHT_HUB_PROJECTWIFI_H +#define ESP8266_MILIGHT_HUB_PROJECTWIFI_H + +#if __has_include() +#include +#endif + +#if defined(ESPMH_WIFI_SSID) && defined(ESPMH_WIFI_PASSWORD) +#define ESPMH_SETUP_WIFI(settings) { \ + Serial.println(F(">>> ESPMH_WIFI_SETUP() <<<"));\ + if (settings.wifiStaticIP.length() > 0) { \ + Serial.printf_P(PSTR("Configuring static IP...%s\n"), settings.wifiStaticIP.c_str()); \ + IPAddress _ip, _subnet, _gw;\ + _ip.fromString(settings.wifiStaticIP);\ + _subnet.fromString(settings.wifiStaticIPNetmask);\ + _gw.fromString(settings.wifiStaticIPGateway);\ + WiFi.config(_ip, _gw, _subnet);\ + };\ + WiFi.begin(ESPMH_WIFI_SSID, ESPMH_WIFI_PASSWORD); \ + Serial.printf_P(PSTR("Connecting to %s...\n"), ESPMH_WIFI_SSID); \ + while (WiFi.status() != WL_CONNECTED) {\ + delay(500);\ + Serial.print(".");\ + }\ + Serial.printf_P(PSTR("Connected to: %s with IP: "), ESPMH_WIFI_SSID); \ + Serial.println(WiFi.localIP()); \ +} +#else +#define ESPMH_SETUP_WIFI(settings) { } +#endif + +#endif //ESP8266_MILIGHT_HUB_PROJECTWIFI_H diff --git a/lib/Helpers/IntParsing.h b/lib/Helpers/IntParsing.h index bcc10ec2..c5229455 100644 --- a/lib/Helpers/IntParsing.h +++ b/lib/Helpers/IntParsing.h @@ -8,7 +8,7 @@ const T strToHex(const char* s, size_t length) { T value = 0; T base = 1; - for (int i = length-1; i >= 0; i--) { + for (size_t i = length-1; i >= 0; i--) { const char c = s[i]; if (c >= '0' && c <= '9') { @@ -43,9 +43,9 @@ const T parseInt(const String& s) { template void hexStrToBytes(const char* s, const size_t sLen, T* buffer, size_t maxLen) { - int idx = 0; + size_t idx = 0; - for (int i = 0; i < sLen && idx < maxLen; ) { + for (size_t i = 0; i < sLen && idx < maxLen; ) { buffer[idx++] = strToHex(s+i, 2); i+= 2; diff --git a/lib/MQTT/BulbStateUpdater.cpp b/lib/MQTT/BulbStateUpdater.cpp index 7bd8ce51..ba20eebf 100644 --- a/lib/MQTT/BulbStateUpdater.cpp +++ b/lib/MQTT/BulbStateUpdater.cpp @@ -37,12 +37,17 @@ void BulbStateUpdater::loop() { } inline void BulbStateUpdater::flushGroup(BulbId bulbId, GroupState& state) { - char buffer[200]; - StaticJsonDocument<200> json; + StaticJsonDocument json; JsonObject message = json.to(); - state.applyState(message, bulbId, settings.groupStateFields); - serializeJson(json, buffer); + + if (json.overflowed()) { + Serial.println(F("ERROR: State is too large for MQTT buffer, continuing anyway. Consider increasing MILIGHT_MQTT_JSON_BUFFER_SIZE.")); + } + + size_t documentSize = measureJson(message); + char buffer[documentSize + 1]; + serializeJson(json, buffer, sizeof(buffer)); mqttClient.sendState( *MiLightRemoteConfig::fromType(bulbId.deviceType), diff --git a/lib/MQTT/BulbStateUpdater.h b/lib/MQTT/BulbStateUpdater.h index c89851c1..6392ea60 100644 --- a/lib/MQTT/BulbStateUpdater.h +++ b/lib/MQTT/BulbStateUpdater.h @@ -7,6 +7,10 @@ #include #include +#ifndef MILIGHT_MQTT_JSON_BUFFER_SIZE +#define MILIGHT_MQTT_JSON_BUFFER_SIZE 1024 +#endif + #ifndef BULB_STATE_UPDATER #define BULB_STATE_UPDATER diff --git a/lib/MQTT/HomeAssistantDiscoveryClient.cpp b/lib/MQTT/HomeAssistantDiscoveryClient.cpp index f97ba86b..6d053a11 100644 --- a/lib/MQTT/HomeAssistantDiscoveryClient.cpp +++ b/lib/MQTT/HomeAssistantDiscoveryClient.cpp @@ -1,25 +1,26 @@ #include #include #include +#include HomeAssistantDiscoveryClient::HomeAssistantDiscoveryClient(Settings& settings, MqttClient* mqttClient) : settings(settings) , mqttClient(mqttClient) { } -void HomeAssistantDiscoveryClient::sendDiscoverableDevices(const std::map& aliases) { +void HomeAssistantDiscoveryClient::sendDiscoverableDevices(const std::map& aliases) { #ifdef MQTT_DEBUG - Serial.println(F("HomeAssistantDiscoveryClient: Sending discoverable devices...")); + Serial.printf_P(PSTR("HomeAssistantDiscoveryClient: Sending %d discoverable devices...\n"), aliases.size()); #endif - for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) { - addConfig(itr->first.c_str(), itr->second); + for (const auto & alias : aliases) { + addConfig(alias.first.c_str(), alias.second.bulbId); } } void HomeAssistantDiscoveryClient::removeOldDevices(const std::map& aliases) { #ifdef MQTT_DEBUG - Serial.println(F("HomeAssistantDiscoveryClient: Removing discoverable devices...")); + Serial.printf_P(PSTR("HomeAssistantDiscoveryClient: Removing %d discoverable devices...\n"), aliases.size()); #endif for (auto itr = aliases.begin(); itr != aliases.end(); ++itr) { @@ -38,12 +39,16 @@ void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bu DynamicJsonDocument config(1024); // Unique ID for this device + alias combo - char uniqidBuffer[30]; - sprintf_P(uniqidBuffer, PSTR("%X-%s"), ESP.getChipId(), alias); + char uniqueIdBuffer[30]; + snprintf_P(uniqueIdBuffer, sizeof(uniqueIdBuffer), PSTR("%X-%s"), ESP.getChipId(), alias); // String to ID the firmware version - char fwVersion[30]; - sprintf_P(fwVersion, PSTR("esp8266_milight_hub v%s"), QUOTE(MILIGHT_HUB_VERSION)); + char fwVersion[100]; + snprintf_P(fwVersion, sizeof(fwVersion), PSTR("esp8266_milight_hub v%s"), QUOTE(MILIGHT_HUB_VERSION)); + + // URL to the device + char deviceUrl[23]; + snprintf_P(deviceUrl, sizeof(deviceUrl), PSTR("http://%s"), WiFi.localIP().toString().c_str()); config[F("dev_cla")] = F("light"); config[F("schema")] = F("json"); @@ -52,14 +57,18 @@ void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bu config[F("cmd_t")] = mqttClient->bindTopicString(settings.mqttTopicPattern, bulbId); // state topic config[F("stat_t")] = mqttClient->bindTopicString(settings.mqttStateTopicPattern, bulbId); - config[F("uniq_id")] = mqttClient->bindTopicString(uniqidBuffer, bulbId); - JsonObject deviceMetadata = config.createNestedObject(F("dev")); + config[F("uniq_id")] = uniqueIdBuffer; + JsonObject originMetadata = config.createNestedObject(F("o")); + originMetadata[F("url")] = deviceUrl; + + JsonObject deviceMetadata = config.createNestedObject(F("dev")); deviceMetadata[F("name")] = settings.hostname; deviceMetadata[F("sw")] = fwVersion; deviceMetadata[F("mf")] = F("espressif"); deviceMetadata[F("mdl")] = QUOTE(FIRMWARE_VARIANT); deviceMetadata[F("identifiers")] = String(ESP.getChipId()); + deviceMetadata[F("cu")] = deviceUrl; // HomeAssistant only supports simple client availability if (settings.mqttClientStatusTopic.length() > 0 && settings.simpleMqttClientStatus) { @@ -104,30 +113,16 @@ void HomeAssistantDiscoveryClient::addConfig(const char* alias, const BulbId& bu break; } - // These bulbs support RGB color - switch (bulbId.deviceType) { - case REMOTE_TYPE_FUT089: - case REMOTE_TYPE_RGB: - case REMOTE_TYPE_RGB_CCT: - case REMOTE_TYPE_RGBW: - config[F("rgb")] = true; - break; - default: - break; //nothing + // Flag RGB support + if (MiLightRemoteTypeHelpers::supportsRgb(bulbId.deviceType)) { + config[F("rgb")] = true; } - // These bulbs support adjustable white values - switch (bulbId.deviceType) { - case REMOTE_TYPE_CCT: - case REMOTE_TYPE_FUT089: - case REMOTE_TYPE_FUT091: - case REMOTE_TYPE_RGB_CCT: - config[GroupStateFieldNames::COLOR_TEMP] = true; - config[F("max_mirs")] = COLOR_TEMP_MAX_MIREDS; - config[F("min_mirs")] = COLOR_TEMP_MIN_MIREDS; - break; - default: - break; //nothing + // Flag adjustable color temp support + if (MiLightRemoteTypeHelpers::supportsColorTemp(bulbId.deviceType)) { + config[GroupStateFieldNames::COLOR_TEMP] = true; + config[F("max_mirs")] = COLOR_TEMP_MAX_MIREDS; + config[F("min_mirs")] = COLOR_TEMP_MIN_MIREDS; } String message; diff --git a/lib/MQTT/HomeAssistantDiscoveryClient.h b/lib/MQTT/HomeAssistantDiscoveryClient.h index ee6fc798..c8333889 100644 --- a/lib/MQTT/HomeAssistantDiscoveryClient.h +++ b/lib/MQTT/HomeAssistantDiscoveryClient.h @@ -11,7 +11,7 @@ class HomeAssistantDiscoveryClient { void addConfig(const char* alias, const BulbId& bulbId); void removeConfig(const BulbId& bulbId); - void sendDiscoverableDevices(const std::map& aliases); + void sendDiscoverableDevices(const std::map& aliases); void removeOldDevices(const std::map& aliases); private: diff --git a/lib/MQTT/MqttClient.cpp b/lib/MQTT/MqttClient.cpp index 095d1ebe..ade753dd 100644 --- a/lib/MQTT/MqttClient.cpp +++ b/lib/MQTT/MqttClient.cpp @@ -219,11 +219,8 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) { printf("MqttClient - Got message on topic: %s\n%s\n", topic, cstrPayload); #endif - char topicPattern[settings.mqttTopicPattern.length()]; - strcpy(topicPattern, settings.mqttTopicPattern.c_str()); - - TokenIterator patternIterator(topicPattern, settings.mqttTopicPattern.length(), '/'); - TokenIterator topicIterator(topic, strlen(topic), '/'); + auto patternIterator = std::make_shared(settings.mqttTopicPattern.c_str(), settings.mqttTopicPattern.length(), '/'); + auto topicIterator = std::make_shared(topic, strlen(topic), '/'); UrlTokenBindings tokenBindings(patternIterator, topicIterator); if (tokenBindings.hasBinding("device_alias")) { @@ -234,7 +231,7 @@ void MqttClient::publishCallback(char* topic, byte* payload, int length) { Serial.printf_P(PSTR("MqttClient - WARNING: could not find device alias: `%s'. Ignoring packet.\n"), alias.c_str()); return; } else { - BulbId bulbId = itr->second; + BulbId bulbId = itr->second.bulbId; deviceId = bulbId.deviceId; config = MiLightRemoteConfig::fromType(bulbId.deviceType); diff --git a/lib/MiLight/MiLightClient.cpp b/lib/MiLight/MiLightClient.cpp index 527db429..50ff1f0d 100644 --- a/lib/MiLight/MiLightClient.cpp +++ b/lib/MiLight/MiLightClient.cpp @@ -322,7 +322,7 @@ void MiLightClient::update(JsonObject request) { JsonVariant brightness = request[GroupStateFieldNames::BRIGHTNESS]; JsonVariant level = request[GroupStateFieldNames::LEVEL]; - const bool isBrightnessDefined = !brightness.isUndefined() || !level.isUndefined(); + const bool isBrightnessDefined = !brightness.isNull() || !level.isNull(); // Always turn on first if (parsedStatus == ON) { @@ -346,9 +346,9 @@ void MiLightClient::update(JsonObject request) { } else { this->updateStatus(ON); - if (! brightness.isUndefined()) { + if (! brightness.isNull()) { handleTransition(GroupStateField::BRIGHTNESS, brightness, transition, 0); - } else if (! level.isUndefined()) { + } else if (! level.isNull()) { handleTransition(GroupStateField::LEVEL, level, transition, 0); } } @@ -515,16 +515,16 @@ void MiLightClient::handleTransition(GroupStateField field, JsonVariant value, f } bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) { - if (! args.containsKey(FS(TransitionParams::FIELD)) - || ! args.containsKey(FS(TransitionParams::END_VALUE))) { + if (! args.containsKey(FPSTR(TransitionParams::FIELD)) + || ! args.containsKey(FPSTR(TransitionParams::END_VALUE))) { responseObj[F("error")] = F("Ignoring transition missing required arguments"); return false; } const BulbId& bulbId = currentRemote->packetFormatter->currentBulbId(); - const char* fieldName = args[FS(TransitionParams::FIELD)]; - JsonVariant startValue = args[FS(TransitionParams::START_VALUE)]; - JsonVariant endValue = args[FS(TransitionParams::END_VALUE)]; + const char* fieldName = args[FPSTR(TransitionParams::FIELD)]; + JsonVariant startValue = args[FPSTR(TransitionParams::START_VALUE)]; + JsonVariant endValue = args[FPSTR(TransitionParams::END_VALUE)]; GroupStateField field = GroupStateFieldHelpers::getFieldByName(fieldName); std::shared_ptr transitionBuilder = nullptr; @@ -547,7 +547,7 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) transitionBuilder = transitions.buildFieldTransition( bulbId, field, - startValue.isUndefined() + startValue.isNull() ? currentState->getParsedFieldValue(field) : startValue.as(), endValue @@ -560,7 +560,7 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) // Color can be decomposed into hue/saturation and these can be transitioned separately if (field == GroupStateField::COLOR) { - ParsedColor _startValue = startValue.isUndefined() + ParsedColor _startValue = startValue.isNull() ? currentState->getColor() : ParsedColor::fromJson(startValue); ParsedColor endColor = ParsedColor::fromJson(endValue); @@ -603,11 +603,11 @@ bool MiLightClient::handleTransition(JsonObject args, JsonDocument& responseObj) return false; } - if (args.containsKey(FS(TransitionParams::DURATION))) { - transitionBuilder->setDuration(args[FS(TransitionParams::DURATION)]); + if (args.containsKey(FPSTR(TransitionParams::DURATION))) { + transitionBuilder->setDuration(args[FPSTR(TransitionParams::DURATION)]); } - if (args.containsKey(FS(TransitionParams::PERIOD))) { - transitionBuilder->setPeriod(args[FS(TransitionParams::PERIOD)]); + if (args.containsKey(FPSTR(TransitionParams::PERIOD))) { + transitionBuilder->setPeriod(args[FPSTR(TransitionParams::PERIOD)]); } transitions.addTransition(transitionBuilder->build()); @@ -627,15 +627,15 @@ void MiLightClient::handleEffect(const String& effect) { JsonVariant MiLightClient::extractStatus(JsonObject object) { JsonVariant status; - if (object.containsKey(FS(GroupStateFieldNames::STATUS))) { - return object[FS(GroupStateFieldNames::STATUS)]; + if (object.containsKey(FPSTR(GroupStateFieldNames::STATUS))) { + return object[FPSTR(GroupStateFieldNames::STATUS)]; } else { - return object[FS(GroupStateFieldNames::STATE)]; + return object[FPSTR(GroupStateFieldNames::STATE)]; } } uint8_t MiLightClient::parseStatus(JsonVariant val) { - if (val.isUndefined()) { + if (val.isNull()) { return STATUS_UNDEFINED; } diff --git a/lib/MiLight/MiLightClient.h b/lib/MiLight/MiLightClient.h index f8102845..d99a8dd1 100644 --- a/lib/MiLight/MiLightClient.h +++ b/lib/MiLight/MiLightClient.h @@ -17,8 +17,6 @@ //#define DEBUG_PRINTF //#define DEBUG_CLIENT_COMMANDS // enable to show each individual change command (like hue, brightness, etc) -#define FS(str) (reinterpret_cast(str)) - namespace RequestKeys { static const char TRANSITION[] = "transition"; }; diff --git a/lib/MiLightState/GroupState.cpp b/lib/MiLightState/GroupState.cpp index cd7a65fe..4e5f1b09 100644 --- a/lib/MiLightState/GroupState.cpp +++ b/lib/MiLightState/GroupState.cpp @@ -241,6 +241,7 @@ bool GroupState::isSetField(GroupStateField field) const { case GroupStateField::COLOR_TEMP: return isSetKelvin(); case GroupStateField::BULB_MODE: + case GroupStateField::COLOR_MODE: return isSetBulbMode(); default: Serial.print(F("WARNING: tried to check if unknown field was set: ")); @@ -820,7 +821,7 @@ void GroupState::applyOhColor(JsonObject state) const { ParsedColor color = getColor(); char ohColorStr[13]; - sprintf(ohColorStr, "%d,%d,%d", color.r, color.g, color.b); + snprintf_P(ohColorStr, sizeof(ohColorStr), PSTR("%d,%d,%d"), color.r, color.g, color.b); state[GroupStateFieldNames::COLOR] = ohColorStr; } @@ -829,7 +830,7 @@ void GroupState::applyHexColor(JsonObject state) const { ParsedColor color = getColor(); char hexColor[8]; - sprintf(hexColor, "#%02X%02X%02X", color.r, color.g, color.b); + snprintf_P(hexColor, sizeof(hexColor), PSTR("#%02X%02X%02X"), color.r, color.g, color.b); state[GroupStateFieldNames::COLOR] = hexColor; } @@ -855,6 +856,29 @@ void GroupState::applyField(JsonObject partialState, const BulbId& bulbId, Group partialState[GroupStateFieldNames::BULB_MODE] = BULB_MODE_NAMES[getBulbMode()]; break; + // For HomeAssistant. Should report: + // 1. "brightness" if no color temp/rgb support + // 2. "rgb" if RGB or RGBW bulb and in color mode + // 3. "color_temp" if WW or RGBW and in color temp mode + // 4. "onoff" if in night mode + case GroupStateField::COLOR_MODE: + if ( + MiLightRemoteTypeHelpers::supportsRgb(bulbId.deviceType) + && getBulbMode() == BULB_MODE_COLOR + ) { + partialState[GroupStateFieldNames::COLOR_MODE] = F("rgb"); + } else if ( + MiLightRemoteTypeHelpers::supportsColorTemp(bulbId.deviceType) + && getBulbMode() == BULB_MODE_WHITE + ) { + partialState[GroupStateFieldNames::COLOR_MODE] = F("color_temp"); + } else if (getBulbMode() == BULB_MODE_NIGHT) { + partialState[GroupStateFieldNames::COLOR_MODE] = F("onoff"); + } else { + partialState[GroupStateFieldNames::COLOR_MODE] = F("brightness"); + } + break; + case GroupStateField::COLOR: if (getBulbMode() == BULB_MODE_COLOR) { applyColor(partialState); diff --git a/lib/MiLightState/GroupStatePersistence.cpp b/lib/MiLightState/GroupStatePersistence.cpp index 14a1f718..30794765 100644 --- a/lib/MiLightState/GroupStatePersistence.cpp +++ b/lib/MiLightState/GroupStatePersistence.cpp @@ -1,5 +1,6 @@ #include #include +#include "ProjectFS.h" static const char FILE_PREFIX[] = "group_states/"; @@ -8,8 +9,8 @@ void GroupStatePersistence::get(const BulbId &id, GroupState& state) { memset(path, 0, 30); buildFilename(id, path); - if (SPIFFS.exists(path)) { - File f = SPIFFS.open(path, "r"); + if (ProjectFS.exists(path)) { + File f = ProjectFS.open(path, "r"); state.load(f); f.close(); } @@ -20,7 +21,7 @@ void GroupStatePersistence::set(const BulbId &id, const GroupState& state) { memset(path, 0, 30); buildFilename(id, path); - File f = SPIFFS.open(path, "w"); + File f = ProjectFS.open(path, "w"); state.dump(f); f.close(); } @@ -29,8 +30,8 @@ void GroupStatePersistence::clear(const BulbId &id) { char path[30]; buildFilename(id, path); - if (SPIFFS.exists(path)) { - SPIFFS.remove(path); + if (ProjectFS.exists(path)) { + ProjectFS.remove(path); } } diff --git a/lib/Radio/MiLightRadio.h b/lib/Radio/MiLightRadio.h index df533cf1..2df9de0c 100644 --- a/lib/Radio/MiLightRadio.h +++ b/lib/Radio/MiLightRadio.h @@ -14,13 +14,13 @@ class MiLightRadio { public: - virtual int begin(); - virtual bool available(); - virtual int read(uint8_t frame[], size_t &frame_length); - virtual int write(uint8_t frame[], size_t frame_length); - virtual int resend(); - virtual int configure(); - virtual const MiLightRadioConfig& config(); + virtual int begin() = 0; + virtual bool available() = 0; + virtual int read(uint8_t frame[], size_t &frame_length) = 0; + virtual int write(uint8_t frame[], size_t frame_length) = 0; + virtual int resend() = 0; + virtual int configure() = 0; + virtual const MiLightRadioConfig& config() = 0; }; diff --git a/lib/Settings/AboutHelper.cpp b/lib/Settings/AboutHelper.cpp index aac1233d..f5a04b4d 100644 --- a/lib/Settings/AboutHelper.cpp +++ b/lib/Settings/AboutHelper.cpp @@ -2,6 +2,12 @@ #include #include #include +#include + +extern "C" { +#include +extern cont_t* g_pcont; +}; String AboutHelper::generateAboutString(bool abbreviated) { DynamicJsonDocument buffer(1024); @@ -15,14 +21,21 @@ String AboutHelper::generateAboutString(bool abbreviated) { } void AboutHelper::generateAboutObject(JsonDocument& obj, bool abbreviated) { - obj["firmware"] = QUOTE(FIRMWARE_NAME); - obj["version"] = QUOTE(MILIGHT_HUB_VERSION); - obj["ip_address"] = WiFi.localIP().toString(); - obj["reset_reason"] = ESP.getResetReason(); + obj[FPSTR("firmware")] = QUOTE(FIRMWARE_NAME); + obj[FPSTR("version")] = QUOTE(MILIGHT_HUB_VERSION); + obj[FPSTR("ip_address")] = WiFi.localIP().toString(); + obj[FPSTR("reset_reason")] = ESP.getResetReason(); if (! abbreviated) { - obj["variant"] = QUOTE(FIRMWARE_VARIANT); - obj["free_heap"] = ESP.getFreeHeap(); - obj["arduino_version"] = ESP.getCoreVersion(); + obj[FPSTR("variant")] = QUOTE(FIRMWARE_VARIANT); + obj[FPSTR("free_heap")] = ESP.getFreeHeap(); + obj[FPSTR("arduino_version")] = ESP.getCoreVersion(); + obj[FPSTR("free_stack")] = cont_get_free_stack(g_pcont); + + FSInfo fsInfo; + ProjectFS.info(fsInfo); + obj[FPSTR("flash_used")] = fsInfo.usedBytes; + obj[FPSTR("flash_total")] = fsInfo.totalBytes; + obj[FPSTR("flash_pct_free")] = fsInfo.totalBytes == 0 ? 0 : (fsInfo.totalBytes - fsInfo.usedBytes) * 100 / fsInfo.totalBytes; } } \ No newline at end of file diff --git a/lib/Settings/BackupManager.cpp b/lib/Settings/BackupManager.cpp new file mode 100644 index 00000000..f91e7e76 --- /dev/null +++ b/lib/Settings/BackupManager.cpp @@ -0,0 +1,72 @@ +// +// Created by chris on 9/18/2023. +// + +#include +#include +#include + +const uint8_t BackupManager::SETTINGS_BACKUP_VERSION = 1; +const uint32_t BackupManager::SETTINGS_MAGIC_HEADER = 0x92A7C300 | SETTINGS_BACKUP_VERSION; + +void BackupManager::createBackup(const Settings& settings, Stream& stream) { + stream.write(reinterpret_cast(&SETTINGS_MAGIC_HEADER), sizeof(SETTINGS_MAGIC_HEADER)); + + GroupAlias::saveAliases(stream, settings.groupIdAliases); + settings.serialize(stream); +} + +BackupManager::RestoreStatus BackupManager::restoreBackup(Settings& settings, Stream& stream) { + uint32_t magicHeader = 0; + // read the header + stream.readBytes(reinterpret_cast(&magicHeader), sizeof(magicHeader)); + + // Check the header + if ((magicHeader & 0xFFFFFF00) != (SETTINGS_MAGIC_HEADER & 0xFFFFFF00)) { + Serial.printf_P(PSTR("ERROR: invalid backup file header. expected %08X but got %08X\n"), SETTINGS_MAGIC_HEADER & 0xFFFFFF00, magicHeader & 0xFFFFFF00); + return BackupManager::RestoreStatus::INVALID_HEADER; + } + + // Check the version + if ((magicHeader & 0xFF) != SETTINGS_BACKUP_VERSION) { + Serial.printf_P(PSTR("ERROR: invalid settings file version. expected %d but got %d\n"), SETTINGS_BACKUP_VERSION, magicHeader & 0xFF); + return BackupManager::RestoreStatus::INVALID_VERSION; + } + + // reset settings to default + settings = Settings(); + + Serial.printf_P(PSTR("Restoring %d byte backup\n"), stream.available()); + GroupAlias::loadAliases(stream, settings.groupIdAliases); + + // read null terminator + stream.read(); + + // Save to persist aliases + settings.save(); + + // Copy remaining part of the buffer to the settings file + + File f = ProjectFS.open(SETTINGS_FILE, "w"); + Serial.println(F("Restoring settings file")); + if (!f) { + Serial.println(F("Opening settings file failed")); + return BackupManager::RestoreStatus::INVALID_FILE; + } else { + Serial.printf_P(PSTR("%d bytes remaining in backup\n"), stream.available()); + WriteBufferingStream bufferedStream(f, 128); + + while (stream.available()) { + bufferedStream.write(stream.read()); + } + + bufferedStream.flush(); + f.close(); + } + + // Reload settings + Settings::load(settings); + settings.save(); + + return BackupManager::RestoreStatus::OK; +} \ No newline at end of file diff --git a/lib/Settings/BackupManager.h b/lib/Settings/BackupManager.h new file mode 100644 index 00000000..88d16afd --- /dev/null +++ b/lib/Settings/BackupManager.h @@ -0,0 +1,33 @@ +// +// Created by chris on 9/18/2023. +// + +#ifndef ESP8266_MILIGHT_HUB_BACKUPMANAGER_H +#define ESP8266_MILIGHT_HUB_BACKUPMANAGER_H + +#include + +class BackupManager { +public: + // last byte stores version + static const uint8_t SETTINGS_BACKUP_VERSION; + static const uint32_t SETTINGS_MAGIC_HEADER; + + enum class RestoreStatus { + OK, + INVALID_HEADER, + INVALID_VERSION, + INVALID_FILE, + INVALID_SIZE, + INVALID_NUM_ALIASES, + INVALID_ALIAS, + INVALID_SETTINGS, + UNKNOWN_ERROR + }; + + static void createBackup(const Settings& settings, Stream& stream); + static RestoreStatus restoreBackup(Settings& settings, Stream& stream); +}; + + +#endif //ESP8266_MILIGHT_HUB_BACKUPMANAGER_H diff --git a/lib/Settings/Settings.cpp b/lib/Settings/Settings.cpp index 04100395..c4329498 100644 --- a/lib/Settings/Settings.cpp +++ b/lib/Settings/Settings.cpp @@ -1,9 +1,11 @@ #include #include -#include #include #include #include +#include +#include +#include #define PORT_POSITION(s) ( s.indexOf(':') ) @@ -67,102 +69,104 @@ void Settings::patch(JsonObject parsedSettings) { return; } - this->setIfPresent(parsedSettings, "admin_username", adminUsername); - this->setIfPresent(parsedSettings, "admin_password", adminPassword); - this->setIfPresent(parsedSettings, "ce_pin", cePin); - this->setIfPresent(parsedSettings, "csn_pin", csnPin); - this->setIfPresent(parsedSettings, "reset_pin", resetPin); - this->setIfPresent(parsedSettings, "led_pin", ledPin); - this->setIfPresent(parsedSettings, "packet_repeats", packetRepeats); - this->setIfPresent(parsedSettings, "http_repeat_factor", httpRepeatFactor); - this->setIfPresent(parsedSettings, "auto_restart_period", _autoRestartPeriod); - this->setIfPresent(parsedSettings, "mqtt_server", _mqttServer); - this->setIfPresent(parsedSettings, "mqtt_username", mqttUsername); - this->setIfPresent(parsedSettings, "mqtt_password", mqttPassword); - this->setIfPresent(parsedSettings, "mqtt_topic_pattern", mqttTopicPattern); - this->setIfPresent(parsedSettings, "mqtt_update_topic_pattern", mqttUpdateTopicPattern); - this->setIfPresent(parsedSettings, "mqtt_state_topic_pattern", mqttStateTopicPattern); - this->setIfPresent(parsedSettings, "mqtt_client_status_topic", mqttClientStatusTopic); - this->setIfPresent(parsedSettings, "simple_mqtt_client_status", simpleMqttClientStatus); - this->setIfPresent(parsedSettings, "discovery_port", discoveryPort); - this->setIfPresent(parsedSettings, "listen_repeats", listenRepeats); - this->setIfPresent(parsedSettings, "state_flush_interval", stateFlushInterval); - this->setIfPresent(parsedSettings, "mqtt_state_rate_limit", mqttStateRateLimit); - this->setIfPresent(parsedSettings, "mqtt_debounce_delay", mqttDebounceDelay); - this->setIfPresent(parsedSettings, "mqtt_retain", mqttRetain); - this->setIfPresent(parsedSettings, "packet_repeat_throttle_threshold", packetRepeatThrottleThreshold); - this->setIfPresent(parsedSettings, "packet_repeat_throttle_sensitivity", packetRepeatThrottleSensitivity); - this->setIfPresent(parsedSettings, "packet_repeat_minimum", packetRepeatMinimum); - this->setIfPresent(parsedSettings, "enable_automatic_mode_switching", enableAutomaticModeSwitching); - this->setIfPresent(parsedSettings, "led_mode_packet_count", ledModePacketCount); - this->setIfPresent(parsedSettings, "hostname", hostname); - this->setIfPresent(parsedSettings, "wifi_static_ip", wifiStaticIP); - this->setIfPresent(parsedSettings, "wifi_static_ip_gateway", wifiStaticIPGateway); - this->setIfPresent(parsedSettings, "wifi_static_ip_netmask", wifiStaticIPNetmask); - this->setIfPresent(parsedSettings, "packet_repeats_per_loop", packetRepeatsPerLoop); - this->setIfPresent(parsedSettings, "home_assistant_discovery_prefix", homeAssistantDiscoveryPrefix); - this->setIfPresent(parsedSettings, "default_transition_period", defaultTransitionPeriod); - - if (parsedSettings.containsKey("wifi_mode")) { - this->wifiMode = wifiModeFromString(parsedSettings["wifi_mode"]); - } - - if (parsedSettings.containsKey("rf24_channels")) { - JsonArray arr = parsedSettings["rf24_channels"]; + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::ADMIN_USERNAME), adminUsername); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::ADMIN_PASSWORD), adminPassword); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::CE_PIN), cePin); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::CSN_PIN), csnPin); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::RESET_PIN), resetPin); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::LED_PIN), ledPin); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::PACKET_REPEATS), packetRepeats); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::HTTP_REPEAT_FACTOR), httpRepeatFactor); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::AUTO_RESTART_PERIOD), _autoRestartPeriod); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_SERVER), _mqttServer); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_USERNAME), mqttUsername); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_PASSWORD), mqttPassword); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_TOPIC_PATTERN), mqttTopicPattern); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_UPDATE_TOPIC_PATTERN), mqttUpdateTopicPattern); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_STATE_TOPIC_PATTERN), mqttStateTopicPattern); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_CLIENT_STATUS_TOPIC), mqttClientStatusTopic); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::SIMPLE_MQTT_CLIENT_STATUS), simpleMqttClientStatus); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::DISCOVERY_PORT), discoveryPort); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::LISTEN_REPEATS), listenRepeats); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::STATE_FLUSH_INTERVAL), stateFlushInterval); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_STATE_RATE_LIMIT), mqttStateRateLimit); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_DEBOUNCE_DELAY), mqttDebounceDelay); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::MQTT_RETAIN), mqttRetain); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::PACKET_REPEAT_THROTTLE_THRESHOLD), packetRepeatThrottleThreshold); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::PACKET_REPEAT_THROTTLE_SENSITIVITY), packetRepeatThrottleSensitivity); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::PACKET_REPEAT_MINIMUM), packetRepeatMinimum); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::ENABLE_AUTOMATIC_MODE_SWITCHING), enableAutomaticModeSwitching); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::LED_MODE_PACKET_COUNT), ledModePacketCount); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::HOSTNAME), hostname); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::WIFI_STATIC_IP), wifiStaticIP); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::WIFI_STATIC_IP_GATEWAY), wifiStaticIPGateway); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::WIFI_STATIC_IP_NETMASK), wifiStaticIPNetmask); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::PACKET_REPEATS_PER_LOOP), packetRepeatsPerLoop); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::HOME_ASSISTANT_DISCOVERY_PREFIX), homeAssistantDiscoveryPrefix); + this->setIfPresent(parsedSettings, FPSTR(SettingsKeys::DEFAULT_TRANSITION_PERIOD), defaultTransitionPeriod); + + if (parsedSettings.containsKey(FPSTR(SettingsKeys::WIFI_MODE))) { + this->wifiMode = wifiModeFromString(parsedSettings[FPSTR(SettingsKeys::WIFI_MODE)]); + } + + if (parsedSettings.containsKey(FPSTR(SettingsKeys::RF24_CHANNELS))) { + JsonArray arr = parsedSettings[FPSTR(SettingsKeys::RF24_CHANNELS)]; rf24Channels = JsonHelpers::jsonArrToVector(arr, RF24ChannelHelpers::valueFromName); } - if (parsedSettings.containsKey("rf24_listen_channel")) { - this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings["rf24_listen_channel"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::RF24_LISTEN_CHANNEL))) { + this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings[FPSTR(SettingsKeys::RF24_LISTEN_CHANNEL)]); } - if (parsedSettings.containsKey("rf24_power_level")) { - this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings["rf24_power_level"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::RF24_POWER_LEVEL))) { + this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings[FPSTR(SettingsKeys::RF24_POWER_LEVEL)]); } - if (parsedSettings.containsKey("led_mode_wifi_config")) { - this->ledModeWifiConfig = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_config"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::LED_MODE_WIFI_CONFIG))) { + this->ledModeWifiConfig = LEDStatus::stringToLEDMode(parsedSettings[FPSTR(SettingsKeys::LED_MODE_WIFI_CONFIG)]); } - if (parsedSettings.containsKey("led_mode_wifi_failed")) { - this->ledModeWifiFailed = LEDStatus::stringToLEDMode(parsedSettings["led_mode_wifi_failed"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::LED_MODE_WIFI_FAILED))) { + this->ledModeWifiFailed = LEDStatus::stringToLEDMode(parsedSettings[FPSTR(SettingsKeys::LED_MODE_WIFI_FAILED)]); } - if (parsedSettings.containsKey("led_mode_operating")) { - this->ledModeOperating = LEDStatus::stringToLEDMode(parsedSettings["led_mode_operating"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::LED_MODE_OPERATING))) { + this->ledModeOperating = LEDStatus::stringToLEDMode(parsedSettings[FPSTR(SettingsKeys::LED_MODE_OPERATING)]); } - if (parsedSettings.containsKey("led_mode_packet")) { - this->ledModePacket = LEDStatus::stringToLEDMode(parsedSettings["led_mode_packet"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::LED_MODE_PACKET))) { + this->ledModePacket = LEDStatus::stringToLEDMode(parsedSettings[FPSTR(SettingsKeys::LED_MODE_PACKET)]); } - if (parsedSettings.containsKey("radio_interface_type")) { - this->radioInterfaceType = Settings::typeFromString(parsedSettings["radio_interface_type"]); + if (parsedSettings.containsKey(FPSTR(SettingsKeys::RADIO_INTERFACE_TYPE))) { + this->radioInterfaceType = Settings::typeFromString(parsedSettings[FPSTR(SettingsKeys::RADIO_INTERFACE_TYPE)]); } - if (parsedSettings.containsKey("device_ids")) { - JsonArray arr = parsedSettings["device_ids"]; + if (parsedSettings.containsKey(FPSTR(SettingsKeys::DEVICE_IDS))) { + JsonArray arr = parsedSettings[FPSTR(SettingsKeys::DEVICE_IDS)]; updateDeviceIds(arr); } - if (parsedSettings.containsKey("gateway_configs")) { - JsonArray arr = parsedSettings["gateway_configs"]; + if (parsedSettings.containsKey(FPSTR(SettingsKeys::GATEWAY_CONFIGS))) { + JsonArray arr = parsedSettings[FPSTR(SettingsKeys::GATEWAY_CONFIGS)]; updateGatewayConfigs(arr); } - if (parsedSettings.containsKey("group_state_fields")) { - JsonArray arr = parsedSettings["group_state_fields"]; + if (parsedSettings.containsKey(FPSTR(SettingsKeys::GROUP_STATE_FIELDS))) { + JsonArray arr = parsedSettings[FPSTR(SettingsKeys::GROUP_STATE_FIELDS)]; groupStateFields = JsonHelpers::jsonArrToVector(arr, GroupStateFieldHelpers::getFieldByName); } - if (parsedSettings.containsKey("group_id_aliases")) { + // this key will only be present in old settings files, but for backwards + // compatability, parse it if it's present. + if (parsedSettings.containsKey(FPSTR(SettingsKeys::GROUP_ID_ALIASES))) { parseGroupIdAliases(parsedSettings); } } -std::map::const_iterator Settings::findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId) { +std::map::const_iterator Settings::findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId) { BulbId searchId{ deviceId, groupId, deviceType }; for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) { - if (searchId == it->second) { + if (searchId == it->second.bulbId) { return it; } } @@ -171,15 +175,16 @@ std::map::const_iterator Settings::findAlias(MiLightRemoteType d } void Settings::parseGroupIdAliases(JsonObject json) { - JsonObject aliases = json["group_id_aliases"]; + JsonObject aliases = json[FPSTR(SettingsKeys::GROUP_ID_ALIASES)].as(); // Save group IDs that were deleted so that they can be processed by discovery // if necessary for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) { - deletedGroupIdAliases[it->second.getCompactId()] = it->second; + deletedGroupIdAliases[it->second.bulbId.getCompactId()] = it->second.bulbId; } groupIdAliases.clear(); + size_t id = 1; for (JsonPair kv : aliases) { JsonArray bulbIdProps = kv.value(); @@ -188,7 +193,7 @@ void Settings::parseGroupIdAliases(JsonObject json) { bulbIdProps[2].as(), MiLightRemoteTypeHelpers::remoteTypeFromString(bulbIdProps[0].as()) }; - groupIdAliases[kv.key().c_str()] = bulbId; + groupIdAliases[kv.key().c_str()] = GroupAlias(id++, kv.key().c_str(), bulbId); // If added this round, do not mark as deleted. deletedGroupIdAliases.erase(bulbId.getCompactId()); @@ -196,23 +201,46 @@ void Settings::parseGroupIdAliases(JsonObject json) { } void Settings::dumpGroupIdAliases(JsonObject json) { - JsonObject aliases = json.createNestedObject("group_id_aliases"); + JsonObject aliases = json.createNestedObject(FPSTR(SettingsKeys::GROUP_ID_ALIASES)); - for (std::map::iterator itr = groupIdAliases.begin(); itr != groupIdAliases.end(); ++itr) { - JsonArray bulbProps = aliases.createNestedArray(itr->first); - BulbId bulbId = itr->second; + for (auto & groupIdAlias : groupIdAliases) { + JsonArray bulbProps = aliases.createNestedArray(groupIdAlias.first); + BulbId bulbId = groupIdAlias.second.bulbId; bulbProps.add(MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType)); bulbProps.add(bulbId.deviceId); bulbProps.add(bulbId.groupId); } } -void Settings::load(Settings& settings) { - if (SPIFFS.exists(SETTINGS_FILE)) { +bool Settings::loadAliases(Settings &settings) { + if (ProjectFS.exists(ALIASES_FILE)) { + File f = ProjectFS.open(ALIASES_FILE, "r"); + ReadBufferingStream bufferedReader{f, 64}; + GroupAlias::loadAliases(bufferedReader, settings.groupIdAliases); + + // find current max id + size_t maxId = 0; + for (auto & alias : settings.groupIdAliases) { + maxId = max(maxId, alias.second.id); + } + settings.groupIdAliasNextId = maxId + 1; + + printf_P(PSTR("loaded %d aliases\n"), settings.groupIdAliases.size()); + + return true; + } else { + return false; + } +} + +bool Settings::load(Settings& settings) { + bool shouldInit = false; + + if (ProjectFS.exists(SETTINGS_FILE)) { // Clear in-memory settings settings = Settings(); - File f = SPIFFS.open(SETTINGS_FILE, "r"); + File f = ProjectFS.open(SETTINGS_FILE, "r"); DynamicJsonDocument json(MILIGHT_HUB_SETTINGS_BUFFER_SIZE); auto error = deserializeJson(json, f); @@ -224,84 +252,113 @@ void Settings::load(Settings& settings) { } else { Serial.print(F("Error parsing saved settings file: ")); Serial.println(error.c_str()); + Serial.println(F("contents:")); + + f = ProjectFS.open(SETTINGS_FILE, "r"); + Serial.println(f.readString()); + + return false; } } else { + shouldInit = true; + } + + // If we loaded aliases from the settings file but not the aliases file, + // port them over to the aliases file. + const bool settingKeyAliasesEmpty = settings.groupIdAliases.empty(); + const bool aliasesFileEmpty = loadAliases(settings); + + if (!settingKeyAliasesEmpty && aliasesFileEmpty) { + Serial.println(F("Porting aliases from settings file to aliases file")); + shouldInit = true; + } + + if (shouldInit) { settings.save(); } -} -String Settings::toJson(const bool prettyPrint) { - String buffer = ""; - StringStream s(buffer); - serialize(s, prettyPrint); - return buffer; + return true; } void Settings::save() { - File f = SPIFFS.open(SETTINGS_FILE, "w"); + File f = ProjectFS.open(SETTINGS_FILE, "w"); if (!f) { Serial.println(F("Opening settings file failed")); + return; } else { + WriteBufferingStream writer{f, 64}; serialize(f); + writer.flush(); f.close(); } + + File aliasesFile = ProjectFS.open(ALIASES_FILE, "w"); + + if (!aliasesFile) { + Serial.println(F("Opening aliases file failed")); + } else { + WriteBufferingStream aliases{aliasesFile, 64}; + GroupAlias::saveAliases(aliases, groupIdAliases); + aliases.flush(); + aliasesFile.close(); + } } -void Settings::serialize(Print& stream, const bool prettyPrint) { +void Settings::serialize(Print& stream, const bool prettyPrint) const { DynamicJsonDocument root(MILIGHT_HUB_SETTINGS_BUFFER_SIZE); - root["admin_username"] = this->adminUsername; - root["admin_password"] = this->adminPassword; - root["ce_pin"] = this->cePin; - root["csn_pin"] = this->csnPin; - root["reset_pin"] = this->resetPin; - root["led_pin"] = this->ledPin; - root["radio_interface_type"] = typeToString(this->radioInterfaceType); - root["packet_repeats"] = this->packetRepeats; - root["http_repeat_factor"] = this->httpRepeatFactor; - root["auto_restart_period"] = this->_autoRestartPeriod; - root["mqtt_server"] = this->_mqttServer; - root["mqtt_username"] = this->mqttUsername; - root["mqtt_password"] = this->mqttPassword; - root["mqtt_topic_pattern"] = this->mqttTopicPattern; - root["mqtt_update_topic_pattern"] = this->mqttUpdateTopicPattern; - root["mqtt_state_topic_pattern"] = this->mqttStateTopicPattern; - root["mqtt_client_status_topic"] = this->mqttClientStatusTopic; - root["simple_mqtt_client_status"] = this->simpleMqttClientStatus; - root["discovery_port"] = this->discoveryPort; - root["listen_repeats"] = this->listenRepeats; - root["state_flush_interval"] = this->stateFlushInterval; - root["mqtt_state_rate_limit"] = this->mqttStateRateLimit; - root["mqtt_debounce_delay"] = this->mqttDebounceDelay; - root["mqtt_retain"] = this->mqttRetain; - root["packet_repeat_throttle_sensitivity"] = this->packetRepeatThrottleSensitivity; - root["packet_repeat_throttle_threshold"] = this->packetRepeatThrottleThreshold; - root["packet_repeat_minimum"] = this->packetRepeatMinimum; - root["enable_automatic_mode_switching"] = this->enableAutomaticModeSwitching; - root["led_mode_wifi_config"] = LEDStatus::LEDModeToString(this->ledModeWifiConfig); - root["led_mode_wifi_failed"] = LEDStatus::LEDModeToString(this->ledModeWifiFailed); - root["led_mode_operating"] = LEDStatus::LEDModeToString(this->ledModeOperating); - root["led_mode_packet"] = LEDStatus::LEDModeToString(this->ledModePacket); - root["led_mode_packet_count"] = this->ledModePacketCount; - root["hostname"] = this->hostname; - root["rf24_power_level"] = RF24PowerLevelHelpers::nameFromValue(this->rf24PowerLevel); - root["rf24_listen_channel"] = RF24ChannelHelpers::nameFromValue(rf24ListenChannel); - root["wifi_static_ip"] = this->wifiStaticIP; - root["wifi_static_ip_gateway"] = this->wifiStaticIPGateway; - root["wifi_static_ip_netmask"] = this->wifiStaticIPNetmask; - root["packet_repeats_per_loop"] = this->packetRepeatsPerLoop; - root["home_assistant_discovery_prefix"] = this->homeAssistantDiscoveryPrefix; - root["wifi_mode"] = wifiModeToString(this->wifiMode); - root["default_transition_period"] = this->defaultTransitionPeriod; - - JsonArray channelArr = root.createNestedArray("rf24_channels"); + root[FPSTR(SettingsKeys::ADMIN_USERNAME)] = this->adminUsername; + root[FPSTR(SettingsKeys::ADMIN_PASSWORD)] = this->adminPassword; + root[FPSTR(SettingsKeys::CE_PIN)] = this->cePin; + root[FPSTR(SettingsKeys::CSN_PIN)] = this->csnPin; + root[FPSTR(SettingsKeys::RESET_PIN)] = this->resetPin; + root[FPSTR(SettingsKeys::LED_PIN)] = this->ledPin; + root[FPSTR(SettingsKeys::RADIO_INTERFACE_TYPE)] = typeToString(this->radioInterfaceType); + root[FPSTR(SettingsKeys::PACKET_REPEATS)] = this->packetRepeats; + root[FPSTR(SettingsKeys::HTTP_REPEAT_FACTOR)] = this->httpRepeatFactor; + root[FPSTR(SettingsKeys::AUTO_RESTART_PERIOD)] = this->_autoRestartPeriod; + root[FPSTR(SettingsKeys::MQTT_SERVER)] = this->_mqttServer; + root[FPSTR(SettingsKeys::MQTT_USERNAME)] = this->mqttUsername; + root[FPSTR(SettingsKeys::MQTT_PASSWORD)] = this->mqttPassword; + root[FPSTR(SettingsKeys::MQTT_TOPIC_PATTERN)] = this->mqttTopicPattern; + root[FPSTR(SettingsKeys::MQTT_UPDATE_TOPIC_PATTERN)] = this->mqttUpdateTopicPattern; + root[FPSTR(SettingsKeys::MQTT_STATE_TOPIC_PATTERN)] = this->mqttStateTopicPattern; + root[FPSTR(SettingsKeys::MQTT_CLIENT_STATUS_TOPIC)] = this->mqttClientStatusTopic; + root[FPSTR(SettingsKeys::SIMPLE_MQTT_CLIENT_STATUS)] = this->simpleMqttClientStatus; + root[FPSTR(SettingsKeys::DISCOVERY_PORT)] = this->discoveryPort; + root[FPSTR(SettingsKeys::LISTEN_REPEATS)] = this->listenRepeats; + root[FPSTR(SettingsKeys::STATE_FLUSH_INTERVAL)] = this->stateFlushInterval; + root[FPSTR(SettingsKeys::MQTT_STATE_RATE_LIMIT)] = this->mqttStateRateLimit; + root[FPSTR(SettingsKeys::MQTT_DEBOUNCE_DELAY)] = this->mqttDebounceDelay; + root[FPSTR(SettingsKeys::MQTT_RETAIN)] = this->mqttRetain; + root[FPSTR(SettingsKeys::PACKET_REPEAT_THROTTLE_SENSITIVITY)] = this->packetRepeatThrottleSensitivity; + root[FPSTR(SettingsKeys::PACKET_REPEAT_THROTTLE_THRESHOLD)] = this->packetRepeatThrottleThreshold; + root[FPSTR(SettingsKeys::PACKET_REPEAT_MINIMUM)] = this->packetRepeatMinimum; + root[FPSTR(SettingsKeys::ENABLE_AUTOMATIC_MODE_SWITCHING)] = this->enableAutomaticModeSwitching; + root[FPSTR(SettingsKeys::LED_MODE_WIFI_CONFIG)] = LEDStatus::LEDModeToString(this->ledModeWifiConfig); + root[FPSTR(SettingsKeys::LED_MODE_WIFI_FAILED)] = LEDStatus::LEDModeToString(this->ledModeWifiFailed); + root[FPSTR(SettingsKeys::LED_MODE_OPERATING)] = LEDStatus::LEDModeToString(this->ledModeOperating); + root[FPSTR(SettingsKeys::LED_MODE_PACKET)] = LEDStatus::LEDModeToString(this->ledModePacket); + root[FPSTR(SettingsKeys::LED_MODE_PACKET_COUNT)] = this->ledModePacketCount; + root[FPSTR(SettingsKeys::HOSTNAME)] = this->hostname; + root[FPSTR(SettingsKeys::RF24_POWER_LEVEL)] = RF24PowerLevelHelpers::nameFromValue(this->rf24PowerLevel); + root[FPSTR(SettingsKeys::RF24_LISTEN_CHANNEL)] = RF24ChannelHelpers::nameFromValue(rf24ListenChannel); + root[FPSTR(SettingsKeys::WIFI_STATIC_IP)] = this->wifiStaticIP; + root[FPSTR(SettingsKeys::WIFI_STATIC_IP_GATEWAY)] = this->wifiStaticIPGateway; + root[FPSTR(SettingsKeys::WIFI_STATIC_IP_NETMASK)] = this->wifiStaticIPNetmask; + root[FPSTR(SettingsKeys::PACKET_REPEATS_PER_LOOP)] = this->packetRepeatsPerLoop; + root[FPSTR(SettingsKeys::HOME_ASSISTANT_DISCOVERY_PREFIX)] = this->homeAssistantDiscoveryPrefix; + root[FPSTR(SettingsKeys::WIFI_MODE)] = wifiModeToString(this->wifiMode); + root[FPSTR(SettingsKeys::DEFAULT_TRANSITION_PERIOD)] = this->defaultTransitionPeriod; + + JsonArray channelArr = root.createNestedArray(FPSTR(SettingsKeys::RF24_CHANNELS)); JsonHelpers::vectorToJsonArr(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue); - JsonArray deviceIdsArr = root.createNestedArray("device_ids"); + JsonArray deviceIdsArr = root.createNestedArray(FPSTR(SettingsKeys::DEVICE_IDS)); JsonHelpers::copyFrom(deviceIdsArr, this->deviceIds); - JsonArray gatewayConfigsArr = root.createNestedArray("gateway_configs"); + JsonArray gatewayConfigsArr = root.createNestedArray(FPSTR(SettingsKeys::GATEWAY_CONFIGS)); for (size_t i = 0; i < this->gatewayConfigs.size(); i++) { JsonArray elmt = gatewayConfigsArr.createNestedArray(); elmt.add(this->gatewayConfigs[i]->deviceId); @@ -309,11 +366,9 @@ void Settings::serialize(Print& stream, const bool prettyPrint) { elmt.add(this->gatewayConfigs[i]->protocolVersion); } - JsonArray groupStateFieldArr = root.createNestedArray("group_state_fields"); + JsonArray groupStateFieldArr = root.createNestedArray(FPSTR(SettingsKeys::GROUP_STATE_FIELDS)); JsonHelpers::vectorToJsonArr(groupStateFieldArr, groupStateFields, GroupStateFieldHelpers::getFieldName); - dumpGroupIdAliases(root.as()); - if (prettyPrint) { serializeJsonPretty(root, stream); } else { @@ -380,4 +435,31 @@ String Settings::wifiModeToString(WifiMode mode) { default: return "n"; } +} + +void Settings::addAlias(const char *alias, const BulbId &bulbId) { + groupIdAliases[alias] = GroupAlias(groupIdAliasNextId++, alias, bulbId); +} + +bool Settings::deleteAlias(size_t id) { + for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) { + if (it->second.id == id) { + groupIdAliases.erase(it); + deletedGroupIdAliases[it->second.bulbId.getCompactId()] = it->second.bulbId; + + return true; + } + } + + return false; +} + +std::map::const_iterator Settings::findAliasById(size_t id) { + for (auto it = groupIdAliases.begin(); it != groupIdAliases.end(); ++it) { + if (it->second.id == id) { + return it; + } + } + + return groupIdAliases.end(); } \ No newline at end of file diff --git a/lib/Settings/Settings.h b/lib/Settings/Settings.h index bb95d386..c3067623 100644 --- a/lib/Settings/Settings.h +++ b/lib/Settings/Settings.h @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -7,6 +6,7 @@ #include #include #include +#include #include #include @@ -47,6 +47,8 @@ #define SETTINGS_FILE "/config.json" #define SETTINGS_TERMINATOR '\0' +#define ALIASES_FILE "/aliases.bin" +#define BACKUP_FILE "_backup.bin" #define WEB_INDEX_FILENAME "/web/index.html" @@ -73,7 +75,7 @@ static const std::vector DEFAULT_GROUP_STATE_FIELDS({ GroupStateField::COMPUTED_COLOR, GroupStateField::MODE, GroupStateField::COLOR_TEMP, - GroupStateField::BULB_MODE + GroupStateField::COLOR_MODE }); struct GatewayConfig { @@ -84,8 +86,61 @@ struct GatewayConfig { const uint8_t protocolVersion; }; +// all keys that appear in JSON +namespace SettingsKeys { + static const char ADMIN_USERNAME[] PROGMEM = "admin_username"; + static const char ADMIN_PASSWORD[] PROGMEM = "admin_password"; + static const char CE_PIN[] PROGMEM = "ce_pin"; + static const char CSN_PIN[] PROGMEM = "csn_pin"; + static const char RESET_PIN[] PROGMEM = "reset_pin"; + static const char LED_PIN[] PROGMEM = "led_pin"; + static const char PACKET_REPEATS[] PROGMEM = "packet_repeats"; + static const char HTTP_REPEAT_FACTOR[] PROGMEM = "http_repeat_factor"; + static const char AUTO_RESTART_PERIOD[] PROGMEM = "auto_restart_period"; + static const char MQTT_SERVER[] PROGMEM = "mqtt_server"; + static const char MQTT_USERNAME[] PROGMEM = "mqtt_username"; + static const char MQTT_PASSWORD[] PROGMEM = "mqtt_password"; + static const char MQTT_TOPIC_PATTERN[] PROGMEM = "mqtt_topic_pattern"; + static const char MQTT_UPDATE_TOPIC_PATTERN[] PROGMEM = "mqtt_update_topic_pattern"; + static const char MQTT_STATE_TOPIC_PATTERN[] PROGMEM = "mqtt_state_topic_pattern"; + static const char MQTT_CLIENT_STATUS_TOPIC[] PROGMEM = "mqtt_client_status_topic"; + static const char SIMPLE_MQTT_CLIENT_STATUS[] PROGMEM = "simple_mqtt_client_status"; + static const char DISCOVERY_PORT[] PROGMEM = "discovery_port"; + static const char LISTEN_REPEATS[] PROGMEM = "listen_repeats"; + static const char STATE_FLUSH_INTERVAL[] PROGMEM = "state_flush_interval"; + static const char MQTT_STATE_RATE_LIMIT[] PROGMEM = "mqtt_state_rate_limit"; + static const char MQTT_DEBOUNCE_DELAY[] PROGMEM = "mqtt_debounce_delay"; + static const char MQTT_RETAIN[] PROGMEM = "mqtt_retain"; + static const char PACKET_REPEAT_THROTTLE_THRESHOLD[] PROGMEM = "packet_repeat_throttle_threshold"; + static const char PACKET_REPEAT_THROTTLE_SENSITIVITY[] PROGMEM = "packet_repeat_throttle_sensitivity"; + static const char PACKET_REPEAT_MINIMUM[] PROGMEM = "packet_repeat_minimum"; + static const char ENABLE_AUTOMATIC_MODE_SWITCHING[] PROGMEM = "enable_automatic_mode_switching"; + static const char LED_MODE_PACKET_COUNT[] PROGMEM = "led_mode_packet_count"; + static const char HOSTNAME[] PROGMEM = "hostname"; + static const char WIFI_STATIC_IP[] PROGMEM = "wifi_static_ip"; + static const char WIFI_STATIC_IP_GATEWAY[] PROGMEM = "wifi_static_ip_gateway"; + static const char WIFI_STATIC_IP_NETMASK[] PROGMEM = "wifi_static_ip_netmask"; + static const char PACKET_REPEATS_PER_LOOP[] PROGMEM = "packet_repeats_per_loop"; + static const char HOME_ASSISTANT_DISCOVERY_PREFIX[] PROGMEM = "home_assistant_discovery_prefix"; + static const char DEFAULT_TRANSITION_PERIOD[] PROGMEM = "default_transition_period"; + static const char WIFI_MODE[] PROGMEM = "wifi_mode"; + static const char RF24_CHANNELS[] PROGMEM = "rf24_channels"; + static const char RF24_LISTEN_CHANNEL[] PROGMEM = "rf24_listen_channel"; + static const char RF24_POWER_LEVEL[] PROGMEM = "rf24_power_level"; + static const char LED_MODE_WIFI_CONFIG[] PROGMEM = "led_mode_wifi_config"; + static const char LED_MODE_WIFI_FAILED[] PROGMEM = "led_mode_wifi_failed"; + static const char LED_MODE_OPERATING[] PROGMEM = "led_mode_operating"; + static const char LED_MODE_PACKET[] PROGMEM = "led_mode_packet"; + static const char RADIO_INTERFACE_TYPE[] PROGMEM = "radio_interface_type"; + static const char DEVICE_IDS[] PROGMEM = "device_ids"; + static const char GATEWAY_CONFIGS[] PROGMEM = "gateway_configs"; + static const char GROUP_STATE_FIELDS[] PROGMEM = "group_state_fields"; + static const char GROUP_ID_ALIASES[] PROGMEM = "group_id_aliases"; +} + class Settings { public: + Settings() : adminUsername(""), adminPassword(""), @@ -119,12 +174,13 @@ class Settings { groupStateFields(DEFAULT_GROUP_STATE_FIELDS), rf24ListenChannel(RF24Channel::RF24_LOW), packetRepeatsPerLoop(10), - wifiMode(WifiMode::N), + wifiMode(WifiMode::G), defaultTransitionPeriod(500), + groupIdAliasNextId(0), _autoRestartPeriod(0) { } - ~Settings() { } + ~Settings() = default; bool isAuthenticationEnabled() const; const String& getUsername() const; @@ -133,21 +189,24 @@ class Settings { bool isAutoRestartEnabled(); size_t getAutoRestartPeriod(); - static void load(Settings& settings); + static bool load(Settings& settings); + static bool loadAliases(Settings& settings); static RadioInterfaceType typeFromString(const String& s); static String typeToString(RadioInterfaceType type); static std::vector defaultListenChannels(); void save(); - String toJson(const bool prettyPrint = true); - void serialize(Print& stream, const bool prettyPrint = false); + void serialize(Print& stream, const bool prettyPrint = false) const; void updateDeviceIds(JsonArray arr); void updateGatewayConfigs(JsonArray arr); void patch(JsonObject obj); String mqttServer(); uint16_t mqttPort(); - std::map::const_iterator findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId); + std::map::const_iterator findAlias(MiLightRemoteType deviceType, uint16_t deviceId, uint8_t groupId); + std::map::const_iterator findAliasById(size_t id); + void addAlias(const char* alias, const BulbId& bulbId); + bool deleteAlias(size_t id); String adminUsername; String adminPassword; @@ -192,11 +251,15 @@ class Settings { String wifiStaticIPNetmask; String wifiStaticIPGateway; size_t packetRepeatsPerLoop; - std::map groupIdAliases; + std::map groupIdAliases; std::map deletedGroupIdAliases; String homeAssistantDiscoveryPrefix; WifiMode wifiMode; uint16_t defaultTransitionPeriod; + size_t groupIdAliasNextId; + + static WifiMode wifiModeFromString(const String& mode); + static String wifiModeToString(WifiMode mode); protected: size_t _autoRestartPeriod; @@ -204,14 +267,25 @@ class Settings { void parseGroupIdAliases(JsonObject json); void dumpGroupIdAliases(JsonObject json); - static WifiMode wifiModeFromString(const String& mode); - static String wifiModeToString(WifiMode mode); - template - void setIfPresent(JsonObject obj, const char* key, T& var) { + void setIfPresent(JsonObject obj, const __FlashStringHelper* key, T& var) { if (obj.containsKey(key)) { JsonVariant val = obj[key]; - var = val.as(); + + // For booleans, parse string/int + if constexpr (std::is_same_v) { + if (val.is()) { + var = val.as(); + } else if (val.is()) { + var = strcmp(val.as(), "true") == 0; + } else if (val.is()) { + var = val.as() == 1; + } else { + var = false; + } + } else { + var = val.as(); + } } } }; diff --git a/lib/Settings/StringStream.h b/lib/Settings/StringStream.h deleted file mode 100644 index a543cc6b..00000000 --- a/lib/Settings/StringStream.h +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Adapated from https://gist.github.com/cmaglie/5883185 - */ - -#ifndef _STRING_STREAM_H_INCLUDED_ -#define _STRING_STREAM_H_INCLUDED_ - -#include - -class StringStream : public Stream -{ -public: - StringStream(String &s) : string(s), position(0) { } - - // Stream methods - virtual int available() { return string.length() - position; } - virtual int read() { return position < string.length() ? string[position++] : -1; } - virtual int peek() { return position < string.length() ? string[position] : -1; } - virtual void flush() { }; - // Print methods - virtual size_t write(uint8_t c) { string += (char)c; return 1; }; - -private: - String &string; - unsigned int length; - unsigned int position; -}; - -#endif // _STRING_STREAM_H_INCLUDED_ \ No newline at end of file diff --git a/lib/Transitions/ColorTransition.cpp b/lib/Transitions/ColorTransition.cpp index 3a21f1c8..565e6db3 100644 --- a/lib/Transitions/ColorTransition.cpp +++ b/lib/Transitions/ColorTransition.cpp @@ -12,22 +12,11 @@ std::shared_ptr ColorTransition::Builder::_build() const { size_t numPeriods = getOrComputeNumPeriods(); size_t period = getOrComputePeriod(); - int16_t dr = end.r - start.r - , dg = end.g - start.g - , db = end.b - start.b; - - RgbColor stepSizes( - calculateStepSizePart(dr, duration, period), - calculateStepSizePart(dg, duration, period), - calculateStepSizePart(db, duration, period) - ); - return std::make_shared( id, bulbId, start, end, - stepSizes, duration, period, numPeriods, @@ -60,42 +49,36 @@ bool ColorTransition::RgbColor::operator==(const RgbColor& other) { ColorTransition::ColorTransition( size_t id, const BulbId& bulbId, - const ParsedColor& startColor, - const ParsedColor& endColor, - RgbColor stepSizes, + const RgbColor& startColor, + const RgbColor& endColor, size_t duration, size_t period, size_t numPeriods, TransitionFn callback -) : Transition(id, bulbId, period, callback) +) : Transition(id, bulbId, period, std::move(callback)) , endColor(endColor) , currentColor(startColor) - , stepSizes(stepSizes) - , lastHue(400) // use impossible values to force a packet send + , stepSizes( + calculateStepSizePart(endColor.r - startColor.r, duration, period), + calculateStepSizePart(endColor.g - startColor.g, duration, period), + calculateStepSizePart(endColor.b - startColor.b, duration, period)) + , lastHue(400) // use impossible values to force a packet send , lastSaturation(200) -{ - int16_t dr = endColor.r - startColor.r - , dg = endColor.g - startColor.g - , db = endColor.b - startColor.b; - // Calculate step sizes in terms of the period - stepSizes.r = calculateStepSizePart(dr, duration, period); - stepSizes.g = calculateStepSizePart(dg, duration, period); - stepSizes.b = calculateStepSizePart(db, duration, period); -} - -size_t ColorTransition::calculateMaxDistance(const ParsedColor& start, const ParsedColor& end) { - int16_t dr = end.r - start.r - , dg = end.g - start.g - , db = end.b - start.b; - - int16_t max = std::max(std::max(dr, dg), db); - int16_t min = std::min(std::min(dr, dg), db); - int16_t maxAbs = std::abs(min) > std::abs(max) ? min : max; + , sentFinalColor(false) +{ } - return maxAbs; +size_t ColorTransition::calculateMaxDistance(const RgbColor& start, const RgbColor& end) { + // return max distance between any of R/G/B + return std::max( + std::max( + std::abs(start.r - end.r), + std::abs(start.g - end.g) + ), + std::abs(start.b - end.b) + ); } -size_t ColorTransition::calculateColorPeriod(ColorTransition* t, const ParsedColor& start, const ParsedColor& end, size_t stepSize, size_t duration) { +size_t ColorTransition::calculateColorPeriod(ColorTransition* t, const RgbColor& start, const RgbColor& end, size_t stepSize, size_t duration) { return Transition::calculatePeriod(calculateMaxDistance(start, end), stepSize, duration); } @@ -104,7 +87,7 @@ int16_t ColorTransition::calculateStepSizePart(int16_t distance, size_t duration int16_t rounded = std::ceil(std::abs(stepSize)); if (distance < 0) { - rounded = -rounded; + rounded *= -1; } return rounded; @@ -117,20 +100,23 @@ void ColorTransition::step() { callback(bulbId, GroupStateField::HUE, parsedColor.hue); lastHue = parsedColor.hue; } + if (parsedColor.saturation != lastSaturation) { callback(bulbId, GroupStateField::SATURATION, parsedColor.saturation); lastSaturation = parsedColor.saturation; } - + if (!isFinished()) { Transition::stepValue(currentColor.r, endColor.r, stepSizes.r); Transition::stepValue(currentColor.g, endColor.g, stepSizes.g); Transition::stepValue(currentColor.b, endColor.b, stepSizes.b); + } else { + this->sentFinalColor = true; } } bool ColorTransition::isFinished() { - return currentColor == endColor; + return this->sentFinalColor; } void ColorTransition::childSerialize(JsonObject& json) { diff --git a/lib/Transitions/ColorTransition.h b/lib/Transitions/ColorTransition.h index 2868037e..cd0e6fbd 100644 --- a/lib/Transitions/ColorTransition.h +++ b/lib/Transitions/ColorTransition.h @@ -21,25 +21,23 @@ class ColorTransition : public Transition { virtual std::shared_ptr _build() const override; private: - const ParsedColor& start; - const ParsedColor& end; - RgbColor stepSizes; + RgbColor start; + RgbColor end; }; ColorTransition( size_t id, const BulbId& bulbId, - const ParsedColor& startColor, - const ParsedColor& endColor, - RgbColor stepSizes, + const RgbColor& startColor, + const RgbColor& endColor, size_t duration, size_t period, size_t numPeriods, TransitionFn callback ); - static size_t calculateColorPeriod(ColorTransition* t, const ParsedColor& start, const ParsedColor& end, size_t stepSize, size_t duration); - inline static size_t calculateMaxDistance(const ParsedColor& start, const ParsedColor& end); + static size_t calculateColorPeriod(ColorTransition* t, const RgbColor& start, const RgbColor& end, size_t stepSize, size_t duration); + inline static size_t calculateMaxDistance(const RgbColor& start, const RgbColor& end); inline static int16_t calculateStepSizePart(int16_t distance, size_t duration, size_t period); virtual bool isFinished() override; @@ -51,6 +49,7 @@ class ColorTransition : public Transition { // Store these to avoid wasted packets uint16_t lastHue; uint16_t lastSaturation; + bool sentFinalColor; virtual void step() override; virtual void childSerialize(JsonObject& json) override; diff --git a/lib/Transitions/Transition.cpp b/lib/Transitions/Transition.cpp index b1fead15..2bb06862 100644 --- a/lib/Transitions/Transition.cpp +++ b/lib/Transitions/Transition.cpp @@ -2,6 +2,13 @@ #include #include +// transition commands are in seconds, convert to ms. +const uint16_t Transition::DURATION_UNIT_MULTIPLIER = 1000; + +// If period goes lower than this, throttle other parameters up to adjust. +const size_t Transition::MIN_PERIOD = 150; +const size_t Transition::DEFAULT_DURATION = 10000; + Transition::Builder::Builder(size_t id, uint16_t defaultPeriod, const BulbId& bulbId, TransitionFn callback, size_t maxSteps) : id(id) , defaultPeriod(defaultPeriod) diff --git a/lib/Transitions/Transition.h b/lib/Transitions/Transition.h index 72404a78..42992df3 100644 --- a/lib/Transitions/Transition.h +++ b/lib/Transitions/Transition.h @@ -13,8 +13,11 @@ class Transition { using TransitionFn = std::function; // transition commands are in seconds, convert to ms. - static const uint16_t DURATION_UNIT_MULTIPLIER = 1000; + static const uint16_t DURATION_UNIT_MULTIPLIER; + // If period goes lower than this, throttle other parameters up to adjust. + static const size_t MIN_PERIOD; + static const size_t DEFAULT_DURATION; class Builder { public: @@ -68,10 +71,6 @@ class Transition { size_t numSetParams() const; }; - // If period goes lower than this, throttle other parameters up to adjust. - static const size_t MIN_PERIOD = 150; - static const size_t DEFAULT_DURATION = 10000; - const size_t id; const BulbId bulbId; const TransitionFn callback; diff --git a/lib/Types/BulbId.cpp b/lib/Types/BulbId.cpp index 037078da..70d3a6d9 100644 --- a/lib/Types/BulbId.cpp +++ b/lib/Types/BulbId.cpp @@ -57,4 +57,23 @@ void BulbId::serialize(JsonArray json) const { json.add(deviceId); json.add(MiLightRemoteTypeHelpers::remoteTypeToString(deviceType)); json.add(groupId); +} + +// reads a BulbId in the format of "deviceType,deviceId,groupId" +void BulbId::load(Stream &stream) { + deviceType = MiLightRemoteTypeHelpers::remoteTypeFromString(stream.readStringUntil('\0')); + deviceId = stream.parseInt(); + groupId = stream.parseInt(); +} + +// writes a BulbId in the format of "deviceType,deviceId,groupId" +void BulbId::dump(Stream &stream) const { + stream.print(MiLightRemoteTypeHelpers::remoteTypeToString(deviceType).c_str()); + stream.print(static_cast(0)); + + stream.print(deviceId); + stream.print(static_cast(0)); + + stream.print(groupId); + stream.print(static_cast(0)); } \ No newline at end of file diff --git a/lib/Types/BulbId.h b/lib/Types/BulbId.h index c96a579c..dc12b188 100644 --- a/lib/Types/BulbId.h +++ b/lib/Types/BulbId.h @@ -19,4 +19,6 @@ struct BulbId { String getHexDeviceId() const; void serialize(JsonObject json) const; void serialize(JsonArray json) const; + void load(Stream& stream); + void dump(Stream& stream) const; }; \ No newline at end of file diff --git a/lib/Types/GroupAlias.cpp b/lib/Types/GroupAlias.cpp new file mode 100644 index 00000000..e82f562f --- /dev/null +++ b/lib/Types/GroupAlias.cpp @@ -0,0 +1,63 @@ +#include + +// reads a GroupAlias from a stream in the format: +// \0\0\0 +bool GroupAlias::load(Stream &stream) { + // read id + id = stream.parseInt(); + + // expect null terminator + char c = stream.read(); + if (c != 0) { + Serial.printf_P(PSTR("ERROR: alias file invalid. expected null after id but got %c (0x%02x)\n"), c, c); + return false; + } + + // read alias + size_t len = stream.readBytesUntil('\0', alias, MAX_ALIAS_LEN); + alias[len] = 0; + + // load bulbId + bulbId.load(stream); + + return true; +} + +void GroupAlias::dump(Stream &stream) const { + // write id and alias + stream.print(id); + stream.print(static_cast(0)); + stream.print(alias); + stream.print(static_cast(0)); + + // write bulbId + bulbId.dump(stream); +} + +void GroupAlias::loadAliases(Stream &stream, std::map &aliases) { + // Read number of aliases + const uint16_t numAliases = stream.parseInt(); + // expect null terminator + stream.read(); + + Serial.printf_P(PSTR("Reading %d aliases\n"), numAliases); + + while (stream.available() && aliases.size() < numAliases) { + GroupAlias alias; + if (alias.load(stream)) { + aliases[String(alias.alias)] = alias; + } + } +} + +void GroupAlias::saveAliases(Stream &stream, const std::map &aliases) { + // Write number of aliases + stream.print(aliases.size()); + stream.write(0); + + Serial.printf_P(PSTR("Saving %d aliases\n"), aliases.size()); + + for (auto & alias : aliases) { + alias.second.dump(stream); + } +} \ No newline at end of file diff --git a/lib/Types/GroupAlias.h b/lib/Types/GroupAlias.h new file mode 100644 index 00000000..01f61fda --- /dev/null +++ b/lib/Types/GroupAlias.h @@ -0,0 +1,34 @@ +#include +#include + +#include + +#ifndef ESP8266_MILIGHT_HUB_GROUPALIAS_H +#define ESP8266_MILIGHT_HUB_GROUPALIAS_H + +#define MAX_ALIAS_LEN 32 + +struct GroupAlias { + size_t id; + char alias[MAX_ALIAS_LEN + 1]; + BulbId bulbId; + + GroupAlias(size_t id, const char* alias, const BulbId& bulbId) + : id(id) + , bulbId(bulbId) + { + strncpy(this->alias, alias, MAX_ALIAS_LEN); + this->alias[MAX_ALIAS_LEN] = 0; + } + GroupAlias() = default; + + ~GroupAlias() = default; + + bool load(Stream& stream); + void dump(Stream& stream) const; + + static void loadAliases(Stream& stream, std::map& aliases); + static void saveAliases(Stream& stream, const std::map& aliases); +}; + +#endif //ESP8266_MILIGHT_HUB_GROUPALIAS_H \ No newline at end of file diff --git a/lib/Types/GroupStateField.cpp b/lib/Types/GroupStateField.cpp index 33434b69..4575a7d6 100644 --- a/lib/Types/GroupStateField.cpp +++ b/lib/Types/GroupStateField.cpp @@ -20,7 +20,8 @@ static const char* STATE_NAMES[] = { GroupStateFieldNames::GROUP_ID, GroupStateFieldNames::DEVICE_TYPE, GroupStateFieldNames::OH_COLOR, - GroupStateFieldNames::HEX_COLOR + GroupStateFieldNames::HEX_COLOR, + GroupStateFieldNames::COLOR_MODE, }; GroupStateField GroupStateFieldHelpers::getFieldByName(const char* name) { diff --git a/lib/Types/GroupStateField.h b/lib/Types/GroupStateField.h index e58dc2dc..919b7e4f 100644 --- a/lib/Types/GroupStateField.h +++ b/lib/Types/GroupStateField.h @@ -24,6 +24,9 @@ namespace GroupStateFieldNames { static const char HEX_COLOR[] = "hex_color"; static const char COMMAND[] = "command"; static const char COMMANDS[] = "commands"; + + // For use with HomeAssistant + static const char COLOR_MODE[] = "color_mode"; }; enum class GroupStateField { @@ -45,7 +48,8 @@ enum class GroupStateField { GROUP_ID, DEVICE_TYPE, OH_COLOR, - HEX_COLOR + HEX_COLOR, + COLOR_MODE, }; class GroupStateFieldHelpers { diff --git a/lib/Types/MiLightRemoteType.cpp b/lib/Types/MiLightRemoteType.cpp index 2e08f7c2..5d5ae8f4 100644 --- a/lib/Types/MiLightRemoteType.cpp +++ b/lib/Types/MiLightRemoteType.cpp @@ -65,4 +65,28 @@ const String MiLightRemoteTypeHelpers::remoteTypeToString(const MiLightRemoteTyp Serial.println(type); return "unknown"; } +} + +const bool MiLightRemoteTypeHelpers::supportsRgb(const MiLightRemoteType type) { + switch (type) { + case REMOTE_TYPE_FUT089: + case REMOTE_TYPE_RGB: + case REMOTE_TYPE_RGB_CCT: + case REMOTE_TYPE_RGBW: + return true; + default: + return false; + } +} + +const bool MiLightRemoteTypeHelpers::supportsColorTemp(const MiLightRemoteType type) { + switch (type) { + case REMOTE_TYPE_CCT: + case REMOTE_TYPE_FUT089: + case REMOTE_TYPE_FUT091: + case REMOTE_TYPE_RGB_CCT: + return true; + default: + return false; + } } \ No newline at end of file diff --git a/lib/Types/MiLightRemoteType.h b/lib/Types/MiLightRemoteType.h index 1b051dd0..4372e03c 100644 --- a/lib/Types/MiLightRemoteType.h +++ b/lib/Types/MiLightRemoteType.h @@ -17,4 +17,6 @@ class MiLightRemoteTypeHelpers { public: static const MiLightRemoteType remoteTypeFromString(const String& type); static const String remoteTypeToString(const MiLightRemoteType type); + static const bool supportsRgb(const MiLightRemoteType type); + static const bool supportsColorTemp(const MiLightRemoteType type); }; \ No newline at end of file diff --git a/lib/WebServer/MiLightHttpServer.cpp b/lib/WebServer/MiLightHttpServer.cpp index 6c84d328..7131f91e 100644 --- a/lib/WebServer/MiLightHttpServer.cpp +++ b/lib/WebServer/MiLightHttpServer.cpp @@ -7,7 +7,12 @@ #include #include #include +#include +#include +#include + #include +#include using namespace std::placeholders; @@ -28,6 +33,14 @@ void MiLightHttpServer::begin() { std::bind(&MiLightHttpServer::handleUpdateFile, this, SETTINGS_FILE) ); + server + .buildHandler("/backup") + .on(HTTP_GET, std::bind(&MiLightHttpServer::handleCreateBackup, this, _1)) + .on( + HTTP_POST, + std::bind(&MiLightHttpServer::handleRestoreBackup, this, _1), + std::bind(&MiLightHttpServer::handleUpdateFile, this, BACKUP_FILE)); + server .buildHandler("/remote_configs") .on(HTTP_GET, std::bind(&MiLightHttpServer::handleGetRadioConfigs, this, _1)); @@ -75,6 +88,26 @@ void MiLightHttpServer::begin() { .buildHandler("/system") .on(HTTP_POST, std::bind(&MiLightHttpServer::handleSystemPost, this, _1)); + server + .buildHandler("/aliases") + .on(HTTP_GET, std::bind(&MiLightHttpServer::handleListAliases, this, _1)) + .on(HTTP_POST, std::bind(&MiLightHttpServer::handleCreateAlias, this, _1)); + + server + .buildHandler("/aliases.bin") + .on(HTTP_GET, std::bind(&MiLightHttpServer::serveFile, this, ALIASES_FILE, APPLICATION_OCTET_STREAM)) + .on(HTTP_DELETE, std::bind(&MiLightHttpServer::handleDeleteAliases, this, _1)) + .on( + HTTP_POST, + std::bind(&MiLightHttpServer::handleUpdateAliases, this, _1), + std::bind(&MiLightHttpServer::handleUpdateFile, this, ALIASES_FILE) + ); + + server + .buildHandler("/aliases/:id") + .on(HTTP_PUT, std::bind(&MiLightHttpServer::handleUpdateAlias, this, _1)) + .on(HTTP_DELETE, std::bind(&MiLightHttpServer::handleDeleteAlias, this, _1)); + server .buildHandler("/firmware") .handleOTA(); @@ -174,8 +207,8 @@ void MiLightHttpServer::handleGetRadioConfigs(RequestContext& request) { } bool MiLightHttpServer::serveFile(const char* file, const char* contentType) { - if (SPIFFS.exists(file)) { - File f = SPIFFS.open(file, "r"); + if (ProjectFS.exists(file)) { + File f = ProjectFS.open(file, "r"); server.streamFile(f, contentType); f.close(); return true; @@ -188,7 +221,7 @@ void MiLightHttpServer::handleUpdateFile(const char* filename) { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { - updateFile = SPIFFS.open(filename, "w"); + updateFile = ProjectFS.open(filename, "w"); } else if(upload.status == UPLOAD_FILE_WRITE){ if (updateFile.write(upload.buf, upload.currentSize) != upload.currentSize) { Serial.println(F("Error updating web file")); @@ -203,11 +236,7 @@ void MiLightHttpServer::handleUpdateSettings(RequestContext& request) { if (! parsedSettings.isNull()) { settings.patch(parsedSettings); - settings.save(); - - if (this->settingsSavedHandler) { - this->settingsSavedHandler(); - } + saveSettings(); request.response.json["success"] = true; Serial.println(F("Settings successfully updated")); @@ -362,7 +391,7 @@ void MiLightHttpServer::_handleGetGroup(bool allowAsync, BulbId bulbId, RequestC void MiLightHttpServer::handleGetGroupAlias(RequestContext& request) { const String alias = request.pathVariables.get("device_alias"); - std::map::iterator it = settings.groupIdAliases.find(alias); + auto it = settings.groupIdAliases.find(alias); if (it == settings.groupIdAliases.end()) { request.response.setCode(404); @@ -370,7 +399,7 @@ void MiLightHttpServer::handleGetGroupAlias(RequestContext& request) { return; } - _handleGetGroup(true, it->second, request); + _handleGetGroup(true, it->second.bulbId, request); } void MiLightHttpServer::handleGetGroup(RequestContext& request) { @@ -391,7 +420,7 @@ void MiLightHttpServer::handleGetGroup(RequestContext& request) { } void MiLightHttpServer::handleDeleteGroup(RequestContext& request) { - const String _deviceId = request.pathVariables.get(GroupStateFieldNames::DEVICE_ID); + const char* _deviceId = request.pathVariables.get("device_id"); uint8_t _groupId = atoi(request.pathVariables.get(GroupStateFieldNames::GROUP_ID)); const MiLightRemoteConfig* _remoteType = MiLightRemoteConfig::fromType(request.pathVariables.get("type")); @@ -410,7 +439,7 @@ void MiLightHttpServer::handleDeleteGroup(RequestContext& request) { void MiLightHttpServer::handleDeleteGroupAlias(RequestContext& request) { const String alias = request.pathVariables.get("device_alias"); - std::map::iterator it = settings.groupIdAliases.find(alias); + auto it = settings.groupIdAliases.find(alias); if (it == settings.groupIdAliases.end()) { request.response.setCode(404); @@ -418,7 +447,7 @@ void MiLightHttpServer::handleDeleteGroupAlias(RequestContext& request) { return; } - _handleDeleteGroup(it->second, request); + _handleDeleteGroup(it->second.bulbId, request); } void MiLightHttpServer::_handleDeleteGroup(BulbId bulbId, RequestContext& request) { @@ -434,7 +463,7 @@ void MiLightHttpServer::_handleDeleteGroup(BulbId bulbId, RequestContext& reques void MiLightHttpServer::handleUpdateGroupAlias(RequestContext& request) { const String alias = request.pathVariables.get("device_alias"); - std::map::iterator it = settings.groupIdAliases.find(alias); + auto it = settings.groupIdAliases.find(alias); if (it == settings.groupIdAliases.end()) { request.response.setCode(404); @@ -442,7 +471,7 @@ void MiLightHttpServer::handleUpdateGroupAlias(RequestContext& request) { return; } - BulbId& bulbId = it->second; + BulbId& bulbId = it->second.bulbId; const MiLightRemoteConfig* config = MiLightRemoteConfig::fromType(bulbId.deviceType); if (config == NULL) { @@ -675,4 +704,218 @@ void MiLightHttpServer::handleCreateTransition(RequestContext& request) { } else { request.response.setCode(400); } +} + +void MiLightHttpServer::handleListAliases(RequestContext& request) { + uint8_t page = request.server.hasArg("page") ? request.server.arg("page").toInt() : 1; + + // at least 1 per page + uint8_t perPage = request.server.hasArg("page_size") ? request.server.arg("page_size").toInt() : DEFAULT_PAGE_SIZE; + perPage = perPage > 0 ? perPage : 1; + + uint8_t numPages = settings.groupIdAliases.empty() ? 1 : ceil(settings.groupIdAliases.size() / (float) perPage); + + // check bounds + if (page < 1 || page > numPages) { + request.response.setCode(404); + request.response.json[F("error")] = F("Page out of bounds"); + request.response.json[F("page")] = page; + request.response.json[F("num_pages")] = numPages; + return; + } + + JsonArray aliases = request.response.json.to().createNestedArray(F("aliases")); + request.response.json[F("page")] = page; + request.response.json[F("count")] = settings.groupIdAliases.size(); + request.response.json[F("num_pages")] = numPages; + + // Skip iterator to start of page + auto it = settings.groupIdAliases.begin(); + std::advance(it, (page - 1) * perPage); + + for (size_t i = 0; i < perPage && it != settings.groupIdAliases.end(); i++, it++) { + JsonObject alias = aliases.createNestedObject(); + alias[F("alias")] = it->first; + alias[F("id")] = it->second.id; + + const BulbId& bulbId = it->second.bulbId; + alias[F("device_id")] = bulbId.deviceId; + alias[F("group_id")] = bulbId.groupId; + alias[F("device_type")] = MiLightRemoteTypeHelpers::remoteTypeToString(bulbId.deviceType); + + } +} + +void MiLightHttpServer::handleCreateAlias(RequestContext& request) { + JsonObject body = request.getJsonBody().as(); + + if (! body.containsKey(F("alias")) + || ! body.containsKey(GroupStateFieldNames::DEVICE_ID) + || ! body.containsKey(GroupStateFieldNames::GROUP_ID) + || ! body.containsKey(GroupStateFieldNames::DEVICE_TYPE)) { + char buffer[200]; + sprintf_P(buffer, PSTR("Must specify required keys: alias, device_id, group_id, device_type")); + + request.response.setCode(400); + request.response.json[F("error")] = buffer; + return; + } + + const String alias = body[F("alias")]; + const uint16_t deviceId = body[GroupStateFieldNames::DEVICE_ID]; + const uint8_t groupId = body[GroupStateFieldNames::GROUP_ID]; + const MiLightRemoteType deviceType = MiLightRemoteTypeHelpers::remoteTypeFromString(body[GroupStateFieldNames::DEVICE_TYPE].as()); + + if (settings.groupIdAliases.find(alias) != settings.groupIdAliases.end()) { + char buffer[200]; + sprintf_P(buffer, PSTR("Alias already exists: %s"), alias.c_str()); + + request.response.setCode(400); + request.response.json[F("error")] = buffer; + return; + } + + settings.addAlias(alias.c_str(), BulbId(deviceId, groupId, deviceType)); + saveSettings(); + + request.response.json[F("success")] = true; + request.response.json[F("id")] = settings.groupIdAliases[alias].id; +} + +void MiLightHttpServer::handleDeleteAlias(RequestContext& request) { + const size_t id = atoi(request.pathVariables.get("id")); + + if (settings.deleteAlias(id)) { + saveSettings(); + request.response.json[F("success")] = true; + } else { + request.response.setCode(404); + request.response.json[F("error")] = F("Alias not found"); + return; + } +} + +void MiLightHttpServer::handleUpdateAlias(RequestContext& request) { + const size_t id = atoi(request.pathVariables.get("id")); + auto alias = settings.findAliasById(id); + + if (alias == settings.groupIdAliases.end()) { + request.response.setCode(404); + request.response.json[F("error")] = F("Alias not found"); + return; + } else { + JsonObject body = request.getJsonBody().as(); + GroupAlias updatedAlias(alias->second); + + if (body.containsKey(F("alias"))) { + strncpy(updatedAlias.alias, body[F("alias")].as(), MAX_ALIAS_LEN); + } + + if (body.containsKey(GroupStateFieldNames::DEVICE_ID)) { + updatedAlias.bulbId.deviceId = body[GroupStateFieldNames::DEVICE_ID]; + } + + if (body.containsKey(GroupStateFieldNames::GROUP_ID)) { + updatedAlias.bulbId.groupId = body[GroupStateFieldNames::GROUP_ID]; + } + + if (body.containsKey(GroupStateFieldNames::DEVICE_TYPE)) { + updatedAlias.bulbId.deviceType = MiLightRemoteTypeHelpers::remoteTypeFromString(body[GroupStateFieldNames::DEVICE_TYPE].as()); + } + + // If alias was updated, delete the old mapping + if (strcmp(alias->second.alias, updatedAlias.alias) != 0) { + settings.deleteAlias(id); + } + + settings.groupIdAliases[updatedAlias.alias] = updatedAlias; + saveSettings(); + + request.response.json[F("success")] = true; + } +} + +void MiLightHttpServer::handleDeleteAliases(RequestContext &request) { + // buffer current aliases so we can mark them all as deleted + std::vector aliases; + for (auto & alias : settings.groupIdAliases) { + aliases.push_back(alias.second); + } + + ProjectFS.remove(ALIASES_FILE); + Settings::load(settings); + + // mark all aliases as deleted + for (auto & alias : aliases) { + settings.deletedGroupIdAliases[alias.bulbId.getCompactId()] = alias.bulbId; + } + + if (this->settingsSavedHandler) { + this->settingsSavedHandler(); + } + + request.response.json[F("success")] = true; +} + +void MiLightHttpServer::handleUpdateAliases(RequestContext& request) { + // buffer current aliases so we can mark any that were removed as deleted + std::vector aliases; + for (auto & alias : settings.groupIdAliases) { + aliases.push_back(alias.second); + } + + Settings::load(settings); + + // mark any aliases that were removed as deleted + for (auto & alias : aliases) { + if (settings.groupIdAliases.find(alias.alias) == settings.groupIdAliases.end()) { + settings.deletedGroupIdAliases[alias.bulbId.getCompactId()] = alias.bulbId; + } + } + + saveSettings(); + + request.response.json[F("success")] = true; +} + +void MiLightHttpServer::saveSettings() { + settings.save(); + + if (this->settingsSavedHandler) { + this->settingsSavedHandler(); + } +} + +void MiLightHttpServer::handleRestoreBackup(RequestContext &request) { + File backupFile = ProjectFS.open(BACKUP_FILE, "r"); + auto status = BackupManager::restoreBackup(settings, backupFile); + + if (status == BackupManager::RestoreStatus::OK) { + request.response.json[F("success")] = true; + request.response.json[F("message")] = F("Backup restored successfully"); + } else { + request.response.setCode(400); + request.response.json[F("error")] = static_cast(status); + } +} + +void MiLightHttpServer::handleCreateBackup(RequestContext &request) { + File backupFile = ProjectFS.open(BACKUP_FILE, "w"); + + if (!backupFile) { + Serial.println(F("Failed to open backup file")); + request.response.setCode(500); + request.response.json[F("error")] = F("Failed to open backup file"); + } + + WriteBufferingStream bufferedStream(backupFile, 64); + BackupManager::createBackup(settings, bufferedStream); + bufferedStream.flush(); + backupFile.close(); + + backupFile = ProjectFS.open(BACKUP_FILE, "r"); + Serial.printf_P(PSTR("Sending backup file of size %d\n"), backupFile.size()); + server.streamFile(backupFile, APPLICATION_OCTET_STREAM); + + ProjectFS.remove(BACKUP_FILE); } \ No newline at end of file diff --git a/lib/WebServer/MiLightHttpServer.h b/lib/WebServer/MiLightHttpServer.h index ff272aa8..8e8c7685 100644 --- a/lib/WebServer/MiLightHttpServer.h +++ b/lib/WebServer/MiLightHttpServer.h @@ -18,9 +18,12 @@ typedef std::function GroupDeletedHandler; using RichHttpConfig = RichHttp::Generics::Configs::EspressifBuiltin; using RequestContext = RichHttpConfig::RequestContextType; +const char APPLICATION_OCTET_STREAM[] PROGMEM = "application/octet-stream"; const char TEXT_PLAIN[] PROGMEM = "text/plain"; const char APPLICATION_JSON[] = "application/json"; +static const uint8_t DEFAULT_PAGE_SIZE = 10; + class MiLightHttpServer { public: MiLightHttpServer( @@ -87,9 +90,22 @@ class MiLightHttpServer { void handleCreateTransition(RequestContext& request); void handleListTransitions(RequestContext& request); + // CRUD methods for /aliases + void handleListAliases(RequestContext& request); + void handleCreateAlias(RequestContext& request); + void handleDeleteAlias(RequestContext& request); + void handleUpdateAlias(RequestContext& request); + void handleDeleteAliases(RequestContext& request); + void handleUpdateAliases(RequestContext& request); + + void handleCreateBackup(RequestContext& request); + void handleRestoreBackup(RequestContext& request); + void handleRequest(const JsonObject& request); void handleWsEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length); + void saveSettings(); + File updateFile; PassthroughAuthProvider authProvider; diff --git a/platformio.ini b/platformio.ini index c2da520d..4d3c9dc2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,109 +8,89 @@ ; Please visit documentation for the other options and examples ; http://docs.platformio.org/page/projectconf.html -[common] +[platformio] +default_envs = + nodemcuv2 + d1_mini + esp12 + esp07 + huzzah + d1_mini_pro + +[base] framework = arduino -platform = espressif8266@~1.8 -board_f_cpu = 160000000L -lib_deps_builtin = -lib_deps_external = - WiFiManager=https://github.com/sidoh/WiFiManager.git#cmidgley +platform = espressif8266@~4 +board_build.ldscript = eagle.flash.4m1m.ld +lib_deps = + https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2 RF24@~1.3.2 - ArduinoJson@~6.10.1 - PubSubClient@~2.7 + ArduinoJson@~6.21 + PubSubClient@~2.8 https://github.com/ratkins/RGBConverter.git#07010f2 - WebSockets@~2.2.0 - CircularBuffer@~1.2.0 - PathVariableHandlers@~2.0.0 - RichHttpServer@~2.0.2 + WebSockets@~2.4 + CircularBuffer@~1.3 + PathVariableHandlers@~3.0 + RichHttpServer@~3.1 + StreamUtils@~1.7 extra_scripts = pre:.build_web.py test_ignore = remote upload_speed = 460800 +monitor_speed = 9600 build_flags = !python3 .get_version.py - # For compatibility with WebSockets 2.1.4 and v2.4 of the Arduino SDK - -D USING_AXTLS -D MQTT_MAX_PACKET_SIZE=360 -D HTTP_UPLOAD_BUFLEN=128 -D FIRMWARE_NAME=milight-hub -D RICH_HTTP_REQUEST_BUFFER_SIZE=2048 -D RICH_HTTP_RESPONSE_BUFFER_SIZE=2048 - -Idist -Ilib/DataStructures -# -D STATE_DEBUG -# -D DEBUG_PRINTF -# -D MQTT_DEBUG -# -D MILIGHT_UDP_DEBUG -# -D STATE_DEBUG + -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 + -I dist +; -D DEBUG_PRINTF +; -D MQTT_DEBUG +; -D MILIGHT_UDP_DEBUG +; -D STATE_DEBUG [env:nodemcuv2] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = nodemcuv2 -build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=nodemcuv2 -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=nodemcuv2 [env:d1_mini] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = d1_mini -build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=d1_mini -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=d1_mini [env:esp12] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = esp12e -build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=esp12 -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=esp12 [env:esp07] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = esp07 -build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.1m64.ld -D FIRMWARE_VARIANT=esp07 -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=esp07 +board_build.ldscript = eagle.flash.1m64.ld [env:huzzah] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = huzzah -build_flags = ${common.build_flags} -D FIRMWARE_VARIANT=huzzah -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=huzzah [env:d1_mini_pro] -platform = ${common.platform} -framework = ${common.framework} -upload_speed = ${common.upload_speed} +extends = base board = d1_mini_pro -build_flags = ${common.build_flags} -Wl,-Tesp8266.flash.4m1m.ld -D FIRMWARE_VARIANT=d1_mini_PRO -extra_scripts = ${common.extra_scripts} -lib_deps = - ${common.lib_deps_builtin} - ${common.lib_deps_external} -test_ignore = ${common.test_ignore} +build_flags = ${base.build_flags} -D FIRMWARE_VARIANT=d1_mini_PRO + +[env:debug] +extends = env:d1_mini +;these options cause weird memory-related issues (like "stack smashing detected"), hardware watchdog, etc. +;keeping them here for reference +;monitor_filters = esp8266_exception_decoder +;build_type = debug + +[env:ota] +extends = env:debug +upload_port = 10.133.8.221 +upload_protocol = custom +upload_command = curl.exe -F "image=@$SOURCE" http://$UPLOAD_PORT/firmware \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 1c76567d..72562d21 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,25 +1,21 @@ #ifndef UNIT_TEST -#include #include #include -#include +#include #include #include -#include #include #include #include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -27,15 +23,18 @@ #include #include #include +#include #include #include +#include "ProjectFS.h" -WiFiManager wifiManager; +WiFiManager* wifiManager; // because of callbacks, these need to be in the higher scope :( WiFiManagerParameter* wifiStaticIP = NULL; WiFiManagerParameter* wifiStaticIPNetmask = NULL; WiFiManagerParameter* wifiStaticIPGateway = NULL; +WiFiManagerParameter* wifiMode = NULL; static LEDStatus *ledStatus; @@ -55,9 +54,7 @@ GroupStateStore* stateStore = NULL; BulbStateUpdater* bulbStateUpdater = NULL; TransitionController transitions; -int numUdpServers = 0; std::vector> udpServers; -WiFiUDP udpSeder; /** * Set up UDP servers (both v5 and v6). Clean up old ones if necessary. @@ -100,7 +97,7 @@ void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) { // set LED mode for a packet movement ledStatus->oneshot(settings.ledModePacket, settings.ledModePacketCount); - if (&bulbId == &DEFAULT_BULB_ID) { + if (bulbId == DEFAULT_BULB_ID) { Serial.println(F("Skipping packet handler because packet was not decoded")); return; } @@ -304,15 +301,11 @@ bool shouldRestart() { return settings.getAutoRestartPeriod()*60*1000 < millis(); } -// give a bit of time to update the status LED -void handleLED() { - ledStatus->handle(); -} - void wifiExtraSettingsChange() { settings.wifiStaticIP = wifiStaticIP->getValue(); settings.wifiStaticIPNetmask = wifiStaticIPNetmask->getValue(); settings.wifiStaticIPGateway = wifiStaticIPGateway->getValue(); + settings.wifiMode = Settings::wifiModeFromString(wifiMode->getValue()); settings.save(); } @@ -329,15 +322,56 @@ void onGroupDeleted(const BulbId& id) { } } +bool initialized = false; +void postConnectSetup() { + if (initialized) return; + initialized = true; + + delete wifiManager; + wifiManager = NULL; + + MDNS.addService("http", "tcp", 80); + + SSDP.setSchemaURL("description.xml"); + SSDP.setHTTPPort(80); + SSDP.setName("ESP8266 MiLight Gateway"); + SSDP.setSerialNumber(ESP.getChipId()); + SSDP.setURL("/"); + SSDP.setDeviceType("upnp:rootdevice"); + SSDP.begin(); + + httpServer = new MiLightHttpServer(settings, milightClient, stateStore, packetSender, radios, transitions); + httpServer->onSettingsSaved(applySettings); + httpServer->onGroupDeleted(onGroupDeleted); + httpServer->on("/description.xml", HTTP_GET, []() { SSDP.schema(httpServer->client()); }); + httpServer->begin(); + + transitions.addListener( + [](const BulbId& bulbId, GroupStateField field, uint16_t value) { + StaticJsonDocument<100> buffer; + + const char* fieldName = GroupStateFieldHelpers::getFieldName(field); + buffer[fieldName] = value; + + milightClient->prepare(bulbId.deviceType, bulbId.deviceId, bulbId.groupId); + milightClient->update(buffer.as()); + } + ); + + Serial.printf_P(PSTR("Setup complete (version %s)\n"), QUOTE(MILIGHT_HUB_VERSION)); +} + void setup() { Serial.begin(9600); String ssid = "ESP" + String(ESP.getChipId()); // load up our persistent settings from the file system - SPIFFS.begin(); + ProjectFS.begin(); Settings::load(settings); applySettings(); + ESPMH_SETUP_WIFI(settings); + // set up the LED status for wifi configuration ledStatus = new LEDStatus(settings.ledPin); ledStatus->continuous(settings.ledModeWifiConfig); @@ -346,15 +380,13 @@ void setup() { if (! MDNS.begin("milight-hub")) { Serial.println(F("Error setting up MDNS responder")); } - // tell Wifi manager to call us during the setup. Note that this "setSetupLoopCallback" is an addition - // made to Wifi manager in a private fork. As of this writing, WifiManager has a new feature coming that - // allows the "autoConnect" method to be non-blocking which can implement this same functionality. However, - // that change is only on the development branch so we are going to continue to use this fork until - // that is merged and ready. - wifiManager.setSetupLoopCallback(handleLED); // Allows us to have static IP config in the captive portal. Yucky pointers to pointers, just to have the settings carry through - wifiManager.setSaveConfigCallback(wifiExtraSettingsChange); + wifiManager = new WiFiManager(); + wifiManager->setSaveConfigCallback(wifiExtraSettingsChange); + wifiManager->setConfigPortalBlocking(false); + wifiManager->setConnectTimeout(20); + wifiManager->setConnectRetries(5); wifiStaticIP = new WiFiManagerParameter( "staticIP", @@ -362,7 +394,7 @@ void setup() { settings.wifiStaticIP.c_str(), MAX_IP_ADDR_LEN ); - wifiManager.addParameter(wifiStaticIP); + wifiManager->addParameter(wifiStaticIP); wifiStaticIPNetmask = new WiFiManagerParameter( "netmask", @@ -370,7 +402,7 @@ void setup() { settings.wifiStaticIPNetmask.c_str(), MAX_IP_ADDR_LEN ); - wifiManager.addParameter(wifiStaticIPNetmask); + wifiManager->addParameter(wifiStaticIPNetmask); wifiStaticIPGateway = new WiFiManagerParameter( "gateway", @@ -378,7 +410,15 @@ void setup() { settings.wifiStaticIPGateway.c_str(), MAX_IP_ADDR_LEN ); - wifiManager.addParameter(wifiStaticIPGateway); + wifiManager->addParameter(wifiStaticIPGateway); + + wifiMode = new WiFiManagerParameter( + "wifiMode", + "WiFi Mode (b/g/n)", + settings.wifiMode == WifiMode::B ? "b" : settings.wifiMode == WifiMode::G ? "g" : "n", + 1 + ); + wifiManager->addParameter(wifiMode); // We have a saved static IP, let's try and use it. if (settings.wifiStaticIP.length() > 0) { @@ -389,88 +429,68 @@ void setup() { _subnet.fromString(settings.wifiStaticIPNetmask); _gw.fromString(settings.wifiStaticIPGateway); - wifiManager.setSTAStaticIPConfig(_ip,_gw,_subnet); + wifiManager->setSTAStaticIPConfig(_ip,_gw,_subnet); } - wifiManager.setConfigPortalTimeout(180); + wifiManager->setConfigPortalTimeout(180); + wifiManager->setConfigPortalTimeoutCallback([]() { + ledStatus->continuous(settings.ledModeWifiFailed); - if (wifiManager.autoConnect(ssid.c_str(), "milightHub")) { + Serial.println(F("Wifi config portal timed out. Restarting...")); + delay(10000); + ESP.restart(); + }); + + if (wifiManager->autoConnect(ssid.c_str(), "milightHub")) { // set LED mode for successful operation ledStatus->continuous(settings.ledModeOperating); Serial.println(F("Wifi connected succesfully\n")); // if the config portal was started, make sure to turn off the config AP WiFi.mode(WIFI_STA); - } else { - // set LED mode for Wifi failed - ledStatus->continuous(settings.ledModeWifiFailed); - Serial.println(F("Wifi failed. Restarting in 10 seconds.\n")); - delay(10000); - ESP.restart(); + postConnectSetup(); } - - - MDNS.addService("http", "tcp", 80); - - SSDP.setSchemaURL("description.xml"); - SSDP.setHTTPPort(80); - SSDP.setName("ESP8266 MiLight Gateway"); - SSDP.setSerialNumber(ESP.getChipId()); - SSDP.setURL("/"); - SSDP.setDeviceType("upnp:rootdevice"); - SSDP.begin(); - - httpServer = new MiLightHttpServer(settings, milightClient, stateStore, packetSender, radios, transitions); - httpServer->onSettingsSaved(applySettings); - httpServer->onGroupDeleted(onGroupDeleted); - httpServer->on("/description.xml", HTTP_GET, []() { SSDP.schema(httpServer->client()); }); - httpServer->begin(); - - transitions.addListener( - [](const BulbId& bulbId, GroupStateField field, uint16_t value) { - StaticJsonDocument<100> buffer; - - const char* fieldName = GroupStateFieldHelpers::getFieldName(field); - buffer[fieldName] = value; - - milightClient->prepare(bulbId.deviceType, bulbId.deviceId, bulbId.groupId); - milightClient->update(buffer.as()); - } - ); - - Serial.printf_P(PSTR("Setup complete (version %s)\n"), QUOTE(MILIGHT_HUB_VERSION)); } +size_t i = 0; + void loop() { - httpServer->handleClient(); + // update LED with status + ledStatus->handle(); - if (mqttClient) { - mqttClient->handleClient(); - bulbStateUpdater->loop(); + if (shouldRestart()) { + Serial.println(F("Auto-restart triggered. Restarting...")); + ESP.restart(); } - for (size_t i = 0; i < udpServers.size(); i++) { - udpServers[i]->handleClient(); + if (wifiManager) { + wifiManager->process(); } - if (discoveryServer) { - discoveryServer->handleClient(); - } + if (WiFi.getMode() == WIFI_STA && WiFi.isConnected()) { + postConnectSetup(); - handleListen(); + httpServer->handleClient(); + if (mqttClient) { + mqttClient->handleClient(); + bulbStateUpdater->loop(); + } - stateStore->limitedFlush(); - packetSender->loop(); + for (auto & udpServer : udpServers) { + udpServer->handleClient(); + } - // update LED with status - ledStatus->handle(); + if (discoveryServer) { + discoveryServer->handleClient(); + } - transitions.loop(); + handleListen(); - if (shouldRestart()) { - Serial.println(F("Auto-restart triggered. Restarting...")); - ESP.restart(); + stateStore->limitedFlush(); + packetSender->loop(); + + transitions.loop(); } } diff --git a/test/d1_mini/test.cpp b/test/d1_mini/test.cpp index 8fe10413..adaf7c54 100644 --- a/test/d1_mini/test.cpp +++ b/test/d1_mini/test.cpp @@ -1,6 +1,6 @@ // #if defined(ARDUINO) && defined(UNIT_TEST) -#include +#include #include #include @@ -343,7 +343,7 @@ void test_group_0() { // setup connects serial, runs test cases (upcoming) void setup() { delay(2000); - SPIFFS.begin(); + ProjectFS.begin(); Serial.begin(9600); UNITY_BEGIN(); diff --git a/test/remote/Gemfile b/test/remote/Gemfile index 7e79e9e3..66a9c2d8 100644 --- a/test/remote/Gemfile +++ b/test/remote/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem 'rspec' +gem 'rspec-retry' gem 'mqtt', '~> 0.5' gem 'dotenv', '~> 2.6' gem 'multipart-post' diff --git a/test/remote/Gemfile.lock b/test/remote/Gemfile.lock index bee8d387..3cfb8800 100644 --- a/test/remote/Gemfile.lock +++ b/test/remote/Gemfile.lock @@ -20,6 +20,8 @@ GEM rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) + rspec-retry (0.6.2) + rspec-core (> 3.3) rspec-support (3.8.0) PLATFORMS @@ -33,6 +35,7 @@ DEPENDENCIES multipart-post net-ping rspec + rspec-retry BUNDLED WITH 1.17.2 diff --git a/test/remote/helpers/transition_helpers.rb b/test/remote/helpers/transition_helpers.rb index f2a08514..1ab20246 100644 --- a/test/remote/helpers/transition_helpers.rb +++ b/test/remote/helpers/transition_helpers.rb @@ -26,7 +26,7 @@ def color_transitions_are_equal(expected:, seen:) end end - def transitions_are_equal(expected:, seen:, allowed_variation: 0, label: nil) + def transitions_are_equal(expected:, seen:, allowed_variation: 0, label: nil, trim_start: true) generate_msg = ->(a, b, i) do s = "Transition step value" @@ -47,6 +47,25 @@ def transitions_are_equal(expected:, seen:, allowed_variation: 0, label: nil) s << " Seen : #{highlight_value(seen, i)}" end + # If enabled, trim any values at the start of "seen" that don't match the first element + # of "expected," along with any repeats of the first value. + # + # Example: expected = [1, 2, 3, 4, 5] + # seen = [nil, nil, 1, 1, 2, 3, 4, 5] + # trim_start = true + # result = [1, 2, 3, 4, 5] + # + # This sometimes happens when receiving a packet from setting the initial state before + # scheduling the transition. + if trim_start + seen = seen.each_with_index.map.drop_while do |x, i| + # only drop if seen length > expected length + (seen.length - i) > expected.length && ( + x != expected.first || (i < seen.length - 1 && seen[i+1] == x) + ) + end.map { |x| x[0] } + end + expect(expected.length).to eq(seen.length), "Transition was a different length than expected.\n" << " Expected : #{expected}\n" << diff --git a/test/remote/lib/api_client.rb b/test/remote/lib/api_client.rb index 4f3d46c1..155b49e7 100644 --- a/test/remote/lib/api_client.rb +++ b/test/remote/lib/api_client.rb @@ -2,8 +2,11 @@ require 'net/http' require 'net/http/post/multipart' require 'uri' +require 'tempfile' class ApiClient + class LivenessError < StandardError; end + def initialize(host, base_id) @host = host @current_id = Integer(base_id) @@ -16,6 +19,53 @@ def self.from_environment ) end + def reset_settings(settings_file = 'settings.json') + upload_json('/settings', settings_file) + + # clear device aliases + clear_aliases + end + + def clear_aliases + delete('/aliases.bin') + end + + def live?(ping_count = 10, timeout = 1, inverted = false) + print "Waiting for #{@host} to be #{inverted ? 'un' : ''}available..." + + ping_test = Net::Ping::External.new(@host, timeout: timeout) + check = inverted ? -> { not ping_test.ping? } : -> { ping_test.ping? } + result = nil + last_call = Time.now + + ping_count.times do + result = check.call + break if result + + this_call = Time.now + time_since_last_call = this_call - last_call + + if time_since_last_call < timeout then + sleep (timeout - time_since_last_call) + end + + last_call = this_call + + print '.' + end + + puts result ? 'OK' : 'FAIL' + result + end + + def wait_for_liveness(ping_count = 10, timeout = 5) + raise LivenessError unless live?(ping_count, timeout) + end + + def wait_for_unavailable(ping_count = 500, timeout = 0.1) + raise LivenessError unless live?(ping_count, timeout, true) + end + def generate_id id = @current_id @current_id += 1 @@ -34,6 +84,7 @@ def clear_auth! def reboot post('/system', '{"command":"restart"}') + wait_for_unavailable end def request(type, path, req_body = nil) @@ -42,12 +93,22 @@ def request(type, path, req_body = nil) req_type = Net::HTTP.const_get(type) req = req_type.new(uri) + if req_body - req['Content-Type'] = 'application/json' - req_body = req_body.to_json if !req_body.is_a?(String) - req.body = req_body + if req_body.is_a?(File) + req['Content-Length'] = req_body.size.to_s + req.set_form [['file', req_body]], 'multipart/form-data' + else + req_body = req_body.to_json if !req_body.is_a?(String) + + req['Content-Type'] = 'application/json' + req['Content-Length'] = req_body.size.to_s + req.body = req_body + end end + http.read_timeout = 3 + if @username && @password req.basic_auth(@username, @password) end @@ -64,7 +125,12 @@ def request(type, path, req_body = nil) body = res.body if res['content-type'].downcase == 'application/json' - body = JSON.parse(body) + begin + body = JSON.parse(body) + rescue JSON::ParserError => e + puts "JSON Parse Error: #{e}\nBody:\n#{res.body}" + raise e + end end body @@ -72,7 +138,19 @@ def request(type, path, req_body = nil) end def upload_json(path, file) - `curl -s "http://#{@host}#{path}" -X POST -F 'f=@#{file}'` + if file.is_a?(String) + upload_json(path, File.new(file)) + else + request(:Post, path, file) + end + end + + def upload_string_as_file(path, string) + Tempfile.create('tmp-upload-file') do |f| + f.write(string) + f.close + upload_json(path, File.new(f)) + end end def patch_settings(settings) diff --git a/test/remote/lib/mqtt_client.rb b/test/remote/lib/mqtt_client.rb index 134df2f2..a2c0e364 100644 --- a/test/remote/lib/mqtt_client.rb +++ b/test/remote/lib/mqtt_client.rb @@ -4,6 +4,7 @@ class MqttClient BreakListenLoopError = Class.new(StandardError) + DEFAULT_TIMEOUT = 20 def initialize(server, username, password, topic_prefix) @client = MQTT::Client.connect("mqtt://#{username}:#{password}@#{server}") @@ -20,7 +21,7 @@ def reconnect @client.connect end - def wait_for_message(topic, timeout = 10) + def wait_for_message(topic, timeout = DEFAULT_TIMEOUT) on_message(topic, timeout) { |topic, message| } wait_for_listeners end @@ -39,11 +40,11 @@ def id_topic_suffix(params) end end - def on_update(id_params = nil, timeout = 10, &block) + def on_update(id_params = nil, timeout = DEFAULT_TIMEOUT, &block) on_id_message('updates', id_params, timeout, &block) end - def on_state(id_params = nil, timeout = 10, &block) + def on_state(id_params = nil, timeout = DEFAULT_TIMEOUT, &block) on_id_message('state', id_params, timeout, &block) end @@ -70,9 +71,21 @@ def on_id_message(path, id_params, timeout, &block) end end - def on_message(topic, timeout = 10, raise_error = true, &block) + def on_json_message(topic, timeout = DEFAULT_TIMEOUT, raise_error = true, &block) + on_message(topic, timeout, raise_error) do |topic, message| + begin + message = JSON.parse(message) + yield(topic, message) + rescue JSON::ParserError => e + false + end + end + end + + def on_message(topic, timeout = DEFAULT_TIMEOUT, raise_error = true, &block) @listen_threads << Thread.new do begin + start_time = Time.now Timeout.timeout(timeout) do @client.get(topic) do |topic, message| ret_val = yield(topic, message) @@ -80,7 +93,6 @@ def on_message(topic, timeout = 10, raise_error = true, &block) end end rescue Timeout::Error => e - puts "Timed out listening for message on: #{topic}" raise e if raise_error rescue BreakListenLoopError end diff --git a/test/remote/settings.json.example b/test/remote/settings.json.example index 61372292..b6c520ba 100644 --- a/test/remote/settings.json.example +++ b/test/remote/settings.json.example @@ -11,8 +11,8 @@ "auto_restart_period": 0, "discovery_port": 0, "listen_repeats": 3, - "state_flush_interval": 2000, - "mqtt_state_rate_limit": 1000, + "state_flush_interval": 200, + "mqtt_state_rate_limit": 100, "mqtt_debounce_delay": 0, "mqtt_retain": true, "packet_repeat_throttle_sensitivity": 0, diff --git a/test/remote/spec/discovery_spec.rb b/test/remote/spec/discovery_spec.rb index 6d2cd0c1..2d7f2890 100644 --- a/test/remote/spec/discovery_spec.rb +++ b/test/remote/spec/discovery_spec.rb @@ -3,7 +3,7 @@ RSpec.describe 'MQTT Discovery' do before(:all) do @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings @test_id = 1 @topic_prefix = mqtt_topic_prefix() @@ -13,6 +13,8 @@ end after(:all) do + @client.clear_aliases + # Clean up any leftover cruft @mqtt_client.on_message("#{@discovery_prefix}#", 1, false) do |topic, message| if message.length > 0 @@ -26,6 +28,7 @@ before(:each) do mqtt_params = mqtt_parameters() + @client.clear_aliases @client.put( '/settings', mqtt_params @@ -56,7 +59,7 @@ it 'should send discovery messages' do saw_message = false - @mqtt_client.on_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| + @mqtt_client.on_json_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| saw_message = true end @@ -76,8 +79,8 @@ saw_message = false config = nil - @mqtt_client.on_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| - config = JSON.parse(message) + @mqtt_client.on_json_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| + config = message saw_message = true end @@ -94,28 +97,32 @@ expected_keys = %w( schema name - command_topic - state_topic + cmd_t + stat_t brightness rgb color_temp effect - effect_list - device + fx_list + dev + dev_cla + uniq_id + max_mirs + min_mirs ) expect(config.keys).to include(*expected_keys) - expect(config['effect_list']).to include(*%w(white_mode night_mode)) - expect(config['effect_list']).to include(*(0..8).map(&:to_s)) + expect(config['fx_list']).to include(*%w(white_mode night_mode)) + expect(config['fx_list']).to include(*(0..8).map(&:to_s)) end it 'should list identifiers for ESP and bulb' do saw_message = false config = nil - @mqtt_client.on_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| - config = JSON.parse(message) - saw_message = config['device'] && config['device']['identifiers'] && config['device']['identifiers'][1] == @id_params[:id] + @mqtt_client.on_json_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| + config = message + saw_message = config['dev'] && config['dev']['identifiers'] end @client.patch_settings( @@ -127,18 +134,18 @@ @mqtt_client.wait_for_listeners - expect(config.keys).to include('device') + expect(config.keys).to include('dev') - device_data = config['device'] + device_data = config['dev'] - expect(device_data.keys).to include(*%w(manufacturer sw_version identifiers)) - expect(device_data['manufacturer']).to eq('esp8266_milight_hub') + expect(device_data.keys).to include(*%w(mf sw identifiers)) + expect(device_data['mf']).to eq('espressif') + # sw will be of the form "esp8266_milight_hub " + expect(device_data['sw']).to match(/^esp8266_milight_hub v.*$/) + # will be the espressif chip id ids = device_data['identifiers'] - expect(ids.length).to eq(4) - expect(ids[1]).to eq(@id_params[:id]) - expect(ids[2]).to eq(@id_params[:type]) - expect(ids[3]).to eq(@id_params[:group_id]) + expect(ids).to be_a(String) end it 'should remove discoverable devices when alias is removed' do @@ -155,15 +162,52 @@ # This should create the device @client.patch_settings( home_assistant_discovery_prefix: @test_discovery_prefix, - group_id_aliases: { - 'test_group' => [@id_params[:type], @id_params[:id], @id_params[:group_id]] - } ) + created_alias = @client.post('/aliases', { + alias: 'test_group', + device_type: @id_params[:type], + group_id: @id_params[:group_id], + device_id: @id_params[:id] + }) # This should clear it + @client.delete("/aliases/#{created_alias['id']}") + + @mqtt_client.wait_for_listeners + + expect(seen_config).to be(true) + expect(seen_blank_message).to be(true), "should see deletion message" + end + + it 'should remove discoverable devices when backup of aliases is restored' do + seen_config = false + seen_blank_message = false + + @mqtt_client.on_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| + seen_config = seen_config || message.length > 0 + seen_blank_message = seen_blank_message || message.length == 0 + + seen_config && seen_blank_message + end + + # This should create the device @client.patch_settings( - group_id_aliases: { } + home_assistant_discovery_prefix: @test_discovery_prefix, ) + @client.post('/aliases', { + alias: 'test_group', + device_type: @id_params[:type], + group_id: @id_params[:group_id], + device_id: @id_params[:id] + }) + + # Generate a backup without this alias and restore it + backup = [ + [1, "test_1", "rgb_cct", 1, 1], + [2, "test_2", "rgb_cct", 2, 2], + [3, "test_3", "rgb_cct", 3, 3], + ].flatten.join("\0") + @client.upload_string_as_file('/aliases.bin', backup) @mqtt_client.wait_for_listeners @@ -173,14 +217,14 @@ it 'should configure devices with an availability topic if client status is configured' do expected_keys = %w( - availability_topic - payload_available - payload_not_available + avty_t + pl_avail + pl_not_avail ) config = nil - @mqtt_client.on_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| - config = JSON.parse(message) + @mqtt_client.on_json_message("#{@test_discovery_prefix}light/+/#{@discovery_suffix}") do |topic, message| + config = message (expected_keys - config.keys).empty? end diff --git a/test/remote/spec/mqtt_spec.rb b/test/remote/spec/mqtt_spec.rb index d8593ccd..e2029206 100644 --- a/test/remote/spec/mqtt_spec.rb +++ b/test/remote/spec/mqtt_spec.rb @@ -3,7 +3,7 @@ RSpec.describe 'MQTT' do before(:all) do @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings end before(:each) do @@ -45,7 +45,7 @@ context 'retained messages' do it 'should publish retained state messages when enabled' do - @client.put('/settings', mqtt_retain: 'true') + @client.put('/settings', mqtt_retain: true) @client.patch_state({status: 'ON'}, @id_params) # Sleep to make sure we're getting a retained message @@ -56,7 +56,7 @@ end it 'should not publish retained state messages when not enabled' do - @client.put('/settings', mqtt_retain: 'false') + @client.put('/settings', mqtt_retain: false) @client.patch_state({status: 'ON'}, @id_params) # Sleep to make sure we're getting a retained message @@ -194,7 +194,7 @@ update_timestamp_gaps = [] num_updates = 50 - @mqtt_client.on_state(@id_params) do |id, message| + @mqtt_client.on_state(@id_params, 20) do |id, message| next_time = Time.now if last_seen != 0 update_timestamp_gaps << next_time - last_seen @@ -206,7 +206,6 @@ (1..num_updates).each do |i| @mqtt_client.patch_state(@id_params, level: i) - sleep 0.1 end @mqtt_client.wait_for_listeners @@ -247,13 +246,13 @@ ) # Set initial state - @client.patch_state({status: 'ON', level: 0}, @id_params) + @client.patch_state({status: 'ON', level: 0}, { **@id_params, blockOnQueue: true }) num_updates = 10 seen_updates = 0 last_level_value = 0 - @mqtt_client.on_state(@id_params) do |id, message| + @mqtt_client.on_state(@id_params, 20) do |id, message| seen_updates += 1 last_level_value = message['level'] last_level_value == num_updates @@ -261,7 +260,6 @@ (1..num_updates).each do |i| @mqtt_client.patch_state(@id_params, level: i) - sleep 0.5 end @mqtt_client.wait_for_listeners diff --git a/test/remote/spec/rest_spec.rb b/test/remote/spec/rest_spec.rb index 45c7a6dd..b4a70bcb 100644 --- a/test/remote/spec/rest_spec.rb +++ b/test/remote/spec/rest_spec.rb @@ -3,7 +3,7 @@ RSpec.describe 'REST Server' do before(:all) do @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings @username = 'a' @password = 'a' @@ -190,4 +190,221 @@ expect(response['status']).to eq('ON') end end + + context 'alias routes' do + before(:each) do + @client.clear_aliases + @test_alias = { + alias: 'test', + device_type: 'rgb_cct', + device_id: 1, + group_id: 2 + } + end + + it 'GET /aliases should work when there are no aliases' do + result = @client.get('/aliases') + + expect(result).to be_a(Hash) + expect(result['aliases']).to be_a(Array) + expect(result['aliases'].length).to eq(0) + expect(result['page']).to eq(1) + expect(result['count']).to eq(0) + end + + it 'POST /aliases should create an alias' do + result = @client.post('/aliases', @test_alias) + + expect(result['success']).to be_truthy + expect(result['id']).to be_a(Numeric) + + aliases = @client.get('/aliases')['aliases'] + + expect(aliases.length).to eq(1) + expect(aliases[0]['alias']).to eq(@test_alias[:alias]) + expect(aliases[0]['device_type']).to eq(@test_alias[:device_type]) + expect(aliases[0]['device_id']).to eq(@test_alias[:device_id]) + expect(aliases[0]['group_id']).to eq(@test_alias[:group_id]) + end + + it 'DELETE /aliases/:alias should delete an alias' do + create_response = @client.post('/aliases', @test_alias) + delete_response = @client.delete("/aliases/#{create_response['id']}") + + expect(delete_response['success']).to be_truthy + + list_response = @client.get('/aliases') + + expect(list_response['aliases'].length).to eq(0) + end + + it 'DELETE /aliases.bin should clear all aliases' do + @client.post('/aliases', @test_alias) + response = @client.delete('/aliases.bin') + expect(response['success']).to be_truthy + + list_response = @client.get('/aliases') + + expect(list_response['aliases'].length).to eq(0) + end + + it 'GET /aliases.bin should return a backup file of aliases which POST /aliases.bin restores' do + create_response = @client.post('/aliases', @test_alias) + result = @client.get('/aliases.bin') + + @client.clear_aliases + expect(@client.get('/aliases')['aliases'].length).to eq(0) + + @client.upload_string_as_file('/aliases.bin', result) + response = @client.get('/aliases') + + expect(response['aliases'].length).to eq(1) + expect(response['aliases'].first['alias']).to eq(@test_alias[:alias]) + end + + it 'PUT /aliases/:id should update an alias' do + create_response = @client.post('/aliases', @test_alias) + + updated_alias = {**@test_alias, device_id: 3, alias: 'updated_alias'} + update_response = @client.put("/aliases/#{create_response['id']}", updated_alias) + + expect(update_response['success']).to be_truthy + + list_response = @client.get('/aliases') + + expect(list_response['aliases'].length).to eq(1) + expect(list_response['aliases'][0]['alias']).to eq(updated_alias[:alias]) + expect(list_response['aliases'][0]['device_type']).to eq(updated_alias[:device_type]) + expect(list_response['aliases'][0]['device_id']).to eq(updated_alias[:device_id]) + expect(list_response['aliases'][0]['group_id']).to eq(updated_alias[:group_id]) + end + + it 'should support uploading a large list of aliases' do + num_aliases = 20 + csv = (1..num_aliases).map do |i| + [i, "test#{i}", 'rgb_cct', i, 1] + end.flatten.join("\0") + + Tempfile.create('aliases.bin') do |file| + file.write(num_aliases) + file.write("\0") + file.write(csv) + file.close + + @client.upload_json('/aliases.bin', file.path) + end + + result = @client.get('/aliases') + + expect(result['count']).to eq(num_aliases) + end + + it 'should support paging' do + csv = (1..20).map do |i| + [i, "test#{i}", 'rgb_cct', i, 1] + end.flatten.join("\0") + # Write length + csv = [20, csv].join("\0") + + @client.upload_string_as_file('/aliases.bin', csv) + result = @client.get('/aliases?page_size=10') + + expect(result['num_pages']).to eq(2) + expect(result['count']).to eq(20) + expect(result['page']).to eq(1) + expect(result['aliases'].length).to eq(10) + + all_aliases = [] + page = 1 + num_pages = result['num_pages'] + + while true do + result = @client.get("/aliases?page=#{page}&page_size=10") + all_aliases += result['aliases'] + + break if (page += 1) > num_pages + end + + expect(all_aliases.length).to eq(20) + end + end + + context 'backup routes' do + before(:each) do + @client.reset_settings + end + + it 'should preserve settings when creating a backup' do + @client.patch_settings({ + mqtt_server: 'abc', + mqtt_topic_pattern: "123", + }) + + # Create a few aliases + aliases = (1..10).map do |i| + { + alias: "test#{i}", + device_type: 'rgb_cct', + device_id: @client.generate_id, + group_id: 1 + } + end + aliases.each do |a| + @client.post('/aliases', a) + end + + backup = @client.get('/backup') + + # Reset settings + @client.reset_settings + + # Ensure settings were reset + expect(@client.get('/settings')['mqtt_server']).to eq('') + expect(@client.get('/aliases')['aliases'].length).to eq(0) + + # Upload backup + @client.upload_string_as_file('/backup', backup) + + # Check settings + result = @client.get('/settings') + + expect(result['mqtt_server']).to eq('abc') + expect(result['mqtt_topic_pattern']).to eq("123") + + # Check aliases + result = @client.get('/aliases') + + expected_aliases = Set.new(aliases.map { |a| a[:alias] }) + actual_aliases = Set.new(result['aliases'].map { |a| a['alias'] }) + + expect(actual_aliases).to eq(expected_aliases) + end + + it 'should override existing settings when restoring a backup' do + @client.patch_settings({ + mqtt_username: 'abc', + }) + @client.post('/aliases', { + alias: "test_alias", + device_type: 'rgb_cct', + device_id: @client.generate_id, + group_id: 1 + }) + + result = @client.get('/backup') + + @client.patch_settings({ + mqtt_server: 'def', + }) + + @client.upload_string_as_file('/backup', result) + + settings = @client.get('/settings') + expect(settings['mqtt_username']).to eq('abc') + end + + it 'should reject invalid backups' do + expect { @client.upload_string_as_file('/backup', 'invalid') }.to raise_error(Net::HTTPServerException) + end + end end \ No newline at end of file diff --git a/test/remote/spec/settings_spec.rb b/test/remote/spec/settings_spec.rb index 97b494ce..a279c788 100644 --- a/test/remote/spec/settings_spec.rb +++ b/test/remote/spec/settings_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Settings' do before(:all) do @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings @username = 'a' @password = 'a' @@ -23,14 +23,25 @@ @client.clear_auth! end + after(:each) do + @client.reset_settings + end + context 'keys' do + it 'should parse boolean values' do + @client.patch_settings({simple_mqtt_client_status: 'true'}) + expect(@client.get('/settings')['simple_mqtt_client_status']).to eq(true) + + @client.patch_settings({simple_mqtt_client_status: 'false'}) + expect(@client.get('/settings')['simple_mqtt_client_status']).to eq(false) + end + it 'should persist known settings keys' do { 'simple_mqtt_client_status' => [true, false], 'mqtt_retain' => [true, false], 'packet_repeats_per_loop' => [10], 'home_assistant_discovery_prefix' => ['', 'abc', 'a/b/c'], - 'wifi_mode' => %w(b g n), 'default_transition_period' => [200, 500] }.each do |key, values| values.each do |v| @@ -43,30 +54,31 @@ context 'POST settings file' do it 'should clobber patched settings' do - file = Tempfile.new('espmh-settings.json') - file.write({ - mqtt_server: 'test123' - }.to_json) - file.close + Tempfile.new('espmh-settings.json') do |file| + file.write({ + mqtt_server: 'test123' + }.to_json) + file.close - @client.upload_json('/settings', file.path) + @client.upload_json('/settings', file.path) - settings = @client.get('/settings') - expect(settings['mqtt_server']).to eq('test123') + settings = @client.get('/settings') + expect(settings['mqtt_server']).to eq('test123') - @client.put('/settings', {mqtt_server: 'abc123', mqtt_username: 'foo'}) + @client.put('/settings', {mqtt_server: 'abc123', mqtt_username: 'foo'}) - settings = @client.get('/settings') - expect(settings['mqtt_server']).to eq('abc123') - expect(settings['mqtt_username']).to eq('foo') + settings = @client.get('/settings') + expect(settings['mqtt_server']).to eq('abc123') + expect(settings['mqtt_username']).to eq('foo') - @client.upload_json('/settings', file.path) - settings = @client.get('/settings') + @client.upload_json('/settings', file.path) + settings = @client.get('/settings') - expect(settings['mqtt_server']).to eq('test123') - expect(settings['mqtt_username']).to eq('') + expect(settings['mqtt_server']).to eq('test123') + expect(settings['mqtt_username']).to eq('') - File.delete(file.path) + File.delete(file.path) + end end it 'should apply POSTed settings' do @@ -80,6 +92,9 @@ @client.upload_json('/settings', file.path) expect { @client.get('/settings') }.to raise_error(Net::HTTPServerException) + + @client.set_auth!(@username, @password) + @client.reset_settings end end @@ -99,7 +114,7 @@ end_mem = @client.get('/about')['free_heap'] - expect(end_mem).to be_within(250).of(start_mem) + expect(end_mem).to be_within(1024).of(start_mem) end end @@ -136,16 +151,54 @@ it 'should store ID labels' do id = 1 - aliases = Hash[ - StateHelpers::ALL_REMOTE_TYPES.map do |remote_type| - ["test_#{id += 1}", [remote_type, id, 1]] - end - ] + aliases = StateHelpers::ALL_REMOTE_TYPES.each_with_index.map do |remote_type, i| + [i, "test_#{id += 1}", remote_type, id, 1] + end + alias_csv = aliases.flatten.join("\0") - @client.patch_settings(group_id_aliases: aliases) - settings = @client.get('/settings') + Tempfile.create('aliases.bin') do |file| + file.write(aliases.size) + file.write("\0") + + file.write(alias_csv) + file.close + + @client.upload_json('/aliases.bin', file.path) + end - expect(settings['group_id_aliases']).to eq(aliases) + expect @client.get('/aliases.bin') == alias_csv + + response = @client.get('/aliases') + stored_aliases = response['aliases'].map { |x| x['alias'] } + + expect(Set.new(stored_aliases)).to eq(Set.new(aliases.map { |x| x[1] })) + end + + it 'group aliases from deprecated settings key should be ported' do + @client.clear_aliases + + Tempfile.create("updated-settings.json") do |file| + settings = JSON.parse(File.read('settings.json')) + settings.merge!({ + group_id_aliases: { + test1: ['rgb_cct', 1, 1], + test2: ['rgb', 2, 2] + } + }) + + file.write(settings.to_json) + file.close + + @client.upload_json('/settings', file.path) + end + + # Add a new alias + @client.post('/aliases', {alias: 'test3', device_type: 'rgb_cct', group_id: 3, device_id: 3}) + + response = @client.get('/aliases') + stored_aliases = response['aliases'].map { |x| x['alias'] } + + expect(Set.new(stored_aliases)).to eq(Set.new(['test1', 'test2', 'test3'])) end end @@ -164,27 +217,13 @@ @client.reboot # Wait for it to come back up - ping_test = Net::Ping::External.new(static_ip) - - 10.times do - break if ping_test.ping? - sleep 1 - end - - expect(ping_test.ping?).to be(true) - static_client = ApiClient.new(static_ip, ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) + static_client.wait_for_liveness + static_client.put('/settings', wifi_static_ip: '') static_client.reboot - ping_test = Net::Ping::External.new(ENV.fetch('ESPMH_HOSTNAME')) - - 10.times do - break if ping_test.ping? - sleep 1 - end - - expect(ping_test.ping?).to be(true) + @client.wait_for_liveness end end @@ -194,7 +233,11 @@ file = Tempfile.new('espmh-settings.json') file.close - @client.upload_json('/settings', file.path) + @client.reset_settings(file.path) + end + + after(:all) do + @client.reset_settings end it 'should have some group state fields defined' do diff --git a/test/remote/spec/spec_helper.rb b/test/remote/spec/spec_helper.rb index 84f40735..1c8539d2 100644 --- a/test/remote/spec/spec_helper.rb +++ b/test/remote/spec/spec_helper.rb @@ -1,4 +1,6 @@ require 'dotenv' +require 'rspec/retry' + require './helpers/state_helpers' require './helpers/mqtt_helpers' require './helpers/transition_helpers' @@ -25,6 +27,10 @@ config.include MqttHelpers config.include TransitionHelpers + config.verbose_retry = true + config.display_try_failure_messages = true + config.default_retry_count = 3 + # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. diff --git a/test/remote/spec/state_spec.rb b/test/remote/spec/state_spec.rb index 96be8972..60e04ffa 100644 --- a/test/remote/spec/state_spec.rb +++ b/test/remote/spec/state_spec.rb @@ -3,7 +3,7 @@ RSpec.describe 'State' do before(:all) do @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings end before(:each) do @@ -563,4 +563,38 @@ expect(state['status']).to eq('ON') end end + + context 'color_mode field' do + before(:all) do + @client.patch_settings({ + group_state_fields: [ + "status", + "level", + "kelvin", + "hue", + "saturation", + "color_mode", + "hex_color" + ] + }) + end + + context 'for rgb+ww lights' do + it 'when in color mode, field should be "rgb" and color fields should be present' do + state = @client.patch_state({status: 'ON', color: '#ff0000'}, @id_params) + + expect(state['color_mode']).to eq('rgb') + expect(state['hue']).to eq(0) + expect(state['saturation']).to eq(100) + expect(state['color']).to eq('#FF0000') + end + + it 'when in white mode, field should be "color_temp" and color fields should not be present' do + state = @client.patch_state({status: 'ON', kelvin: 100, level: 100}, @id_params) + + expect(state['color_mode']).to eq('color_temp') + expect(state['kelvin']).to eq(100) + end + end + end end \ No newline at end of file diff --git a/test/remote/spec/transition_spec.rb b/test/remote/spec/transition_spec.rb index bfabf17f..1f5c9e9b 100644 --- a/test/remote/spec/transition_spec.rb +++ b/test/remote/spec/transition_spec.rb @@ -3,7 +3,6 @@ RSpec.describe 'Transitions' do before(:all) do @client = ApiClient.from_environment - @client.upload_json('/settings', 'settings.json') @transition_params = { field: 'level', start_value: 0, @@ -12,9 +11,8 @@ period: 400 } @num_transition_updates = (@transition_params[:duration]*1000)/@transition_params[:period] - end - before(:each) do + @client.reset_settings mqtt_params = mqtt_parameters() @updates_topic = mqtt_params[:updates_topic] @topic_prefix = mqtt_topic_prefix() @@ -25,7 +23,9 @@ mqtt_update_topic_pattern: "#{@topic_prefix}updates/:device_id/:device_type/:group_id" ) ) + end + before(:each) do @id_params = { id: @client.generate_id, type: 'rgb_cct', @@ -69,7 +69,7 @@ end it 'should list active transitions' do - @client.schedule_transition(@id_params, @transition_params) + @client.schedule_transition(@id_params, {**@transition_params, duration: 100.0}) response = @client.transitions @@ -77,7 +77,7 @@ end it 'should support getting an active transition with GET /transitions/:id' do - @client.schedule_transition(@id_params, @transition_params) + @client.schedule_transition(@id_params, {**@transition_params, duration: 100.0}) response = @client.transitions detail_response = @client.get("/transitions/#{response.last['id']}") @@ -86,7 +86,7 @@ end it 'should support deleting active transitions with DELETE /transitions/:id' do - @client.schedule_transition(@id_params, @transition_params) + @client.schedule_transition(@id_params, {**@transition_params, duration: 100.0}) response = @client.transitions @@ -117,28 +117,27 @@ end it 'should transition field' do - seen_updates = 0 + seen_updates = [] last_value = nil @client.patch_state({status: 'ON', level: 0}, @id_params) @mqtt_client.on_update(@id_params) do |id, msg| - if msg.include?('brightness') - seen_updates += 1 - last_value = msg['brightness'] - end - - last_value == 255 + seen_updates << msg['brightness'] + seen_updates.last == 255 end @client.patch_state({level: 100, transition: 2.0}, @id_params) - @mqtt_client.wait_for_listeners expected_updates = calculate_transition_steps(start_value: 0, end_value: 255, duration: 2000) - expect(last_value).to eq(255) - expect(seen_updates).to eq(expected_updates.length) + transitions_are_equal( + expected: expected_updates, + seen: seen_updates, + # Allow some variation for the lossy level -> brightness conversion + allowed_variation: 2 + ) end it 'should transition a field downwards' do @@ -148,6 +147,9 @@ @client.patch_state({status: 'ON'}, @id_params) @client.patch_state({level: 100}, @id_params) + # Wait for the initial update to be sent + sleep 2 + @mqtt_client.on_update(@id_params) do |id, msg| if msg.include?('brightness') seen_updates += 1 @@ -172,6 +174,9 @@ @client.patch_state({status: 'ON', hue: 0, level: 100}, @id_params) + # Wait for the initial update to be sent + sleep 2 + @mqtt_client.on_update(@id_params) do |id, msg| msg.each do |k, v| updates[k] ||= [] @@ -322,7 +327,7 @@ @mqtt_client.wait_for_listeners - expect(seen_updates['state']).to eq(['ON']) + expect(seen_updates['state'].last).to eq('ON') transitions_are_equal( expected: calculate_transition_steps(start_value: 0, end_value: 255, duration: 1000), seen: seen_updates['brightness'], @@ -361,6 +366,8 @@ @client.patch_state({status: 'ON', brightness: 99}, @id_params) @client.patch_state({status: 'OFF'}, @id_params) + sleep 2 + @mqtt_client.on_update(@id_params) do |id, message| message.each do |k, v| seen_updates[k] ||= [] @@ -461,6 +468,8 @@ @client.patch_state({status: 'ON', level: 0}, @id_params) @client.patch_state({status: 'OFF'}, @id_params) + sleep 2 + @mqtt_client.on_update(@id_params) do |id, message| message.each do |k, v| seen_updates[k] ||= [] @@ -470,10 +479,9 @@ end @client.patch_state({status: 'ON', brightness: 128, transition: 1.0}, @id_params) - @mqtt_client.wait_for_listeners - expect(seen_updates['state']).to eq(['ON']) + expect(seen_updates['state'].last).to eq('ON') transitions_are_equal( expected: calculate_transition_steps(start_value: 0, end_value: 128, duration: 1000), seen: seen_updates['brightness'], @@ -502,7 +510,11 @@ @client.patch_state({'status' => 'ON', field => min}, @id_params) + # Wait for the initial update to be sent + sleep 2 + @mqtt_client.on_update(@id_params) do |id, message| + puts "didn't include #{update_field}: #{message}" unless message.include?(update_field) seen_updates << message message[update_field] == update_max end @@ -701,6 +713,8 @@ @client.patch_state({'status' => 'ON', field => 0}, @id_params) seen_updates = [] + sleep 2 + @mqtt_client.on_update(@id_params) do |id, message| seen_updates << message[field] if !message[field].nil? seen_updates.last == 255 diff --git a/test/remote/spec/udp_spec.rb b/test/remote/spec/udp_spec.rb index 00087730..b224b4c9 100644 --- a/test/remote/spec/udp_spec.rb +++ b/test/remote/spec/udp_spec.rb @@ -5,7 +5,7 @@ before(:all) do @host = ENV.fetch('ESPMH_HOSTNAME') @client = ApiClient.new(@host, ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE')) - @client.upload_json('/settings', 'settings.json') + @client.reset_settings @client.patch_settings( mqtt_parameters() ) @client.patch_settings( mqtt_update_topic_pattern: '' ) @@ -124,9 +124,6 @@ it 'should respond to v5 discovery' do @discovery_socket.send('Link_Wi-Fi', 0, @discovery_host, @discovery_port) - # wait for response - sleep 1 - response, _ = @discovery_socket.recvfrom_nonblock(1024) response = response.split(',') diff --git a/web/src/index.html b/web/src/index.html index 0a5c2428..5f977032 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -505,17 +505,17 @@