diff --git a/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.cpp b/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.cpp index edec0b9a..498eda8f 100644 --- a/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.cpp +++ b/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,12 @@ void MqttManagerConfig::load() { boost::algorithm::trim(timezone_str); MqttManagerConfig::timezone = timezone_str; SPDLOG_INFO("Read timezone {} from /etc/timezone.", timezone_str); + MqttManagerConfig::timezone_posix_rule = MqttManagerConfig::read_posix_tz_rule(fmt::format("/usr/share/zoneinfo/{}", timezone_str)); + if (MqttManagerConfig::timezone_posix_rule.empty()) { + SPDLOG_WARN("Could not read a POSIX TZ rule for timezone {}. NSPanels will show the time sent by the manager.", timezone_str); + } else { + SPDLOG_INFO("Using POSIX TZ rule {} for timezone {}.", MqttManagerConfig::timezone_posix_rule, timezone_str); + } SPDLOG_DEBUG("Clearing config values cache."); MqttManagerConfig::_settings_values_cache.clear(); @@ -529,6 +536,30 @@ std::expected MqttManagerConfig::get_us_horizontal_mirrored_t return MqttManagerConfig::_md5_checksum_us_horizontal_mirrored_tft4; } +std::string MqttManagerConfig::read_posix_tz_rule(const std::string &tzif_path) { + try { + std::ifstream file(tzif_path, std::ios::in | std::ios::binary); + if (!file) { + return ""; + } + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + // Version 2 and later files end with the rule between two newlines (RFC 8536, section 3.3). + // Version 1 files have a zero version byte and no rule. + if (content.size() < 5 || content.compare(0, 4, "TZif") != 0 || content[4] < '2' || content.back() != '\n') { + return ""; + } + size_t rule_start = content.rfind('\n', content.size() - 2); + if (rule_start == std::string::npos) { + return ""; + } + return content.substr(rule_start + 1, content.size() - rule_start - 2); + } catch (std::exception &ex) { + SPDLOG_WARN("Caught exception while reading POSIX TZ rule from '{}': {}", tzif_path, ex.what()); + return ""; + } +} + // TESTING #if defined(TEST_MODE) && TEST_MODE == 1 #include diff --git a/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.hpp b/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.hpp index 4e7d2be0..d1993aa5 100644 --- a/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.hpp +++ b/docker/MQTTManager/include/mqtt_manager_config/mqtt_manager_config.hpp @@ -88,6 +88,15 @@ class MqttManagerConfig { static void populate_default_and_clean(); static inline std::string timezone; + // POSIX TZ rule for timezone, read from its tzdata file. Empty if it could not be read. + static inline std::string timezone_posix_rule; + + /** + * Read the POSIX TZ rule from the end of a tzdata (TZif) file, ex. "CET-1CEST,M3.5.0,M10.5.0/3". + * @param tzif_path: Path of the file, ex. /usr/share/zoneinfo/Europe/Stockholm. + * @return The rule, or empty if the file is missing, not version 2 or later, or has no rule. + */ + static std::string read_posix_tz_rule(const std::string &tzif_path); template static T get_setting_with_default(MQTT_MANAGER_SETTING key); diff --git a/docker/MQTTManager/include/nspanel/nspanel.cpp b/docker/MQTTManager/include/nspanel/nspanel.cpp index 2017c7b3..5deed68e 100644 --- a/docker/MQTTManager/include/nspanel/nspanel.cpp +++ b/docker/MQTTManager/include/nspanel/nspanel.cpp @@ -299,6 +299,7 @@ void NSPanel::send_config() { config.set_screensaver_dim_level(std::stoi(this->_get_nspanel_setting_with_default("screensaver_dim_level", MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::SCREENSAVER_DIM_LEVEL)))); config.set_screensaver_activation_timeout(std::stoi(this->_get_nspanel_setting_with_default("screensaver_activation_timeout", MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::SCREENSAVER_ACTIVATION_TIMEOUT)))); config.set_clock_us_style(MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::CLOCK_US_STYLE)); + config.set_tz_posix(MqttManagerConfig::timezone_posix_rule); config.set_use_fahrenheit(MqttManagerConfig::get_setting_with_default(MQTT_MANAGER_SETTING::USE_FAHRENHEIT)); config.set_is_us_panel(this->_get_nspanel_setting_with_default("is_us_panel", "False").compare("True") == 0); config.set_reverse_relays(this->_get_nspanel_setting_with_default("reverse_relays", "False").compare("True") == 0); diff --git a/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.cc b/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.cc index 65f882d7..ee19e505 100644 --- a/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.cc +++ b/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.cc @@ -538,6 +538,9 @@ inline constexpr NSPanelConfig::Impl_::Impl_( inside_temperature_sensor_mqtt_topic_( &::google::protobuf::internal::fixed_address_empty_string, ::_pbi::ConstantInitialized()), + tz_posix_( + &::google::protobuf::internal::fixed_address_empty_string, + ::_pbi::ConstantInitialized()), default_room_{0}, default_page_{static_cast< ::NSPanelConfig_NSPanelDefaultPage >(0)}, screensaver_activation_timeout_{0}, @@ -655,6 +658,7 @@ const ::uint32_t PROTOBUF_FIELD_OFFSET(::NSPanelConfig, _impl_.button2_lower_temperature_), PROTOBUF_FIELD_OFFSET(::NSPanelConfig, _impl_.button2_upper_temperature_), PROTOBUF_FIELD_OFFSET(::NSPanelConfig, _impl_.sounds_), + PROTOBUF_FIELD_OFFSET(::NSPanelConfig, _impl_.tz_posix_), ~0u, // no _has_bits_ PROTOBUF_FIELD_OFFSET(::NSPanelWarning, _internal_metadata_), ~0u, // no _extensions_ @@ -901,23 +905,23 @@ static const ::_pbi::MigrationSchema {0, -1, -1, sizeof(::NSPanelConfig_RoomInfo)}, {11, -1, -1, sizeof(::NSPanelConfig_Sound)}, {21, -1, -1, sizeof(::NSPanelConfig)}, - {66, -1, -1, sizeof(::NSPanelWarning)}, - {76, -1, -1, sizeof(::NSPanelStatusReport)}, - {99, -1, -1, sizeof(::NSPanelRoomEntitiesPage_EntitySlot)}, - {116, -1, -1, sizeof(::NSPanelRoomEntitiesPage)}, - {128, -1, -1, sizeof(::NSPanelRoomStatus)}, - {149, -1, -1, sizeof(::NSPanelWeatherUpdate_ForecastItem)}, - {162, -1, -1, sizeof(::NSPanelWeatherUpdate)}, - {178, -1, -1, sizeof(::NSPanelMQTTManagerCommand_FirstPageTurnLightOn)}, - {193, -1, -1, sizeof(::NSPanelMQTTManagerCommand_FirstPageTurnLightOff)}, - {203, -1, -1, sizeof(::NSPanelMQTTManagerCommand_LightCommand)}, - {220, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ToggleEntityFromEntitiesPage)}, - {230, -1, -1, sizeof(::NSPanelMQTTManagerCommand_SaveSceneCommand)}, - {240, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ButtonPressed)}, - {249, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ThermostatTemperatureCommand)}, - {259, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ThermostatCommand)}, - {270, -1, -1, sizeof(::NSPanelMQTTManagerCommand_MediaPlayerCommand)}, - {286, -1, -1, sizeof(::NSPanelMQTTManagerCommand)}, + {67, -1, -1, sizeof(::NSPanelWarning)}, + {77, -1, -1, sizeof(::NSPanelStatusReport)}, + {100, -1, -1, sizeof(::NSPanelRoomEntitiesPage_EntitySlot)}, + {117, -1, -1, sizeof(::NSPanelRoomEntitiesPage)}, + {129, -1, -1, sizeof(::NSPanelRoomStatus)}, + {150, -1, -1, sizeof(::NSPanelWeatherUpdate_ForecastItem)}, + {163, -1, -1, sizeof(::NSPanelWeatherUpdate)}, + {179, -1, -1, sizeof(::NSPanelMQTTManagerCommand_FirstPageTurnLightOn)}, + {194, -1, -1, sizeof(::NSPanelMQTTManagerCommand_FirstPageTurnLightOff)}, + {204, -1, -1, sizeof(::NSPanelMQTTManagerCommand_LightCommand)}, + {221, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ToggleEntityFromEntitiesPage)}, + {231, -1, -1, sizeof(::NSPanelMQTTManagerCommand_SaveSceneCommand)}, + {241, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ButtonPressed)}, + {250, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ThermostatTemperatureCommand)}, + {260, -1, -1, sizeof(::NSPanelMQTTManagerCommand_ThermostatCommand)}, + {271, -1, -1, sizeof(::NSPanelMQTTManagerCommand_MediaPlayerCommand)}, + {287, -1, -1, sizeof(::NSPanelMQTTManagerCommand)}, }; static const ::_pb::Message* const file_default_instances[] = { &::_NSPanelConfig_RoomInfo_default_instance_._instance, @@ -943,7 +947,7 @@ static const ::_pb::Message* const file_default_instances[] = { }; const char descriptor_table_protodef_protobuf_5fnspanel_2eproto[] ABSL_ATTRIBUTE_SECTION_VARIABLE( protodesc_cold) = { - "\n\026protobuf_nspanel.proto\"\303\016\n\rNSPanelConf" + "\n\026protobuf_nspanel.proto\"\325\016\n\rNSPanelConf" "ig\022\014\n\004name\030\001 \001(\t\022\024\n\014default_room\030\002 \001(\005\0227" "\n\014default_page\030\003 \001(\0162!.NSPanelConfig.NSP" "anelDefaultPage\022&\n\036screensaver_activatio" @@ -976,131 +980,132 @@ const char descriptor_table_protodef_protobuf_5fnspanel_2eproto[] ABSL_ATTRIBUTE "per_temperature\030* \001(\005\022!\n\031button2_lower_t" "emperature\030+ \001(\005\022!\n\031button2_upper_temper" "ature\030, \001(\005\022$\n\006sounds\030- \003(\0132\024.NSPanelCon" - "fig.Sound\032L\n\010RoomInfo\022\017\n\007room_id\030\001 \001(\005\022\027" - "\n\017entity_page_ids\030\002 \003(\005\022\026\n\016scene_page_id" - "s\030\003 \003(\005\032G\n\005Sound\022/\n\005event\030\001 \001(\0162 .NSPane" - "lConfig.NSPanelSoundEvent\022\r\n\005rtttl\030\002 \001(\t" - "\"8\n\022NSPanelDefaultPage\022\010\n\004HOME\020\000\022\n\n\006SCEN" - "ES\020\001\022\014\n\010ENTITIES\020\002\"\250\001\n\026NSPanelScreensave" - "rMode\022\033\n\027WEATHER_WITH_BACKGROUND\020\000\022\036\n\032WE" - "ATHER_WITHOUT_BACKGROUND\020\001\022\034\n\030DATETIME_W" - "ITH_BACKGROUND\020\003\022\037\n\033DATETIME_WITHOUT_BAC" - "KGROUND\020\004\022\022\n\016NO_SCREENSAVER\020\005\"i\n\021NSPanel" - "ButtonMode\022\n\n\006DIRECT\020\000\022\n\n\006FOLLOW\020\001\022\022\n\016NO" - "TIFY_MANAGER\020\002\022\023\n\017THERMOSTAT_HEAT\020\003\022\023\n\017T" - "HERMOSTAT_COOL\020\004\"D\n\021NSPanelSoundEvent\022\t\n" - "\005TOUCH\020\000\022\023\n\017SCENE_ACTIVATED\020\001\022\017\n\013SCENE_S" - "AVED\020\002\"C\n\016NSPanelWarning\022#\n\005level\030\001 \001(\0162" - "\024.NSPanelWarningLevel\022\014\n\004text\030\002 \001(\t\"\351\003\n\023" - "NSPanelStatusReport\0221\n\rnspanel_state\030\001 \001" - "(\0162\032.NSPanelStatusReport.state\022\027\n\017update" - "_progress\030\002 \001(\005\022\014\n\004rssi\030\003 \001(\005\022\025\n\rheap_us" - "ed_pct\030\004 \001(\005\022\023\n\013mac_address\030\005 \001(\t\022\023\n\013tem" - "perature\030\006 \001(\002\022\022\n\nip_address\030\007 \001(\t\022!\n\010wa" - "rnings\030\010 \003(\0132\017.NSPanelWarning\022\024\n\014md5_fir" - "mware\030\t \001(\t\022\024\n\014md5_littlefs\030\n \001(\t\022\023\n\013md5" - "_tft_gui\030\013 \001(\t\022\024\n\014has_humidity\030\014 \001(\010\022\020\n\010" - "humidity\030\r \001(\002\022\024\n\014has_pressure\030\016 \001(\010\022\020\n\010" - "pressure\030\017 \001(\002\"o\n\005state\022\n\n\006ONLINE\020\000\022\013\n\007O" - "FFLINE\020\001\022\020\n\014UPDATING_TFT\020\002\022\025\n\021UPDATING_F" - "IRMWARE\020\003\022\025\n\021UPDATING_LITTLEFS\020\004\022\r\n\tREBO" - "OTING\020\005\"\246\004\n\027NSPanelRoomEntitiesPage\022\n\n\002i" - "d\030\001 \001(\005\022\021\n\tpage_type\030\002 \001(\005\022\023\n\013header_tex" - "t\030\003 \001(\t\0225\n\010entities\030\004 \003(\0132#.NSPanelRoomE" - "ntitiesPage.EntitySlot\032\237\003\n\nEntitySlot\022\032\n" - "\022room_view_position\030\001 \001(\005\022\014\n\004name\030\002 \001(\t\022" - "\014\n\004icon\030\003 \001(\t\022\013\n\003pco\030\004 \001(\005\022\014\n\004pco2\030\005 \001(\005" - "\022\026\n\016can_save_scene\030\006 \001(\010\022\030\n\020mqtt_state_t" - "opic\030\007 \001(\t\022<\n\004type\030\010 \001(\0162..NSPanelRoomEn" - "titiesPage.EntitySlot.EntityType\022\n\n\002id\030\t" - " \001(\005\"\301\001\n\nEntityType\022\033\n\027ENTITY_TYPE_UNSPE" - "CIFIED\020\000\022\025\n\021ENTITY_TYPE_LIGHT\020\001\022\026\n\022ENTIT" - "Y_TYPE_SWITCH\020\002\022\026\n\022ENTITY_TYPE_BUTTON\020\003\022" - "\032\n\026ENTITY_TYPE_THERMOSTAT\020\004\022\025\n\021ENTITY_TY" - "PE_SCENE\020\005\022\034\n\030ENTITY_TYPE_MEDIA_PLAYER\020\006" - "\"\226\003\n\021NSPanelRoomStatus\022\n\n\002id\030\001 \001(\005\022\014\n\004na" - "me\030\002 \001(\t\022\031\n\021average_dim_level\030\003 \001(\005\022 \n\030c" - "eiling_lights_dim_level\030\004 \001(\005\022\036\n\026table_l" - "ights_dim_level\030\005 \001(\005\022!\n\031average_color_t" - "emperature\030\006 \001(\005\022.\n&ceiling_lights_color" - "_temperature_value\030\007 \001(\005\022,\n$table_lights" - "_color_temperature_value\030\010 \001(\005\022\032\n\022num_ce" - "iling_lights\030\t \001(\005\022\030\n\020num_table_lights\030\n" - " \001(\005\022\035\n\025num_ceiling_lights_on\030\013 \001(\005\022\033\n\023n" - "um_table_lights_on\030\014 \001(\005\022\027\n\017entity_page_" - "ids\030\r \003(\005\"\277\003\n\024NSPanelWeatherUpdate\022:\n\016fo" - "recast_items\030\001 \003(\0132\".NSPanelWeatherUpdat" - "e.ForecastItem\022\034\n\024current_weather_icon\030\002" - " \001(\t\022\"\n\032current_temperature_string\030\003 \001(\t" - "\022\"\n\032current_maxmin_temperature\030\004 \001(\t\022\033\n\023" - "current_wind_string\030\005 \001(\t\022\026\n\016sunrise_str" - "ing\030\006 \001(\t\022\025\n\rsunset_string\030\007 \001(\t\022$\n\034curr" - "ent_precipitation_string\030\010 \001(\t\032\222\001\n\014Forec" - "astItem\022\024\n\014weather_icon\030\001 \001(\t\022\034\n\024precipi" - "tation_string\030\002 \001(\t\022!\n\031temperature_maxmi" - "n_string\030\003 \001(\t\022\023\n\013wind_string\030\004 \001(\t\022\026\n\016d" - "isplay_string\030\005 \001(\t\"\272\020\n\031NSPanelMQTTManag" - "erCommand\022M\n\022first_page_turn_on\030\001 \001(\0132/." - "NSPanelMQTTManagerCommand.FirstPageTurnL" - "ightOnH\000\022O\n\023first_page_turn_off\030\002 \001(\01320." - "NSPanelMQTTManagerCommand.FirstPageTurnL" - "ightOffH\000\022@\n\rlight_command\030\003 \001(\0132\'.NSPan" - "elMQTTManagerCommand.LightCommandH\000\022c\n t" - "oggle_entity_from_entities_page\030\004 \001(\01327." - "NSPanelMQTTManagerCommand.ToggleEntityFr" - "omEntitiesPageH\000\022I\n\022save_scene_command\030\005" - " \001(\0132+.NSPanelMQTTManagerCommand.SaveSce" - "neCommandH\000\022B\n\016button_pressed\030\006 \001(\0132(.NS" - "PanelMQTTManagerCommand.ButtonPressedH\000\022" - "a\n\036thermostat_temperature_command\030\007 \001(\0132" - "7.NSPanelMQTTManagerCommand.ThermostatTe" - "mperatureCommandH\000\022J\n\022thermostat_command" - "\030\010 \001(\0132,.NSPanelMQTTManagerCommand.Therm" - "ostatCommandH\000\022M\n\024media_player_command\030\t" - " \001(\0132-.NSPanelMQTTManagerCommand.MediaPl" - "ayerCommandH\000\022\022\n\nnspanel_id\030d \001(\005\032\372\001\n\024Fi" - "rstPageTurnLightOn\022E\n\raffect_lights\030\001 \001(" - "\0162..NSPanelMQTTManagerCommand.AffectLigh" - "tsOptions\022\037\n\027brightness_slider_value\030\002 \001" - "(\005\022\033\n\023kelvin_slider_value\030\003 \001(\005\022\025\n\rselec" - "ted_room\030\004 \001(\005\022\016\n\006global\030\005 \001(\010\022\034\n\024has_br" - "ightness_value\030\006 \001(\010\022\030\n\020has_kelvin_value" - "\030\007 \001(\010\032n\n\025FirstPageTurnLightOff\022E\n\raffec" - "t_lights\030\001 \001(\0162..NSPanelMQTTManagerComma" - "nd.AffectLightsOptions\022\016\n\006global\030\002 \001(\010\032\321" - "\001\n\014LightCommand\022\021\n\tlight_ids\030\001 \003(\005\022\026\n\016ha" - "s_brightness\030\002 \001(\010\022\022\n\nbrightness\030\003 \001(\005\022\035" - "\n\025has_color_temperature\030\004 \001(\010\022\031\n\021color_t" - "emperature\030\005 \001(\005\022\017\n\007has_hue\030\006 \001(\010\022\013\n\003hue" - "\030\007 \001(\005\022\026\n\016has_saturation\030\010 \001(\010\022\022\n\nsatura" - "tion\030\t \001(\005\032K\n\034ToggleEntityFromEntitiesPa" - "ge\022\026\n\016entity_page_id\030\001 \001(\005\022\023\n\013entity_slo" - "t\030\002 \001(\005\032\?\n\020SaveSceneCommand\022\026\n\016entity_pa" - "ge_id\030\001 \001(\005\022\023\n\013entity_slot\030\002 \001(\005\032\"\n\rButt" - "onPressed\022\021\n\tbutton_id\030\002 \001(\005\032J\n\034Thermost" - "atTemperatureCommand\022\025\n\rthermostat_id\030\001 " - "\001(\005\022\023\n\013temperature\030\002 \001(\002\032M\n\021ThermostatCo" - "mmand\022\025\n\rthermostat_id\030\001 \001(\005\022\016\n\006option\030\002" - " \001(\t\022\021\n\tnew_value\030\003 \001(\t\032\321\002\n\022MediaPlayerC" - "ommand\022\027\n\017media_player_id\030\001 \001(\005\022U\n\017playb" - "ack_action\030\002 \001(\0162<.NSPanelMQTTManagerCom" - "mand.MediaPlayerCommand.PlaybackAction\022\022" - "\n\nhas_volume\030\003 \001(\010\022\016\n\006volume\030\004 \001(\005\022\031\n\021ha" - "s_source_volume\030\005 \001(\010\022\025\n\rsource_volume\030\006" - " \001(\005\022\021\n\thas_muted\030\007 \001(\010\022\r\n\005muted\030\010 \001(\010\"S" - "\n\016PlaybackAction\022\010\n\004NONE\020\000\022\010\n\004PLAY\020\001\022\t\n\005" - "PAUSE\020\002\022\016\n\nNEXT_TRACK\020\003\022\022\n\016PREVIOUS_TRAC" - "K\020\004\"D\n\023AffectLightsOptions\022\007\n\003ALL\020\000\022\020\n\014T" - "ABLE_LIGHTS\020\001\022\022\n\016CEILING_LIGHTS\020\002B\r\n\013Com" - "mandData*[\n\023NSPanelWarningLevel\022\014\n\010CRITI" - "CAL\020\000\022\t\n\005ERROR\020\001\022\013\n\007WARNING\020\002\022\010\n\004INFO\020\003\022" - "\t\n\005DEBUG\020\004\022\t\n\005TRACE\020\005b\006proto3" + "fig.Sound\022\020\n\010tz_posix\030. \001(\t\032L\n\010RoomInfo\022" + "\017\n\007room_id\030\001 \001(\005\022\027\n\017entity_page_ids\030\002 \003(" + "\005\022\026\n\016scene_page_ids\030\003 \003(\005\032G\n\005Sound\022/\n\005ev" + "ent\030\001 \001(\0162 .NSPanelConfig.NSPanelSoundEv" + "ent\022\r\n\005rtttl\030\002 \001(\t\"8\n\022NSPanelDefaultPage" + "\022\010\n\004HOME\020\000\022\n\n\006SCENES\020\001\022\014\n\010ENTITIES\020\002\"\250\001\n" + "\026NSPanelScreensaverMode\022\033\n\027WEATHER_WITH_" + "BACKGROUND\020\000\022\036\n\032WEATHER_WITHOUT_BACKGROU" + "ND\020\001\022\034\n\030DATETIME_WITH_BACKGROUND\020\003\022\037\n\033DA" + "TETIME_WITHOUT_BACKGROUND\020\004\022\022\n\016NO_SCREEN" + "SAVER\020\005\"i\n\021NSPanelButtonMode\022\n\n\006DIRECT\020\000" + "\022\n\n\006FOLLOW\020\001\022\022\n\016NOTIFY_MANAGER\020\002\022\023\n\017THER" + "MOSTAT_HEAT\020\003\022\023\n\017THERMOSTAT_COOL\020\004\"D\n\021NS" + "PanelSoundEvent\022\t\n\005TOUCH\020\000\022\023\n\017SCENE_ACTI" + "VATED\020\001\022\017\n\013SCENE_SAVED\020\002\"C\n\016NSPanelWarni" + "ng\022#\n\005level\030\001 \001(\0162\024.NSPanelWarningLevel\022" + "\014\n\004text\030\002 \001(\t\"\351\003\n\023NSPanelStatusReport\0221\n" + "\rnspanel_state\030\001 \001(\0162\032.NSPanelStatusRepo" + "rt.state\022\027\n\017update_progress\030\002 \001(\005\022\014\n\004rss" + "i\030\003 \001(\005\022\025\n\rheap_used_pct\030\004 \001(\005\022\023\n\013mac_ad" + "dress\030\005 \001(\t\022\023\n\013temperature\030\006 \001(\002\022\022\n\nip_a" + "ddress\030\007 \001(\t\022!\n\010warnings\030\010 \003(\0132\017.NSPanel" + "Warning\022\024\n\014md5_firmware\030\t \001(\t\022\024\n\014md5_lit" + "tlefs\030\n \001(\t\022\023\n\013md5_tft_gui\030\013 \001(\t\022\024\n\014has_" + "humidity\030\014 \001(\010\022\020\n\010humidity\030\r \001(\002\022\024\n\014has_" + "pressure\030\016 \001(\010\022\020\n\010pressure\030\017 \001(\002\"o\n\005stat" + "e\022\n\n\006ONLINE\020\000\022\013\n\007OFFLINE\020\001\022\020\n\014UPDATING_T" + "FT\020\002\022\025\n\021UPDATING_FIRMWARE\020\003\022\025\n\021UPDATING_" + "LITTLEFS\020\004\022\r\n\tREBOOTING\020\005\"\246\004\n\027NSPanelRoo" + "mEntitiesPage\022\n\n\002id\030\001 \001(\005\022\021\n\tpage_type\030\002" + " \001(\005\022\023\n\013header_text\030\003 \001(\t\0225\n\010entities\030\004 " + "\003(\0132#.NSPanelRoomEntitiesPage.EntitySlot" + "\032\237\003\n\nEntitySlot\022\032\n\022room_view_position\030\001 " + "\001(\005\022\014\n\004name\030\002 \001(\t\022\014\n\004icon\030\003 \001(\t\022\013\n\003pco\030\004" + " \001(\005\022\014\n\004pco2\030\005 \001(\005\022\026\n\016can_save_scene\030\006 \001" + "(\010\022\030\n\020mqtt_state_topic\030\007 \001(\t\022<\n\004type\030\010 \001" + "(\0162..NSPanelRoomEntitiesPage.EntitySlot." + "EntityType\022\n\n\002id\030\t \001(\005\"\301\001\n\nEntityType\022\033\n" + "\027ENTITY_TYPE_UNSPECIFIED\020\000\022\025\n\021ENTITY_TYP" + "E_LIGHT\020\001\022\026\n\022ENTITY_TYPE_SWITCH\020\002\022\026\n\022ENT" + "ITY_TYPE_BUTTON\020\003\022\032\n\026ENTITY_TYPE_THERMOS" + "TAT\020\004\022\025\n\021ENTITY_TYPE_SCENE\020\005\022\034\n\030ENTITY_T" + "YPE_MEDIA_PLAYER\020\006\"\226\003\n\021NSPanelRoomStatus" + "\022\n\n\002id\030\001 \001(\005\022\014\n\004name\030\002 \001(\t\022\031\n\021average_di" + "m_level\030\003 \001(\005\022 \n\030ceiling_lights_dim_leve" + "l\030\004 \001(\005\022\036\n\026table_lights_dim_level\030\005 \001(\005\022" + "!\n\031average_color_temperature\030\006 \001(\005\022.\n&ce" + "iling_lights_color_temperature_value\030\007 \001" + "(\005\022,\n$table_lights_color_temperature_val" + "ue\030\010 \001(\005\022\032\n\022num_ceiling_lights\030\t \001(\005\022\030\n\020" + "num_table_lights\030\n \001(\005\022\035\n\025num_ceiling_li" + "ghts_on\030\013 \001(\005\022\033\n\023num_table_lights_on\030\014 \001" + "(\005\022\027\n\017entity_page_ids\030\r \003(\005\"\277\003\n\024NSPanelW" + "eatherUpdate\022:\n\016forecast_items\030\001 \003(\0132\".N" + "SPanelWeatherUpdate.ForecastItem\022\034\n\024curr" + "ent_weather_icon\030\002 \001(\t\022\"\n\032current_temper" + "ature_string\030\003 \001(\t\022\"\n\032current_maxmin_tem" + "perature\030\004 \001(\t\022\033\n\023current_wind_string\030\005 " + "\001(\t\022\026\n\016sunrise_string\030\006 \001(\t\022\025\n\rsunset_st" + "ring\030\007 \001(\t\022$\n\034current_precipitation_stri" + "ng\030\010 \001(\t\032\222\001\n\014ForecastItem\022\024\n\014weather_ico" + "n\030\001 \001(\t\022\034\n\024precipitation_string\030\002 \001(\t\022!\n" + "\031temperature_maxmin_string\030\003 \001(\t\022\023\n\013wind" + "_string\030\004 \001(\t\022\026\n\016display_string\030\005 \001(\t\"\272\020" + "\n\031NSPanelMQTTManagerCommand\022M\n\022first_pag" + "e_turn_on\030\001 \001(\0132/.NSPanelMQTTManagerComm" + "and.FirstPageTurnLightOnH\000\022O\n\023first_page" + "_turn_off\030\002 \001(\01320.NSPanelMQTTManagerComm" + "and.FirstPageTurnLightOffH\000\022@\n\rlight_com" + "mand\030\003 \001(\0132\'.NSPanelMQTTManagerCommand.L" + "ightCommandH\000\022c\n toggle_entity_from_enti" + "ties_page\030\004 \001(\01327.NSPanelMQTTManagerComm" + "and.ToggleEntityFromEntitiesPageH\000\022I\n\022sa" + "ve_scene_command\030\005 \001(\0132+.NSPanelMQTTMana" + "gerCommand.SaveSceneCommandH\000\022B\n\016button_" + "pressed\030\006 \001(\0132(.NSPanelMQTTManagerComman" + "d.ButtonPressedH\000\022a\n\036thermostat_temperat" + "ure_command\030\007 \001(\01327.NSPanelMQTTManagerCo" + "mmand.ThermostatTemperatureCommandH\000\022J\n\022" + "thermostat_command\030\010 \001(\0132,.NSPanelMQTTMa" + "nagerCommand.ThermostatCommandH\000\022M\n\024medi" + "a_player_command\030\t \001(\0132-.NSPanelMQTTMana" + "gerCommand.MediaPlayerCommandH\000\022\022\n\nnspan" + "el_id\030d \001(\005\032\372\001\n\024FirstPageTurnLightOn\022E\n\r" + "affect_lights\030\001 \001(\0162..NSPanelMQTTManager" + "Command.AffectLightsOptions\022\037\n\027brightnes" + "s_slider_value\030\002 \001(\005\022\033\n\023kelvin_slider_va" + "lue\030\003 \001(\005\022\025\n\rselected_room\030\004 \001(\005\022\016\n\006glob" + "al\030\005 \001(\010\022\034\n\024has_brightness_value\030\006 \001(\010\022\030" + "\n\020has_kelvin_value\030\007 \001(\010\032n\n\025FirstPageTur" + "nLightOff\022E\n\raffect_lights\030\001 \001(\0162..NSPan" + "elMQTTManagerCommand.AffectLightsOptions" + "\022\016\n\006global\030\002 \001(\010\032\321\001\n\014LightCommand\022\021\n\tlig" + "ht_ids\030\001 \003(\005\022\026\n\016has_brightness\030\002 \001(\010\022\022\n\n" + "brightness\030\003 \001(\005\022\035\n\025has_color_temperatur" + "e\030\004 \001(\010\022\031\n\021color_temperature\030\005 \001(\005\022\017\n\007ha" + "s_hue\030\006 \001(\010\022\013\n\003hue\030\007 \001(\005\022\026\n\016has_saturati" + "on\030\010 \001(\010\022\022\n\nsaturation\030\t \001(\005\032K\n\034ToggleEn" + "tityFromEntitiesPage\022\026\n\016entity_page_id\030\001" + " \001(\005\022\023\n\013entity_slot\030\002 \001(\005\032\?\n\020SaveSceneCo" + "mmand\022\026\n\016entity_page_id\030\001 \001(\005\022\023\n\013entity_" + "slot\030\002 \001(\005\032\"\n\rButtonPressed\022\021\n\tbutton_id" + "\030\002 \001(\005\032J\n\034ThermostatTemperatureCommand\022\025" + "\n\rthermostat_id\030\001 \001(\005\022\023\n\013temperature\030\002 \001" + "(\002\032M\n\021ThermostatCommand\022\025\n\rthermostat_id" + "\030\001 \001(\005\022\016\n\006option\030\002 \001(\t\022\021\n\tnew_value\030\003 \001(" + "\t\032\321\002\n\022MediaPlayerCommand\022\027\n\017media_player" + "_id\030\001 \001(\005\022U\n\017playback_action\030\002 \001(\0162<.NSP" + "anelMQTTManagerCommand.MediaPlayerComman" + "d.PlaybackAction\022\022\n\nhas_volume\030\003 \001(\010\022\016\n\006" + "volume\030\004 \001(\005\022\031\n\021has_source_volume\030\005 \001(\010\022" + "\025\n\rsource_volume\030\006 \001(\005\022\021\n\thas_muted\030\007 \001(" + "\010\022\r\n\005muted\030\010 \001(\010\"S\n\016PlaybackAction\022\010\n\004NO" + "NE\020\000\022\010\n\004PLAY\020\001\022\t\n\005PAUSE\020\002\022\016\n\nNEXT_TRACK\020" + "\003\022\022\n\016PREVIOUS_TRACK\020\004\"D\n\023AffectLightsOpt" + "ions\022\007\n\003ALL\020\000\022\020\n\014TABLE_LIGHTS\020\001\022\022\n\016CEILI" + "NG_LIGHTS\020\002B\r\n\013CommandData*[\n\023NSPanelWar" + "ningLevel\022\014\n\010CRITICAL\020\000\022\t\n\005ERROR\020\001\022\013\n\007WA" + "RNING\020\002\022\010\n\004INFO\020\003\022\t\n\005DEBUG\020\004\022\t\n\005TRACE\020\005b" + "\006proto3" }; static ::absl::once_flag descriptor_table_protobuf_5fnspanel_2eproto_once; PROTOBUF_CONSTINIT const ::_pbi::DescriptorTable descriptor_table_protobuf_5fnspanel_2eproto = { false, false, - 6069, + 6087, descriptor_table_protodef_protobuf_5fnspanel_2eproto, "protobuf_nspanel.proto", &descriptor_table_protobuf_5fnspanel_2eproto_once, @@ -1803,6 +1808,7 @@ inline PROTOBUF_NDEBUG_INLINE NSPanelConfig::Impl_::Impl_( sounds_{visibility, arena, from.sounds_}, name_(arena, from.name_), inside_temperature_sensor_mqtt_topic_(arena, from.inside_temperature_sensor_mqtt_topic_), + tz_posix_(arena, from.tz_posix_), _cached_size_{0} {} NSPanelConfig::NSPanelConfig( @@ -1837,6 +1843,7 @@ inline PROTOBUF_NDEBUG_INLINE NSPanelConfig::Impl_::Impl_( sounds_{visibility, arena}, name_(arena), inside_temperature_sensor_mqtt_topic_(arena), + tz_posix_(arena), _cached_size_{0} {} inline void NSPanelConfig::SharedCtor(::_pb::Arena* arena) { @@ -1857,6 +1864,7 @@ inline void NSPanelConfig::SharedDtor() { ABSL_DCHECK(GetArena() == nullptr); _impl_.name_.Destroy(); _impl_.inside_temperature_sensor_mqtt_topic_.Destroy(); + _impl_.tz_posix_.Destroy(); _impl_.~Impl_(); } @@ -1881,15 +1889,15 @@ NSPanelConfig::GetClassData() const { return _data_.base(); } PROTOBUF_CONSTINIT PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 -const ::_pbi::TcParseTable<5, 37, 2, 94, 7> NSPanelConfig::_table_ = { +const ::_pbi::TcParseTable<5, 38, 2, 102, 7> NSPanelConfig::_table_ = { { 0, // no _has_bits_ 0, // no _extensions_ - 45, 248, // max_field_number, fast_idx_mask + 46, 248, // max_field_number, fast_idx_mask offsetof(decltype(_table_), field_lookup_table), 515899392, // skipmap offsetof(decltype(_table_), field_entries), - 37, // num_field_entries + 38, // num_field_entries 2, // num_aux_entries offsetof(decltype(_table_), aux_entries), &_NSPanelConfig_default_instance_._instance, @@ -1995,7 +2003,7 @@ const ::_pbi::TcParseTable<5, 37, 2, 94, 7> NSPanelConfig::_table_ = { {504, 63, 0, PROTOBUF_FIELD_OFFSET(NSPanelConfig, _impl_.optimistic_mode_)}}, }}, {{ 33, 0, 1, - 57354, 26, + 49162, 26, 65535, 65535 }}, {{ // string name = 1; @@ -2109,14 +2117,18 @@ const ::_pbi::TcParseTable<5, 37, 2, 94, 7> NSPanelConfig::_table_ = { // repeated .NSPanelConfig.Sound sounds = 45; {PROTOBUF_FIELD_OFFSET(NSPanelConfig, _impl_.sounds_), 0, 1, (0 | ::_fl::kFcRepeated | ::_fl::kMessage | ::_fl::kTvTable)}, + // string tz_posix = 46; + {PROTOBUF_FIELD_OFFSET(NSPanelConfig, _impl_.tz_posix_), 0, 0, + (0 | ::_fl::kFcSingular | ::_fl::kUtf8String | ::_fl::kRepAString)}, }}, {{ {::_pbi::TcParser::GetTable<::NSPanelConfig_RoomInfo>()}, {::_pbi::TcParser::GetTable<::NSPanelConfig_Sound>()}, }}, {{ - "\15\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\44\0\0\0\0\0\0\0" + "\15\4\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\44\0\0\0\0\0\10\0" "NSPanelConfig" "name" "inside_temperature_sensor_mqtt_topic" + "tz_posix" }}, }; @@ -2134,6 +2146,7 @@ PROTOBUF_NOINLINE void NSPanelConfig::Clear() { _impl_.sounds_.Clear(); _impl_.name_.ClearToEmpty(); _impl_.inside_temperature_sensor_mqtt_topic_.ClearToEmpty(); + _impl_.tz_posix_.ClearToEmpty(); ::memset(&_impl_.default_room_, 0, static_cast<::size_t>( reinterpret_cast(&_impl_.button2_upper_temperature_) - reinterpret_cast(&_impl_.default_room_)) + sizeof(_impl_.button2_upper_temperature_)); @@ -2422,6 +2435,14 @@ ::uint8_t* NSPanelConfig::_InternalSerialize( target, stream); } + // string tz_posix = 46; + if (!this->_internal_tz_posix().empty()) { + const std::string& _s = this->_internal_tz_posix(); + ::google::protobuf::internal::WireFormatLite::VerifyUtf8String( + _s.data(), static_cast(_s.length()), ::google::protobuf::internal::WireFormatLite::SERIALIZE, "NSPanelConfig.tz_posix"); + target = stream->WriteStringMaybeAliased(46, _s, target); + } + if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) { target = ::_pbi::WireFormat::InternalSerializeUnknownFieldsToArray( @@ -2501,6 +2522,12 @@ ::size_t NSPanelConfig::ByteSizeLong() const { this->_internal_inside_temperature_sensor_mqtt_topic()); } + // string tz_posix = 46; + if (!this->_internal_tz_posix().empty()) { + total_size += 2 + ::google::protobuf::internal::WireFormatLite::StringSize( + this->_internal_tz_posix()); + } + // int32 default_room = 2; if (this->_internal_default_room() != 0) { total_size += ::_pbi::WireFormatLite::Int32SizePlusOne( @@ -2696,6 +2723,9 @@ void NSPanelConfig::MergeImpl(::google::protobuf::MessageLite& to_msg, const ::g if (!from._internal_inside_temperature_sensor_mqtt_topic().empty()) { _this->_internal_set_inside_temperature_sensor_mqtt_topic(from._internal_inside_temperature_sensor_mqtt_topic()); } + if (!from._internal_tz_posix().empty()) { + _this->_internal_set_tz_posix(from._internal_tz_posix()); + } if (from._internal_default_room() != 0) { _this->_impl_.default_room_ = from._impl_.default_room_; } @@ -2809,6 +2839,7 @@ void NSPanelConfig::InternalSwap(NSPanelConfig* PROTOBUF_RESTRICT other) { _impl_.sounds_.InternalSwap(&other->_impl_.sounds_); ::_pbi::ArenaStringPtr::InternalSwap(&_impl_.name_, &other->_impl_.name_, arena); ::_pbi::ArenaStringPtr::InternalSwap(&_impl_.inside_temperature_sensor_mqtt_topic_, &other->_impl_.inside_temperature_sensor_mqtt_topic_, arena); + ::_pbi::ArenaStringPtr::InternalSwap(&_impl_.tz_posix_, &other->_impl_.tz_posix_, arena); ::google::protobuf::internal::memswap< PROTOBUF_FIELD_OFFSET(NSPanelConfig, _impl_.button2_upper_temperature_) + sizeof(NSPanelConfig::_impl_.button2_upper_temperature_) diff --git a/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.h b/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.h index 887ad042..bea54965 100644 --- a/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.h +++ b/docker/MQTTManager/include/protobuf/protobuf_nspanel.pb.h @@ -5389,6 +5389,7 @@ class NSPanelConfig final : public ::google::protobuf::Message kSoundsFieldNumber = 45, kNameFieldNumber = 1, kInsideTemperatureSensorMqttTopicFieldNumber = 40, + kTzPosixFieldNumber = 46, kDefaultRoomFieldNumber = 2, kDefaultPageFieldNumber = 3, kScreensaverActivationTimeoutFieldNumber = 4, @@ -5539,6 +5540,22 @@ class NSPanelConfig final : public ::google::protobuf::Message const std::string& value); std::string* _internal_mutable_inside_temperature_sensor_mqtt_topic(); + public: + // string tz_posix = 46; + void clear_tz_posix() ; + const std::string& tz_posix() const; + template + void set_tz_posix(Arg_&& arg, Args_... args); + std::string* mutable_tz_posix(); + PROTOBUF_NODISCARD std::string* release_tz_posix(); + void set_allocated_tz_posix(std::string* value); + + private: + const std::string& _internal_tz_posix() const; + inline PROTOBUF_ALWAYS_INLINE void _internal_set_tz_posix( + const std::string& value); + std::string* _internal_mutable_tz_posix(); + public: // int32 default_room = 2; void clear_default_room() ; @@ -5845,8 +5862,8 @@ class NSPanelConfig final : public ::google::protobuf::Message class _Internal; friend class ::google::protobuf::internal::TcParser; static const ::google::protobuf::internal::TcParseTable< - 5, 37, 2, - 94, 7> + 5, 38, 2, + 102, 7> _table_; static constexpr const void* _raw_default_instance_ = @@ -5876,6 +5893,7 @@ class NSPanelConfig final : public ::google::protobuf::Message ::google::protobuf::RepeatedPtrField< ::NSPanelConfig_Sound > sounds_; ::google::protobuf::internal::ArenaStringPtr name_; ::google::protobuf::internal::ArenaStringPtr inside_temperature_sensor_mqtt_topic_; + ::google::protobuf::internal::ArenaStringPtr tz_posix_; ::int32_t default_room_; int default_page_; ::int32_t screensaver_activation_timeout_; @@ -7114,6 +7132,56 @@ NSPanelConfig::_internal_mutable_sounds() { return &_impl_.sounds_; } +// string tz_posix = 46; +inline void NSPanelConfig::clear_tz_posix() { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.tz_posix_.ClearToEmpty(); +} +inline const std::string& NSPanelConfig::tz_posix() const + ABSL_ATTRIBUTE_LIFETIME_BOUND { + // @@protoc_insertion_point(field_get:NSPanelConfig.tz_posix) + return _internal_tz_posix(); +} +template +inline PROTOBUF_ALWAYS_INLINE void NSPanelConfig::set_tz_posix(Arg_&& arg, + Args_... args) { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.tz_posix_.Set(static_cast(arg), args..., GetArena()); + // @@protoc_insertion_point(field_set:NSPanelConfig.tz_posix) +} +inline std::string* NSPanelConfig::mutable_tz_posix() ABSL_ATTRIBUTE_LIFETIME_BOUND { + std::string* _s = _internal_mutable_tz_posix(); + // @@protoc_insertion_point(field_mutable:NSPanelConfig.tz_posix) + return _s; +} +inline const std::string& NSPanelConfig::_internal_tz_posix() const { + ::google::protobuf::internal::TSanRead(&_impl_); + return _impl_.tz_posix_.Get(); +} +inline void NSPanelConfig::_internal_set_tz_posix(const std::string& value) { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.tz_posix_.Set(value, GetArena()); +} +inline std::string* NSPanelConfig::_internal_mutable_tz_posix() { + ::google::protobuf::internal::TSanWrite(&_impl_); + return _impl_.tz_posix_.Mutable( GetArena()); +} +inline std::string* NSPanelConfig::release_tz_posix() { + ::google::protobuf::internal::TSanWrite(&_impl_); + // @@protoc_insertion_point(field_release:NSPanelConfig.tz_posix) + return _impl_.tz_posix_.Release(); +} +inline void NSPanelConfig::set_allocated_tz_posix(std::string* value) { + ::google::protobuf::internal::TSanWrite(&_impl_); + _impl_.tz_posix_.SetAllocated(value, GetArena()); + #ifdef PROTOBUF_FORCE_COPY_DEFAULT_STRING + if (_impl_.tz_posix_.IsDefault()) { + _impl_.tz_posix_.Set("", GetArena()); + } + #endif // PROTOBUF_FORCE_COPY_DEFAULT_STRING + // @@protoc_insertion_point(field_set_allocated:NSPanelConfig.tz_posix) +} + // ------------------------------------------------------------------- // NSPanelWarning diff --git a/docker/MQTTManager/tests/nspanel_test.cpp b/docker/MQTTManager/tests/nspanel_test.cpp index a501772f..959ce8e7 100644 --- a/docker/MQTTManager/tests/nspanel_test.cpp +++ b/docker/MQTTManager/tests/nspanel_test.cpp @@ -206,6 +206,30 @@ TEST_F(NSPanelTest, panel_settings_are_sent_in_the_config) { EXPECT_FALSE(config->show_screensaver_outside_temperature()); } +TEST_F(NSPanelTest, timezone_rule_is_sent_in_the_config) { + std::string previous_rule = MqttManagerConfig::timezone_posix_rule; + OnExit restore_rule{[&] { MqttManagerConfig::timezone_posix_rule = previous_rule; }}; + MqttManagerConfig::timezone_posix_rule = "CET-1CEST,M3.5.0,M10.5.0/3"; + + load_panel(); + + auto config = last_published_config(); + ASSERT_TRUE(config.has_value()); + EXPECT_EQ(config->tz_posix(), "CET-1CEST,M3.5.0,M10.5.0/3"); +} + +TEST_F(NSPanelTest, unknown_timezone_rule_is_sent_empty) { + std::string previous_rule = MqttManagerConfig::timezone_posix_rule; + OnExit restore_rule{[&] { MqttManagerConfig::timezone_posix_rule = previous_rule; }}; + MqttManagerConfig::timezone_posix_rule = ""; + + load_panel(); + + auto config = last_published_config(); + ASSERT_TRUE(config.has_value()); + EXPECT_EQ(config->tz_posix(), ""); +} + TEST_F(NSPanelTest, temperature_calibration_is_sent_in_tenths_of_a_degree) { set_panel_setting("temperature_calibration", "-1.5"); // Django stores str(float(...)). diff --git a/docker/MQTTManager/tests/posix_tz_rule_test.cpp b/docker/MQTTManager/tests/posix_tz_rule_test.cpp new file mode 100644 index 00000000..3eeee315 --- /dev/null +++ b/docker/MQTTManager/tests/posix_tz_rule_test.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +namespace { + +// A tzdata file of the given version: the header, a stand-in for the binary data (which can hold +// newlines), then, from version 2, the rule between two newlines. +std::string tzif(char version, const std::string &rule) { + std::string content = std::string("TZif") + version + std::string(15, '\0') + "\x01\n\x02\n\x03"; + if (version != '\0') { + content += "\n" + rule + "\n"; + } + return content; +} + +class PosixTzRuleTest : public ::testing::Test { +protected: + void SetUp() override { + path = std::filesystem::temp_directory_path() / "nspm_posix_tz_rule_test"; + } + + void TearDown() override { + std::filesystem::remove(path); + } + + std::string read(const std::string &content) { + std::ofstream(path, std::ios::binary) << content; + return MqttManagerConfig::read_posix_tz_rule(path); + } + + std::filesystem::path path; +}; + +} // namespace + +TEST_F(PosixTzRuleTest, reads_the_rule_from_a_version_2_file) { + EXPECT_EQ(read(tzif('2', "CET-1CEST,M3.5.0,M10.5.0/3")), "CET-1CEST,M3.5.0,M10.5.0/3"); +} + +TEST_F(PosixTzRuleTest, reads_the_rule_from_a_version_3_file) { + EXPECT_EQ(read(tzif('3', "IST-2IDT,M3.4.4/26,M10.5.0")), "IST-2IDT,M3.4.4/26,M10.5.0"); +} + +TEST_F(PosixTzRuleTest, version_1_file_has_no_rule) { + EXPECT_EQ(read(tzif('\0', "")), ""); +} + +TEST_F(PosixTzRuleTest, empty_rule_is_empty) { + EXPECT_EQ(read(tzif('2', "")), ""); +} + +TEST_F(PosixTzRuleTest, file_that_is_not_tzdata_has_no_rule) { + EXPECT_EQ(read("Europe/Stockholm\n"), ""); + EXPECT_EQ(read(""), ""); +} + +TEST_F(PosixTzRuleTest, missing_file_has_no_rule) { + EXPECT_EQ(MqttManagerConfig::read_posix_tz_rule("/nonexistent/zoneinfo/Europe/Stockholm"), ""); +} + +// Against the real tzdata the manager image ships, if this one has it too. +TEST(PosixTzRuleSystemTest, reads_the_rule_from_installed_tzdata) { + const std::string utc = "/usr/share/zoneinfo/UTC"; + if (!std::filesystem::exists(utc)) { + GTEST_SKIP() << "No tzdata installed at " << utc; + } + EXPECT_EQ(MqttManagerConfig::read_posix_tz_rule(utc), "UTC0"); +} diff --git a/docker/protobuf/protobuf_nspanel.proto b/docker/protobuf/protobuf_nspanel.proto index e5ca8579..0e2ce72c 100644 --- a/docker/protobuf/protobuf_nspanel.proto +++ b/docker/protobuf/protobuf_nspanel.proto @@ -89,6 +89,10 @@ message NSPanelConfig { string rtttl = 2; // Melody in RTTTL format, ex. "touch:d=32,o=6,b=190:g6,16p,32c7" } repeated Sound sounds = 45; + + // POSIX TZ rule for the manager's timezone, ex. "CET-1CEST,M3.5.0,M10.5.0/3", for setenv("TZ", ...) and tzset(). + // Empty if the manager could not work it out, in which case show the time from the manager's status/time topic. + string tz_posix = 46; } enum NSPanelWarningLevel {