Plugin Directory

Changeset 3732074


Ignore:
Timestamp:
10/07/2026 08:07:53 AM (2 weeks ago)
Author:
wpveloxa
Message:

Release 1.0.2

Location:
iot-data-widget
Files:
6 added
40 edited
1 copied

Legend:

Unmodified
Added
Removed
  • iot-data-widget/tags/1.0.2/admin/class-diagnostics-page.php

    r3728389 r3732074  
    131131                $cron_jobs         = $this->cron_jobs_info();
    132132                $connections       = $this->connections_info();
    133                 $alert_rules       = $this->alert_manager->get_rules();
     133                // Null = sekcja ukryta: alerty są Pro (klasa fizycznie tylko w paczce Pro).
     134                $alert_rules       = class_exists( 'IOTDW_Alert_Evaluator' ) ? $this->alert_manager->get_rules() : null;
    134135                $connections_by_id = array_column( $this->connector_manager->get_connections(), null, 'id' );
    135136                $recent_errors     = IOTDW_Diagnostics_Logger::get_recent_errors();
    … …  
    152153         * @param array $cron_jobs         Z cron_jobs_info().
    153154         * @param array $connections       Z connections_info().
    154          * @param array $alert_rules       Z IOTDW_Alert_Manager::get_rules().
     155         * @param array|null $alert_rules  Z IOTDW_Alert_Manager::get_rules(); null = sekcja ukryta (wersja darmowa).
    155156         * @param array $connections_by_id Połączenia indeksowane po ID.
    156157         * @param array $recent_errors     Z IOTDW_Diagnostics_Logger::get_recent_errors().
    … …  
    158159         * @return string
    159160         */
    160         private function build_report_text( array $environment, array $cron_jobs, array $connections, array $alert_rules, array $connections_by_id, array $recent_errors, array $data_sizes ): string {
     161        private function build_report_text( array $environment, array $cron_jobs, array $connections, ?array $alert_rules, array $connections_by_id, array $recent_errors, array $data_sizes ): string {
    161162                // Code review v1.8: wcześniej wszystkie stringi w tej metodzie były
    162163                // gołym angielskim tekstem, niezgodnie z CLAUDE.md ("i18n od
    … …  
    264265                $lines[] = '';
    265266
    266                 $lines[] = __( '== Alert rules ==', 'iot-data-widget' );
    267                 if ( empty( $alert_rules ) ) {
    268                         $lines[] = __( 'None configured.', 'iot-data-widget' );
    269                 } else {
     267                if ( null !== $alert_rules ) {
     268                        $lines[] = __( '== Alert rules ==', 'iot-data-widget' );
     269                        if ( empty( $alert_rules ) ) {
     270                                $lines[] = __( 'None configured.', 'iot-data-widget' );
     271                        }
    270272                        foreach ( $alert_rules as $rule ) {
    271273                                $rule_connection = $connections_by_id[ $rule['connection_id'] ] ?? null;
    … …  
    285287                                );
    286288                        }
    287                 }
    288                 $lines[] = '';
     289                        $lines[] = '';
     290                }
    289291
    290292                $lines[] = __( '== Recent errors ==', 'iot-data-widget' );
    … …  
    328330                global $wpdb;
    329331
    330                 $tables = array(
    331                         __( 'Recorded value history', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_History_Recorder::TABLE,
    332                         __( 'Widget usage tracking', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_Widget_Usage_Tracker::TABLE,
    333                         __( 'Alert rules', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_Alert_Manager::TABLE,
    334                         __( 'MQTT cache', 'iot-data-widget' )  => $wpdb->prefix . IOTDW_Cron_Value_Cache::CACHE_TABLE,
    335                         __( 'Error log', 'iot-data-widget' )   => $wpdb->prefix . IOTDW_Diagnostics_Logger::TABLE_ERROR_LOG,
    336                 );
     332                // Tabele funkcji Pro tylko wtedy, gdy klasa Pro istnieje w paczce.
     333                $tables = array();
     334
     335                if ( class_exists( 'IOTDW_Renderer_Chart' ) ) {
     336                        $tables[ __( 'Recorded value history', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_History_Recorder::TABLE;
     337                }
     338
     339                $tables[ __( 'Widget usage tracking', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Widget_Usage_Tracker::TABLE;
     340
     341                if ( class_exists( 'IOTDW_Alert_Evaluator' ) ) {
     342                        $tables[ __( 'Alert rules', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Alert_Manager::TABLE;
     343                }
     344
     345                if ( class_exists( 'IOTDW_MQTT_Poll_Engine' ) ) {
     346                        $tables[ __( 'MQTT cache', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Cron_Value_Cache::CACHE_TABLE;
     347                }
     348
     349                $tables[ __( 'Error log', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Diagnostics_Logger::TABLE_ERROR_LOG;
    337350
    338351                $rows = array();
    … …  
    478491         */
    479492        private function cron_jobs_info(): array {
     493                // 'requires_class' — crony funkcji Pro tylko wtedy, gdy klasa Pro
     494                // fizycznie istnieje w paczce (wersja darmowa ich nie ma, więc wiersz
     495                // "Poll MQTT brokers" itp. był tam mylący).
    480496                $hooks = array(
    481                         IOTDW_History_Recorder::SNAPSHOT_HOOK => __( 'Record value history', 'iot-data-widget' ),
    482                         IOTDW_History_Recorder::CLEANUP_HOOK  => __( 'Clean up old history & error log', 'iot-data-widget' ),
    483                         IOTDW_Cron_Value_Cache::POLL_HOOK     => __( 'Poll MQTT brokers', 'iot-data-widget' ),
    484                         // Dopisane (code review) — brakowało tu odkąd IOTDW_Email_Report_Sender
    485                         // powstał (v1.3 Tydzień 7); ten cron logował się już wtedy do
    486                         // IOTDW_Diagnostics_Logger, po prostu nikt nie dodał go na tej liście.
    487                         IOTDW_Email_Report_Sender::SEND_HOOK  => __( 'Send due email reports', 'iot-data-widget' ),
     497                        IOTDW_History_Recorder::SNAPSHOT_HOOK => array( __( 'Record value history', 'iot-data-widget' ), 'IOTDW_Renderer_Chart' ),
     498                        IOTDW_History_Recorder::CLEANUP_HOOK  => array( __( 'Clean up old history & error log', 'iot-data-widget' ), '' ),
     499                        IOTDW_Cron_Value_Cache::POLL_HOOK     => array( __( 'Poll MQTT brokers', 'iot-data-widget' ), 'IOTDW_MQTT_Poll_Engine' ),
     500                        IOTDW_Email_Report_Sender::SEND_HOOK  => array( __( 'Send due email reports', 'iot-data-widget' ), 'IOTDW_Email_Report_Dispatcher' ),
    488501                );
    489502
    490503                $jobs = array();
    491504
    492                 foreach ( $hooks as $hook => $label ) {
     505                foreach ( $hooks as $hook => list( $label, $requires_class ) ) {
     506                        if ( '' !== $requires_class && ! class_exists( $requires_class ) ) {
     507                                continue;
     508                        }
     509
    493510                        $jobs[] = array(
    494511                                'hook'         => $hook,
  • iot-data-widget/tags/1.0.2/admin/class-settings-page.php

    r3728389 r3732074  
    171171                $remove_data_on_uninstall = (bool) ( $settings['remove_data_on_uninstall'] ?? false );
    172172                $stale_threshold_minutes  = (int) ( $settings[ IOTDW_Renderer::STALE_THRESHOLD_SETTING_KEY ] ?? IOTDW_Renderer::DEFAULT_STALE_MINUTES );
     173                $allow_local_network      = ! empty( $settings[ IOTDW_Http_Client::SETTING_KEY ] );
     174                $can_manage_local_network = IOTDW_Http_Client::can_manage_local_network();
    173175                $import_result            = get_transient( $this->import_result_transient_key() );
    174176
    … …  
    219221                $settings['show_branding']            = ! empty( $_POST['show_branding'] );
    220222                $settings['remove_data_on_uninstall'] = ! empty( $_POST['remove_data_on_uninstall'] );
     223
     224                // Na multisite tylko manage_network_options (patrz IOTDW_Http_Client::can_manage_local_network()).
     225                if ( IOTDW_Http_Client::can_manage_local_network() ) {
     226                        $settings[ IOTDW_Http_Client::SETTING_KEY ] = ! empty( $_POST['allow_local_network'] );
     227                }
    221228
    222229                if ( isset( $_POST['stale_threshold_minutes'] ) ) {
  • iot-data-widget/tags/1.0.2/admin/class-setup-wizard.php

    r3730704 r3732074  
    898898         *
    899899         * @param string               $push_type  Wybrana platforma.
    900          * @param array<string,string> $connection Zapisany kanał (połączenie 'push').
     900         * @param array<string,string> $iotdw_connection Zapisany kanał (połączenie 'push').
    901901         */
    902902        private function render_push_snippet_step( string $push_type, array $iotdw_connection ): void {
    … …  
    975975                        // jednego, rzadkiego przypadku brzegowego.
    976976                        wp_safe_redirect(
    977                                 add_query_arg( 'iotdw_notice', 'connection_limit', admin_url( 'admin.php?page=iot-data-widget' ) )
     977                                add_query_arg( 'iotdw_notice', 'demo_failed', admin_url( 'admin.php?page=iot-data-widget' ) )
    978978                        );
    979979                        exit;
    … …  
    10511051         * przed obiema tymi ścieżkami.
    10521052         *
    1053          * @param string $type_key      Wybrany klucz typu (już zweryfikowany przez find_type() w render_page()).
     1053         * @param string $iotdw_type_key      Wybrany klucz typu (już zweryfikowany przez find_type() w render_page()).
    10541054         * @param string $connection_id ID istniejącego połączenia (powrót z Kroku 3), albo pusty string dla nowego.
    10551055         * @param string $mode          'pull'/'push'/'' — do linku "Back" (zgłoszenie 2026-09-07: bez tego "Back" gubił mode/group i wracał na Krok 0 zamiast na Krok 1).
    … …  
    10981098         * pojedynczym entity=..., bez zapisu, bez zmian w tym przepływie.
    10991099         *
    1100          * @param string                $type_key   Wybrany klucz typu (do linku "Back").
    1101          * @param array<string, string> $connection Zapisane połączenie (Krok 2).
     1100         * @param string                $iotdw_type_key   Wybrany klucz typu (do linku "Back").
     1101         * @param array<string, string> $iotdw_connection Zapisane połączenie (Krok 2).
    11021102         */
    11031103        private function render_step_3( string $iotdw_type_key, array $iotdw_connection ): void {
    … …  
    12351235         * krok 3), nie trzeba tego dublować w kreatorze.
    12361236         *
    1237          * @param string                $type_key    Wybrany klucz typu (do linku "Back").
    1238          * @param array<string, string> $connection  Zapisane połączenie.
     1237         * @param string                $iotdw_type_key    Wybrany klucz typu (do linku "Back").
     1238         * @param array<string, string> $iotdw_connection  Zapisane połączenie.
    12391239         * @param string[]              $entity_ids  Encje zaznaczone w Kroku 3 — jedna dla konektorów nie-Listable
    12401240         *                                            (bez zmian), kilka dla Listable (kreator multi-entity, v1.9 Tydzień 5).
    … …  
    13251325         *
    13261326         * @param string                $type_key   Wybrany klucz typu.
    1327          * @param array<string, string> $connection Zapisane połączenie — użyte przez wymagany niżej widok (step-5-done.php), nie bezpośrednio w tej metodzie.
     1327         * @param array<string, string> $iotdw_connection Zapisane połączenie — użyte przez wymagany niżej widok (step-5-done.php), nie bezpośrednio w tej metodzie.
    13281328         * @param string[]              $entity_ids Encje zaznaczone w Kroku 3 (jedna albo kilka, v1.9) — jak wyżej, użyte przez widok.
    13291329         */
  • iot-data-widget/tags/1.0.2/admin/views/diagnostics.php

    r3728389 r3732074  
    1010 * @var array<int, array{hook: string, label: string, is_scheduled: bool, next_run: int|false, last_run: array{ran_at: string, success: bool, message: string}|null}> $cron_jobs Zaplanowane zadania.
    1111 * @var array<int, array{name: string, connector_label: string, last_checked_at: string|null, last_status: string|null, consecutive_errors: int, last_error_message: string|null, is_push: bool, push_active: bool|null, last_received_at: string|null}> $connections Skonfigurowane połączenia + ich status.
    12  * @var array<int, array<string, mixed>>     $alert_rules       Zapisane reguły alertów (IOTDW_Alert_Manager::get_rules()).
     12 * @var array<int, array<string, mixed>>|null $alert_rules       Zapisane reguły alertów (IOTDW_Alert_Manager::get_rules()); null = wersja darmowa, sekcja ukryta.
    1313 * @var array<string, array<string, string>> $connections_by_id Połączenia indeksowane po ID (do nazw w tabeli reguł alertów).
    1414 * @var array<int, array{id: int, occurred_at: string, source: string, message: string}> $recent_errors Najnowsze wpisy logu błędów.
    … …  
    233233        ?>
    234234
     235        <?php if ( null !== $alert_rules ) : // Alerty są Pro — na wersji darmowej sekcji nie ma. ?>
    235236        <h2><?php esc_html_e( 'Alert rules', 'iot-data-widget' ); ?></h2>
    236237        <?php
    … …  
    273274        require __DIR__ . '/partials/data-table.php';
    274275        ?>
     276        <?php endif; ?>
    275277
    276278        <h2><?php esc_html_e( 'Recent errors', 'iot-data-widget' ); ?></h2>
  • iot-data-widget/tags/1.0.2/admin/views/general-settings-page.php

    r3728389 r3732074  
    1616 * @var string                             $export_default_to     Domyślna data końcowa formularza eksportu CSV (v1.3 Tydzień 6).
    1717 * @var int                                $stale_threshold_minutes Próg "stare dane" w minutach (v2.0, docs/mvp-v2-0-push-mode-plan.md §5).
     18 * @var bool                               $allow_local_network   "Allow local network addresses" (1.0.2, IOTDW_Http_Client).
     19 * @var bool                               $can_manage_local_network Czy bieżący użytkownik może to zmienić (multisite: manage_network_options).
    1820 */
    1921
    … …  
    119121                        </tr>
    120122                        <tr>
     123                                <th><?php esc_html_e( 'Local network', 'iot-data-widget' ); ?></th>
     124                                <td>
     125                                        <label>
     126                                                <input type="checkbox" name="allow_local_network" value="1" <?php checked( $allow_local_network ); ?> <?php disabled( ! $can_manage_local_network ); ?> />
     127                                                <?php esc_html_e( 'Allow local network addresses', 'iot-data-widget' ); ?>
     128                                        </label>
     129                                        <p class="description">
     130                                                <?php esc_html_e( 'Turn this on only if WordPress runs on the same network as your devices (e.g. a home server) and connects to addresses like 192.168.x.x or homeassistant.local. Off by default: it stops a misconfigured or malicious URL from reaching other services on your server\'s network. Cloud metadata and link-local addresses are always blocked.', 'iot-data-widget' ); ?>
     131                                        </p>
     132                                        <?php if ( ! $can_manage_local_network ) : ?>
     133                                                <p class="description"><?php esc_html_e( 'On a multisite network, only a network administrator can change this.', 'iot-data-widget' ); ?></p>
     134                                        <?php endif; ?>
     135                                </td>
     136                        </tr>
     137                        <tr>
    121138                                <th><?php esc_html_e( 'Uninstall', 'iot-data-widget' ); ?></th>
    122139                                <td>
  • iot-data-widget/tags/1.0.2/admin/views/settings-page.php

    r3728389 r3732074  
    3232                $iotdw_current_notice = sanitize_key( wp_unslash( $_GET['iotdw_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    3333
    34                 if ( 'connection_limit' === $iotdw_current_notice ) {
     34                if ( 'demo_failed' === $iotdw_current_notice ) {
     35                        // Dawniej "The free version supports one connection" — pozostałość
     36                        // po zniesionym limicie; zapis połączenia demo po prostu się nie udał.
    3537                        $iotdw_notice_type    = 'error';
    36                         $iotdw_notice_message = __( 'The free version supports one connection. Upgrade to Pro for unlimited connections.', 'iot-data-widget' );
     38                        $iotdw_notice_message = __( 'The demo connection could not be saved. Please try again, or add a connection manually below.', 'iot-data-widget' );
    3739                } else {
    3840                        $iotdw_notice_type    = 'success';
  • iot-data-widget/tags/1.0.2/admin/views/wizard/step-0-location.php

    r3728389 r3732074  
    2828                        <?php esc_html_e( 'On external/shared hosting (typical) — my device sends data to my site', 'iot-data-widget' ); ?>
    2929                </a>
    30                 <a class="button button-hero" href="<?php echo esc_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ); ?>">
     30                <?php // 1.0.2: ta opcja włącza też "Allow local network addresses" (IOTDW_Local_Network::handle_enable()). ?>
     31                <a class="button button-hero" href="<?php echo esc_url( IOTDW_Local_Network::enable_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ) ); ?>">
    3132                        <?php esc_html_e( 'Same network as my devices (Raspberry Pi, NAS, home server) — my site fetches data from the device', 'iot-data-widget' ); ?>
    3233                </a>
    … …  
    4546                ?>
    4647                <a class="button button-hero" href="<?php echo esc_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ); ?>">
    47                         <?php esc_html_e( 'On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there', 'iot-data-widget' ); ?>
     48                        <?php
     49                        // Shelly Cloud i preset Supla są Pro — na wersji darmowej oznaczone.
     50                        if ( class_exists( 'IOTDW_Connector_Presets' ) ) {
     51                                esc_html_e( 'On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there', 'iot-data-widget' );
     52                        } else {
     53                                esc_html_e( 'On external/shared hosting, but my device has its own cloud service (a publicly-reachable Home Assistant; Shelly Cloud and Supla in Pro) — my site fetches data from there', 'iot-data-widget' );
     54                        }
     55                        ?>
    4856                </a>
    4957        </p>
  • iot-data-widget/tags/1.0.2/includes/class-history-recorder.php

    r3728389 r3732074  
    219219                // phpcs:enable
    220220
     221                $keys = array();
     222
    221223                foreach ( $pairs as $pair ) {
    222                         $this->record_snapshot( $pair->connection_id, $pair->entity_id );
     224                        $keys[ $pair->connection_id . '|' . $pair->entity_id ] = array( $pair->connection_id, $pair->entity_id );
     225                }
     226
     227                // Wykresy w obszarach widgetów motywu (1.0.2) — nie ma ich w tabeli.
     228                foreach ( ( new IOTDW_Widget_Usage_Tracker( $this->connector_manager ) )->widget_area_usage() as $pair ) {
     229                        if ( in_array( $pair['style'], array( 'chart', 'sparkline' ), true ) ) {
     230                                $keys[ $pair['connection_id'] . '|' . $pair['entity_id'] ] = array( $pair['connection_id'], $pair['entity_id'] );
     231                        }
     232                }
     233
     234                foreach ( $keys as list( $connection_id, $entity_id ) ) {
     235                        $this->record_snapshot( $connection_id, $entity_id );
    223236                }
    224237        }
  • iot-data-widget/tags/1.0.2/includes/class-plugin.php

    r3728389 r3732074  
    266266                $this->init_modules();
    267267                $this->maybe_migrate_credentials();
     268                // Osobno od maybe_upgrade_db() (ta nie czyta połączeń) — jednorazowo,
     269                // dopóki ustawienie "Allow local network addresses" nie ma wartości.
     270                IOTDW_Local_Network::maybe_migrate( $this->connector_manager );
    268271
    269272                add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_admin_toolkit_css' ) );
    … …  
    327330                IOTDW_Push_Channel_Manager::create_table();
    328331
     332                // 1.0.2: aktualizacja z wersji darmowej <= 1.0.1 zdejmuje z
     333                // harmonogramu crony funkcji Pro (patrz sync_cron_events()).
     334                self::sync_cron_events();
     335
    329336                update_option( self::DB_VERSION_OPTION, IOTDW_VERSION );
     337        }
     338
     339        /**
     340         * Planuje crony wtyczki — te funkcji Pro tylko wtedy, gdy plik Pro
     341         * fizycznie istnieje w paczce (ten sam test file_exists() co
     342         * init_modules()), w przeciwnym razie zdejmuje je z harmonogramu.
     343         * Wersja darmowa <= 1.0.1 planowała wszystkie cztery (zgłoszenie
     344         * 2026-10-06): MQTT/raporty nic nie robiły, a snapshot historii odpytywał
     345         * urządzenia dla wykresów, których wersja darmowa nie wyświetla.
     346         * Wołane przy aktywacji i przy każdej zmianie wersji.
     347         */
     348        public static function sync_cron_events(): void {
     349                $premium = IOTDW_PLUGIN_DIR . 'includes/premium/';
     350
     351                // Niestandardowe harmonogramy muszą być znane, zanim wp_schedule_event() ich użyje.
     352                add_filter( 'cron_schedules', array( 'IOTDW_History_Recorder', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
     353                add_filter( 'cron_schedules', array( 'IOTDW_Cron_Value_Cache', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
     354
     355                $events = array(
     356                        // Historia zasila wykresy/sparkline i alerty — obie funkcje Pro.
     357                        IOTDW_History_Recorder::SNAPSHOT_HOOK => array( IOTDW_History_Recorder::CRON_SCHEDULE, file_exists( $premium . 'class-renderer-chart.php' ) || file_exists( $premium . 'class-alert-evaluator.php' ) ),
     358                        // Czyści też dziennik błędów — potrzebny na obu wersjach.
     359                        IOTDW_History_Recorder::CLEANUP_HOOK  => array( 'daily', true ),
     360                        IOTDW_Cron_Value_Cache::POLL_HOOK     => array( IOTDW_Cron_Value_Cache::CRON_SCHEDULE, file_exists( $premium . 'class-mqtt-poll-engine.php' ) ),
     361                        IOTDW_Email_Report_Sender::SEND_HOOK  => array( 'hourly', file_exists( $premium . 'class-email-report-dispatcher.php' ) ),
     362                );
     363
     364                foreach ( $events as $hook => list( $recurrence, $wanted ) ) {
     365                        if ( ! $wanted ) {
     366                                wp_clear_scheduled_hook( $hook );
     367                        } elseif ( ! wp_next_scheduled( $hook ) ) {
     368                                wp_schedule_event( time(), $recurrence, $hook );
     369                        }
     370                }
    330371        }
    331372
    … …  
    373414                }
    374415
     416                require_once IOTDW_PLUGIN_DIR . 'includes/class-http-client.php';
     417                require_once IOTDW_PLUGIN_DIR . 'includes/class-local-network.php';
    375418                require_once IOTDW_PLUGIN_DIR . 'includes/class-value-normalizer.php';
    376419                require_once IOTDW_PLUGIN_DIR . 'includes/class-json-path.php';
    … …  
    557600                $this->mqtt_poller   = new IOTDW_Cron_Value_Cache( $this->connector_manager );
    558601                $this->settings_page = new IOTDW_Settings_Page( $this->connector_manager, $this->widget_usage_tracker );
     602                // 1.0.2 — "Allow local network addresses": powiadomienie + kreator (same hooki).
     603                new IOTDW_Local_Network();
    559604                // alert_manager (skeleton: CRUD + schemat) zostaje zawsze —
    560605                // IOTDW_Diagnostics_Page je wstrzykuje (raport "Alert rules" działa
    … …  
    623668                update_option( self::DB_VERSION_OPTION, IOTDW_VERSION );
    624669
    625                 // Rejestrujemy niestandardowe harmonogramy od razu — inaczej
    626                 // wp_schedule_event() poniżej nie znalazłby ich w wp_get_schedules().
    627                 add_filter( 'cron_schedules', array( 'IOTDW_History_Recorder', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
    628                 add_filter( 'cron_schedules', array( 'IOTDW_Cron_Value_Cache', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
    629 
    630                 if ( ! wp_next_scheduled( IOTDW_History_Recorder::SNAPSHOT_HOOK ) ) {
    631                         wp_schedule_event( time(), IOTDW_History_Recorder::CRON_SCHEDULE, IOTDW_History_Recorder::SNAPSHOT_HOOK );
    632                 }
    633 
    634                 if ( ! wp_next_scheduled( IOTDW_History_Recorder::CLEANUP_HOOK ) ) {
    635                         wp_schedule_event( time(), 'daily', IOTDW_History_Recorder::CLEANUP_HOOK );
    636                 }
    637 
    638                 if ( ! wp_next_scheduled( IOTDW_Cron_Value_Cache::POLL_HOOK ) ) {
    639                         wp_schedule_event( time(), IOTDW_Cron_Value_Cache::CRON_SCHEDULE, IOTDW_Cron_Value_Cache::POLL_HOOK );
    640                 }
    641 
    642                 // Co godzinę — częstotliwość dzienna/tygodniowa nie wymaga precyzji
    643                 // większej niż to (patrz IOTDW_Email_Report_Sender::is_due()), a
    644                 // wbudowany harmonogram 'hourly' istnieje zawsze, bez rejestrowania
    645                 // własnego (w przeciwieństwie do History Recordera/MQTT Pollera).
    646                 if ( ! wp_next_scheduled( IOTDW_Email_Report_Sender::SEND_HOOK ) ) {
    647                         wp_schedule_event( time(), 'hourly', IOTDW_Email_Report_Sender::SEND_HOOK );
    648                 }
     670                self::sync_cron_events();
    649671
    650672                // Sygnał "pokaż kreator" dla IOTDW_Setup_Wizard::maybe_redirect_after_activation()
  • iot-data-widget/tags/1.0.2/includes/class-rest-api.php

    r3728389 r3732074  
    618618                }
    619619
     620                // Wynik pokazywany wprost adminowi ("✓ …") — "false" z surowego bool
     621                // JSON jako "Off", jak na karcie/liście.
     622                if ( ! empty( $result['display_value'] ) ) {
     623                        $result['display_value'] = IOTDW_Value_Normalizer::human_display( (string) $result['display_value'] );
     624                }
     625
    620626                return rest_ensure_response(
    621627                        array(
  • iot-data-widget/tags/1.0.2/includes/class-shortcode.php

    r3730704 r3732074  
    9191         */
    9292        public function render( $atts ): string {
     93                $html = $this->render_widget( $atts );
     94
     95                // Blokowy wrapper (1.0.2): motywy wyrównują swoje bloki do kolumny
     96                // treści przez margin: auto, co nie działa na elementach inline-block
     97                // (card/gauge stały przy krawędzi strony, np. w Twenty Twenty-One).
     98                // Wrapper dostaje szerokość kolumny, widget stoi w nim do lewej.
     99                // Pusty wynik (błąd ukryty przed gościem) — bez wrappera, żeby nie
     100                // zostawiał pustego odstępu.
     101                return '' === trim( $html ) ? $html : '<div class="iot-dw-widget-wrap">' . $html . '</div>';
     102        }
     103
     104        /**
     105         * Właściwe renderowanie widgetu (bez wrappera, patrz render()).
     106         *
     107         * @param array|string $atts Atrybuty shortcode.
     108         * @return string
     109         */
     110        private function render_widget( $atts ): string {
    93111                $this->rendered_widget = true;
    94112
    … …  
    119137                );
    120138
     139                $this->enqueue_late( (string) $atts['style'] );
     140
    121141                if ( 'list' === $atts['style'] ) {
    122142                        return $this->render_list( $atts );
    … …  
    256276         *
    257277         * @param IOTDW_Abstract_Connector $connector     Instancja konektora dla tego połączenia.
    258          * @param array<string, mixed>      $connection_row Zapisane połączenie (musi zawierać 'id'/'connector', opcjonalnie 'preset') — do wyliczenia $min_ttl, patrz enforce_min_ttl().
    259          * @param string                    $entity_id     ID/ścieżka encji.
    260          * @param int                       $ttl           TTL cache dla udanej odpowiedzi (sekundy), zażądany przez shortcode/blok.
     278         * @param array<string, mixed>     $connection_row Zapisane połączenie (musi zawierać 'id'/'connector', opcjonalnie 'preset') — do wyliczenia $min_ttl, patrz enforce_min_ttl().
     279         * @param string                   $entity_id     ID/ścieżka encji.
     280         * @param int                      $ttl           TTL cache dla udanej odpowiedzi (sekundy), zażądany przez shortcode/blok.
    261281         * @return array Znormalizowana struktura danych konektora, ze stemplowanymi connection_id/entity_id.
    262282         */
    … …  
    578598                }
    579599
    580                 // v1.8 Tydzień 6 — tokeny (typografia/odstępy/cienie z Tygodnia 5)
    581                 // muszą się załadować przed widget-styles.css, które teraz z nich
    582                 // korzysta (var(--iot-dw-text-xl) itd.) — stąd jako zależność, nie
    583                 // tylko osobny enqueue obok.
     600                $this->enqueue_styles();
     601
     602                // class_exists() (Guideline 5, 2026-09-24) — chart-widget.js/css są
     603                // plikami premium-only, w paczce darmowej fizycznie nie istnieją.
     604                if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'chart' ) ) ) {
     605                        $this->enqueue_chart_assets();
     606                }
     607
     608                // v1.8 Tydzień 3 — hybrydowe doładowanie. "chart" celowo poza listą
     609                // stylów (patrz komentarz nagłówkowy public/js/widget-refresh.js).
     610                if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'card', 'gauge', 'sparkline', 'list', 'grid' ) ) ) {
     611                        $this->enqueue_refresh_script();
     612                }
     613        }
     614
     615        /**
     616         * Późne ładowanie zasobów dla widgetu wyrenderowanego POZA treścią
     617         * pojedynczego posta (1.0.2: obszar widgetów motywu klasycznego, lista
     618         * wpisów, szablon) — maybe_enqueue_assets() widzi tylko post_content
     619         * strony pojedynczej. WordPress drukuje style/skrypty dodane po wp_head
     620         * w stopce. Tylko front-end — podglądy w adminie/REST mają własne.
     621         *
     622         * @param string $style Styl widgetu.
     623         */
     624        private function enqueue_late( string $style ): void {
     625                if ( is_admin() || ! did_action( 'wp_enqueue_scripts' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
     626                        return;
     627                }
     628
     629                $this->enqueue_styles();
     630
     631                if ( 'chart' === $style ) {
     632                        $this->enqueue_chart_assets();
     633                } else {
     634                        $this->enqueue_refresh_script();
     635                }
     636        }
     637
     638        /**
     639         * CSS widgetu + zmienne wyglądu (raz na żądanie).
     640         */
     641        private function enqueue_styles(): void {
     642                if ( wp_style_is( 'iot-data-widget-theme', 'enqueued' ) ) {
     643                        return;
     644                }
     645
     646                // v1.8 Tydzień 6 — tokeny muszą się załadować przed widget-styles.css.
    584647                wp_enqueue_style(
    585648                        'iot-data-widget-tokens',
    … …  
    607670                        IOTDW_Style_Settings::build_css_rule( IOTDW_Style_Settings::css_variables( IOTDW_Style_Settings::get_settings() ) )
    608671                );
    609 
    610                 // class_exists() (Guideline 5, 2026-09-24; dawniej dodatkowo
    611                 // iotdw_is_premium(), usunięte jako runtime license-check) — bez
    612                 // tego warunku strona z style="chart" na wersji darmowej ładowała
    613                 // Chart.js + chart-widget.js/css na próżno (renderer i tak pokazuje
    614                 // komunikat "wymaga Pro", nigdy nie odwołuje się do tych plików) —
    615                 // zbędny ciężar, a chart-widget.js/css są plikami premium-only
    616                 // (patrz @fs_premium_only w iot-data-widget.php), więc w paczce
    617                 // darmowej fizycznie nie istnieją.
    618                 if ( class_exists( 'IOTDW_Renderer_Chart' ) && IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'chart' ) ) ) {
    619                         wp_enqueue_script(
    620                                 'chart-js',
    621                                 IOTDW_PLUGIN_URL . 'public/js/vendor/chart.umd.min.js',
    622                                 array(),
    623                                 '4.5.1',
    624                                 true
    625                         );
    626 
    627                         wp_enqueue_script(
    628                                 'iot-data-widget-chart',
    629                                 IOTDW_PLUGIN_URL . 'public/js/chart-widget.js',
    630                                 array( 'chart-js' ),
    631                                 IOTDW_VERSION,
    632                                 true
    633                         );
    634 
    635                         wp_enqueue_style(
    636                                 'iot-data-widget-chart',
    637                                 IOTDW_PLUGIN_URL . 'public/css/chart-widget.css',
    638                                 array( 'iot-data-widget' ),
    639                                 IOTDW_VERSION
    640                         );
    641                 }
    642 
    643                 // v1.8 Tydzień 3 — hybrydowe doładowanie. "chart" celowo poza listą
    644                 // stylów (patrz komentarz nagłówkowy public/js/widget-refresh.js) —
    645                 // strona z samym wykresem nie ładuje tego skryptu w ogóle.
    646                 if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'card', 'gauge', 'sparkline', 'list', 'grid' ) ) ) {
    647                         wp_enqueue_script(
    648                                 'iot-data-widget-widget-refresh',
    649                                 IOTDW_PLUGIN_URL . 'public/js/widget-refresh.js',
    650                                 array(),
    651                                 IOTDW_VERSION,
    652                                 true
    653                         );
    654 
    655                         wp_localize_script(
    656                                 'iot-data-widget-widget-refresh',
    657                                 'iotdwWidgetRefresh',
    658                                 array( 'restUrl' => esc_url_raw( rest_url( IOTDW_Rest_Api::NAMESPACE ) ) )
    659                         );
    660                 }
     672        }
     673
     674        /**
     675         * Chart.js + chart-widget.js/css (tylko Pro — pliki istnieją tylko tam).
     676         */
     677        private function enqueue_chart_assets(): void {
     678                if ( ! class_exists( 'IOTDW_Renderer_Chart' ) || wp_script_is( 'iot-data-widget-chart', 'enqueued' ) ) {
     679                        return;
     680                }
     681
     682                wp_enqueue_script(
     683                        'chart-js',
     684                        IOTDW_PLUGIN_URL . 'public/js/vendor/chart.umd.min.js',
     685                        array(),
     686                        '4.5.1',
     687                        true
     688                );
     689
     690                wp_enqueue_script(
     691                        'iot-data-widget-chart',
     692                        IOTDW_PLUGIN_URL . 'public/js/chart-widget.js',
     693                        array( 'chart-js' ),
     694                        IOTDW_VERSION,
     695                        true
     696                );
     697
     698                wp_enqueue_style(
     699                        'iot-data-widget-chart',
     700                        IOTDW_PLUGIN_URL . 'public/css/chart-widget.css',
     701                        array( 'iot-data-widget' ),
     702                        IOTDW_VERSION
     703                );
     704        }
     705
     706        /**
     707         * Skrypt jednorazowego odświeżenia wartości po załadowaniu strony.
     708         */
     709        private function enqueue_refresh_script(): void {
     710                if ( wp_script_is( 'iot-data-widget-widget-refresh', 'enqueued' ) ) {
     711                        return;
     712                }
     713
     714                wp_enqueue_script(
     715                        'iot-data-widget-widget-refresh',
     716                        IOTDW_PLUGIN_URL . 'public/js/widget-refresh.js',
     717                        array(),
     718                        IOTDW_VERSION,
     719                        true
     720                );
     721
     722                wp_localize_script(
     723                        'iot-data-widget-widget-refresh',
     724                        'iotdwWidgetRefresh',
     725                        array( 'restUrl' => esc_url_raw( rest_url( IOTDW_Rest_Api::NAMESPACE ) ) )
     726                );
    661727        }
    662728
    … …  
    702768         *
    703769         * @param IOTDW_Abstract_Connector $connector Instancja konektora.
    704          * @param string                    $entity_id ID encji / ścieżka do wartości.
     770         * @param string                   $entity_id ID encji / ścieżka do wartości.
    705771         * @return array
    706772         */
    … …  
    738804                                'entities' => $connector->list_entities(),
    739805                                'error'    => null,
     806                        );
     807                } catch ( \RuntimeException $e ) {
     808                        // Celowe błędy konektora (brak połączenia, 401, adres zablokowany)
     809                        // — komunikat już jest dla użytkownika, bez "Unexpected".
     810                        return array(
     811                                'entities' => array(),
     812                                'error'    => $e->getMessage(),
    740813                        );
    741814                } catch ( \Throwable $e ) {
  • iot-data-widget/tags/1.0.2/includes/class-value-normalizer.php

    r3728389 r3732074  
    8787                );
    8888        }
     89
     90        /**
     91         * Tekst stanu do pokazania człowiekowi: dosłowne "true"/"false" (surowy
     92         * bool z JSON, np. Shelly switch:0.output) → przetłumaczone "On"/"Off",
     93         * tak jak robi to już IOTDW_Shortcode dla karty/listy. Każdy inny tekst
     94         * (HA "home", Tasmota "ON"...) wraca bez zmian. Nie dla webhooków —
     95         * tam display_value zostaje surowe (kontrakt dla odbiorców maszynowych).
     96         *
     97         * @param string $display_value display_value z konektora.
     98         * @return string
     99         */
     100        public static function human_display( string $display_value ): string {
     101                switch ( strtolower( trim( $display_value ) ) ) {
     102                        case 'true':
     103                                return __( 'On', 'iot-data-widget' );
     104                        case 'false':
     105                                return __( 'Off', 'iot-data-widget' );
     106                        default:
     107                                return $display_value;
     108                }
     109        }
    89110}
  • iot-data-widget/tags/1.0.2/includes/class-widget-usage-tracker.php

    r3730704 r3732074  
    5151                add_action( 'save_post', array( $this, 'sync_post' ) );
    5252                add_action( 'before_delete_post', array( $this, 'remove_post' ) );
     53
     54                foreach ( array( 'sidebars_widgets', 'widget_block', 'widget_text', 'widget_custom_html', IOTDW_Connector_Manager::CONNECTIONS_OPTION ) as $option ) {
     55                        add_action( 'update_option_' . $option, array( __CLASS__, 'flush_widget_area_usage' ) );
     56                        add_action( 'add_option_' . $option, array( __CLASS__, 'flush_widget_area_usage' ) );
     57                }
     58        }
     59
     60        /**
     61         * Transient z parami użytymi w obszarach widgetów motywu klasycznego.
     62         */
     63        const WIDGET_AREA_TRANSIENT = 'iotdw_widget_area_usage';
     64
     65        /**
     66         * Typy widgetów, w których treści szukamy [iotdw_widget] (id_base => pole treści).
     67         */
     68        const WIDGET_CONTENT_FIELDS = array(
     69                'block'       => 'content',
     70                'text'        => 'text',
     71                'custom_html' => 'content',
     72        );
     73
     74        /**
     75         * Kasuje cache par z obszarów widgetów (zmiana widgetów/pasków/połączeń).
     76         */
     77        public static function flush_widget_area_usage(): void {
     78                delete_transient( self::WIDGET_AREA_TRANSIENT );
     79        }
     80
     81        /**
     82         * Użycie widgetów w AKTYWNYCH obszarach widgetów motywu klasycznego —
     83         * odpowiednik wierszy tabeli użycia dla postów (to samo: connection_id,
     84         * entity_id, style), ale liczony z opcji widgetów zamiast zapisywany,
     85         * żeby nie zmieniać schematu tabeli (post_id jest kluczem jej
     86         * czyszczenia). Widgety nieaktywne (wp_inactive_widgets) się nie
     87         * liczą — nie są nigdzie wyświetlane. Cache w transiencie, kasowany przy
     88         * każdej zmianie widgetów/pasków/połączeń (patrz konstruktor).
     89         *
     90         * Konsumenci: is_publicly_used() (/widget-data), odpytywanie MQTT
     91         * (IOTDW_MQTT_Poll_Engine) i zapis historii (IOTDW_History_Recorder).
     92         *
     93         * @return array<int, array{connection_id: string, entity_id: string, style: string}>
     94         */
     95        public function widget_area_usage(): array {
     96                $cached = get_transient( self::WIDGET_AREA_TRANSIENT );
     97
     98                if ( is_array( $cached ) && ( array() === $cached || isset( $cached[0]['style'] ) ) ) {
     99                        return $cached;
     100                }
     101
     102                $usage = array();
     103                // Opcja wprost — wp_get_sidebars_widgets() poza adminem zapamiętuje
     104                // wynik w globalnej zmiennej na całe żądanie.
     105                $sidebars = get_option( 'sidebars_widgets', array() );
     106                $sidebars = is_array( $sidebars ) ? $sidebars : array();
     107
     108                foreach ( $sidebars as $sidebar_id => $widget_ids ) {
     109                        if ( 'wp_inactive_widgets' === $sidebar_id || 'array_version' === $sidebar_id || ! is_array( $widget_ids ) ) {
     110                                continue;
     111                        }
     112
     113                        foreach ( $widget_ids as $widget_id ) {
     114                                if ( ! preg_match( '/^(.+)-(\d+)$/', (string) $widget_id, $m ) || ! isset( self::WIDGET_CONTENT_FIELDS[ $m[1] ] ) ) {
     115                                        continue;
     116                                }
     117
     118                                $instances = get_option( 'widget_' . $m[1], array() );
     119                                $content   = $instances[ (int) $m[2] ][ self::WIDGET_CONTENT_FIELDS[ $m[1] ] ] ?? '';
     120
     121                                if ( ! is_string( $content ) || '' === $content ) {
     122                                        continue;
     123                                }
     124
     125                                foreach ( $this->extract_tracked_pairs( $content ) as $pair ) {
     126                                        $usage[ $pair['connection_id'] . '|' . $pair['entity_id'] . '|' . $pair['style'] ] = $pair;
     127                                }
     128                        }
     129                }
     130
     131                $usage = array_values( $usage );
     132
     133                set_transient( self::WIDGET_AREA_TRANSIENT, $usage, DAY_IN_SECONDS );
     134
     135                return $usage;
    53136        }
    54137
    … …  
    201284         * kwalifikują się — nie ma dla nich żadnego posta do zweryfikowania.
    202285         *
     286         * 1.0.2: kwalifikuje się też widget w AKTYWNYM obszarze widgetów motywu
     287         * klasycznego (patrz widget_area_usage()) — wyświetlany publicznie tak
     288         * samo jak opublikowany post. Wcześniej taki widget nigdy się nie
     289         * odświeżał (404 z /widget-data).
     290         *
    203291         * @param string $connection_id ID połączenia (kanoniczne, nie nazwa).
    204292         * @param string $entity_id     ID encji.
    … …  
    228316                        // ma, ale żądający je zna (poprawne ciasteczko).
    229317                        if ( null !== $post && ! post_password_required( $post ) ) {
     318                                return true;
     319                        }
     320                }
     321
     322                foreach ( $this->widget_area_usage() as $pair ) {
     323                        if ( $pair['connection_id'] === $connection_id && $pair['entity_id'] === $entity_id ) {
    230324                                return true;
    231325                        }
  • iot-data-widget/tags/1.0.2/includes/connectors/abstract-connector.php

    r3728389 r3732074  
    132132                }
    133133
     134                if ( null !== IOTDW_Http_Client::check_blocked( $base_url ) ) {
     135                        // Odrzucone przed wysłaniem — komunikat IOTDW_Http_Client już mówi dlaczego.
     136                        return $message;
     137                }
     138
    134139                return $message . ' ' . sprintf(
    135140                        /* translators: %s: the local-network host/IP that was configured */
  • iot-data-widget/tags/1.0.2/includes/connectors/class-ha-connector.php

    r3728389 r3732074  
    5454         */
    5555        public function fetch_data( string $entity_id ): array {
    56                 $response = wp_remote_get(
     56                $response = IOTDW_Http_Client::get(
    5757                        $this->base_url . '/api/states/' . rawurlencode( $entity_id ),
    5858                        array(
    … …  
    137137                // zwykle znacznie więcej danych niż pojedynczy odczyt encji, więc
    138138                // dłuższy zapas jest tu uzasadniony bez kosztu dla front-endu.
    139                 $response = wp_remote_get(
     139                $response = IOTDW_Http_Client::get(
    140140                        $this->base_url . '/api/states',
    141141                        array(
  • iot-data-widget/tags/1.0.2/includes/connectors/class-rest-connector.php

    r3728389 r3732074  
    153153                }
    154154
    155                 $response = wp_remote_get(
     155                $response = IOTDW_Http_Client::get(
    156156                        $request_url,
    157157                        array(
  • iot-data-widget/tags/1.0.2/iot-data-widget.php

    r3730704 r3732074  
    44 * Plugin Name:       IoT Data Widget
    55 * Description:       Displays live data from smart home devices (Home Assistant, REST API, MQTT, Shelly/Tasmota/ESPHome presets, Shelly Cloud) on your website via shortcode and Gutenberg block, with a guided setup wizard, a redesigned and auto-refreshing appearance, and a built-in diagnostics page. Pro adds threshold alerts, email reports, and a public embed mode.
    6  * Version:           1.0.1
     6 * Version:           1.0.2
    77 * Requires at least: 6.0
    88 * Requires PHP:      8.1
    … …  
    175175        // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals
    176176    }
    177     define( 'IOTDW_VERSION', '1.0.1' );
     177    define( 'IOTDW_VERSION', '1.0.2' );
    178178    define( 'IOTDW_PLUGIN_FILE', __FILE__ );
    179179    define( 'IOTDW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • iot-data-widget/tags/1.0.2/languages/iot-data-widget.pot

    r3730704 r3732074  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: IoT Data Widget 1.0.1\n"
     5"Project-Id-Version: IoT Data Widget 1.0.2\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/iot-data-widget\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-06T10:46:23+00:00\n"
     12"POT-Creation-Date: 2026-10-06T17:55:40+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    8282#: admin/class-email-reports-page.php:151
    8383#: admin/class-email-reports-page.php:230
    84 #: admin/class-settings-page.php:213
    85 #: admin/class-settings-page.php:239
    86 #: admin/class-settings-page.php:261
    87 #: admin/class-settings-page.php:367
    88 #: admin/class-settings-page.php:435
     84#: admin/class-settings-page.php:215
     85#: admin/class-settings-page.php:246
     86#: admin/class-settings-page.php:268
     87#: admin/class-settings-page.php:374
     88#: admin/class-settings-page.php:442
    8989#: admin/class-style-settings-page.php:157
     90#: includes/class-local-network.php:140
     91#: includes/class-local-network.php:173
    9092#: includes/premium/class-history-csv-export.php:59
    9193msgid "Insufficient permissions."
    … …  
    140142#: admin/class-alert-rules-page.php:216
    141143#: admin/class-email-reports-page.php:207
    142 #: admin/class-settings-page.php:416
     144#: admin/class-settings-page.php:423
    143145msgid "Please choose a valid connection."
    144146msgstr ""
    … …  
    146148#: admin/class-alert-rules-page.php:220
    147149#: admin/class-email-reports-page.php:211
    148 #: admin/class-settings-page.php:420
     150#: admin/class-settings-page.php:427
    149151msgid "Entity is required."
    150152msgstr ""
    … …  
    172174msgstr ""
    173175
    174 #: admin/class-diagnostics-page.php:169
     176#: admin/class-diagnostics-page.php:170
    175177msgid "IoT Data Widget — Diagnostic Report"
    176178msgstr ""
    177179
    178180#. translators: %s: current date/time in UTC, e.g. "2026-08-13 12:00:00"
    179 #: admin/class-diagnostics-page.php:171
     181#: admin/class-diagnostics-page.php:172
    180182#, php-format
    181183msgid "Generated: %s UTC"
    182184msgstr ""
    183185
    184 #: admin/class-diagnostics-page.php:174
     186#: admin/class-diagnostics-page.php:175
    185187msgid "== Environment =="
    186188msgstr ""
    187189
    188190#. translators: %s: PHP version, e.g. "8.1.34"
    189 #: admin/class-diagnostics-page.php:176
     191#: admin/class-diagnostics-page.php:177
    190192#, php-format
    191193msgid "PHP version: %s"
    … …  
    193195
    194196#. translators: %s: WordPress version, e.g. "7.0.3"
    195 #: admin/class-diagnostics-page.php:178
     197#: admin/class-diagnostics-page.php:179
    196198#, php-format
    197199msgid "WordPress version: %s"
    … …  
    199201
    200202#. translators: %s: active theme name and version, e.g. "Twenty Twenty-Five 1.4"
    201 #: admin/class-diagnostics-page.php:180
     203#: admin/class-diagnostics-page.php:181
    202204#, php-format
    203205msgid "Active theme: %s"
    … …  
    205207
    206208#. translators: %s: PHP memory limit, e.g. "256M"
    207 #: admin/class-diagnostics-page.php:182
     209#: admin/class-diagnostics-page.php:183
    208210#, php-format
    209211msgid "Memory limit: %s"
    … …  
    211213
    212214#. translators: %s: PHP max execution time in seconds, e.g. "30"
    213 #: admin/class-diagnostics-page.php:184
     215#: admin/class-diagnostics-page.php:185
    214216#, php-format
    215217msgid "Max execution time: %ss"
    … …  
    217219
    218220#. translators: %s: comma-separated list of other active plugin names, or "None"
    219 #: admin/class-diagnostics-page.php:187
     221#: admin/class-diagnostics-page.php:188
    220222#, php-format
    221223msgid "Other active plugins: %s"
    222224msgstr ""
    223225
    224 #: admin/class-diagnostics-page.php:188
     226#: admin/class-diagnostics-page.php:189
    225227#: admin/views/diagnostics.php:76
    226228msgid "None"
    227229msgstr ""
    228230
    229 #: admin/class-diagnostics-page.php:192
     231#: admin/class-diagnostics-page.php:193
    230232msgid "== Scheduled tasks =="
    231233msgstr ""
    232234
    233 #: admin/class-diagnostics-page.php:195
     235#: admin/class-diagnostics-page.php:196
    234236#: admin/views/diagnostics.php:102
    235237msgid "Not scheduled"
    236238msgstr ""
    237239
    238 #: admin/class-diagnostics-page.php:197
     240#: admin/class-diagnostics-page.php:198
    239241#: admin/views/diagnostics.php:104
    240242msgid "Scheduled, never run yet"
    241243msgstr ""
    242244
    243 #: admin/class-diagnostics-page.php:199
     245#: admin/class-diagnostics-page.php:200
    244246#: admin/views/diagnostics.php:106
    245247msgid "Scheduled & running"
    … …  
    247249
    248250#. translators: %s: reason the last cron run failed
    249 #: admin/class-diagnostics-page.php:203
     251#: admin/class-diagnostics-page.php:204
    250252#, php-format
    251253msgid "Scheduled, last run failed: %s"
    … …  
    253255
    254256#. translators: 1: task label, 2: status text, 3: next scheduled run, 4: last run time
    255 #: admin/class-diagnostics-page.php:213
     257#: admin/class-diagnostics-page.php:214
    256258#, php-format
    257259msgid "%1$s: %2$s (next: %3$s, last: %4$s)"
    258260msgstr ""
    259261
    260 #: admin/class-diagnostics-page.php:222
     262#: admin/class-diagnostics-page.php:223
    261263msgid "== Connections =="
    262264msgstr ""
    263265
    264 #: admin/class-diagnostics-page.php:224
    265 #: admin/class-diagnostics-page.php:268
     266#: admin/class-diagnostics-page.php:225
     267#: admin/class-diagnostics-page.php:270
    266268msgid "None configured."
    267269msgstr ""
    268270
    269 #: admin/class-diagnostics-page.php:229
     271#: admin/class-diagnostics-page.php:230
    270272msgid "no data received yet"
    271273msgstr ""
    272274
    273 #: admin/class-diagnostics-page.php:230
     275#: admin/class-diagnostics-page.php:231
    274276msgid "active"
    275277msgstr ""
    276278
    277 #: admin/class-diagnostics-page.php:230
     279#: admin/class-diagnostics-page.php:231
    278280msgid "inactive"
    279281msgstr ""
    280282
    281283#. translators: 1: connection name, 2: connector type label, 3: push status text, 4: last received time
    282 #: admin/class-diagnostics-page.php:234
     284#: admin/class-diagnostics-page.php:235
    283285#, php-format
    284286msgid "%1$s (%2$s): %3$s, last received %4$s"
    285287msgstr ""
    286288
    287 #: admin/class-diagnostics-page.php:245
     289#: admin/class-diagnostics-page.php:246
    288290#: admin/views/diagnostics.php:208
    289 #: admin/views/diagnostics.php:327
     291#: admin/views/diagnostics.php:329
    290292msgid "No data yet"
    291293msgstr ""
    292294
    293 #: admin/class-diagnostics-page.php:246
     295#: admin/class-diagnostics-page.php:247
    294296#: admin/views/diagnostics.php:210
    295297msgid "OK"
    296298msgstr ""
    297299
    298 #: admin/class-diagnostics-page.php:246
     300#: admin/class-diagnostics-page.php:247
    299301#: admin/views/diagnostics.php:212
    300302msgid "Error"
    … …  
    302304
    303305#. translators: 1: connection name, 2: connector type label, 3: status text, 4: consecutive error count, 5: last checked time
    304 #: admin/class-diagnostics-page.php:250
     306#: admin/class-diagnostics-page.php:251
    305307#, php-format
    306308msgid "%1$s (%2$s): %3$s, %4$d consecutive errors, last checked %5$s"
    … …  
    308310
    309311#. translators: %s: last recorded error message for this connection
    310 #: admin/class-diagnostics-page.php:260
     312#: admin/class-diagnostics-page.php:261
    311313#, php-format
    312314msgid "  Last error: %s"
    313315msgstr ""
    314316
    315 #: admin/class-diagnostics-page.php:266
     317#: admin/class-diagnostics-page.php:268
    316318msgid "== Alert rules =="
    317319msgstr ""
    318320
    319321#. translators: 1: connection name, 2: entity ID, 3: comparison operator (> or <), 4: threshold value, 5: "Triggered" or "Normal", 6: optional " (last triggered: ...)" note or empty string
    320 #: admin/class-diagnostics-page.php:275
     322#: admin/class-diagnostics-page.php:277
    321323#, php-format
    322324msgid "%1$s / %2$s: %3$s %4$s — %5$s%6$s"
    323325msgstr ""
    324326
    325 #: admin/class-diagnostics-page.php:280
    326 #: admin/views/diagnostics.php:251
     327#: admin/class-diagnostics-page.php:282
     328#: admin/views/diagnostics.php:252
    327329#: includes/premium/admin-views/alert-rules-page.php:54
    328330msgid "Triggered"
    329331msgstr ""
    330332
    331 #: admin/class-diagnostics-page.php:280
    332 #: admin/views/diagnostics.php:252
     333#: admin/class-diagnostics-page.php:282
     334#: admin/views/diagnostics.php:253
    333335#: includes/premium/admin-views/alert-rules-page.php:55
    334336msgid "Normal"
    … …  
    336338
    337339#. translators: %s: last-triggered date/time in UTC
    338 #: admin/class-diagnostics-page.php:283
     340#: admin/class-diagnostics-page.php:285
    339341#, php-format
    340342msgid " (last triggered: %s UTC)"
    341343msgstr ""
    342344
    343 #: admin/class-diagnostics-page.php:290
     345#: admin/class-diagnostics-page.php:292
    344346msgid "== Recent errors =="
    345347msgstr ""
    346348
    347 #: admin/class-diagnostics-page.php:292
     349#: admin/class-diagnostics-page.php:294
    348350msgid "None recorded."
    349351msgstr ""
    350352
    351353#. translators: 1: date/time in UTC, 2: error source, 3: error message
    352 #: admin/class-diagnostics-page.php:297
     354#: admin/class-diagnostics-page.php:299
    353355#, php-format
    354356msgid "[%1$s UTC] %2$s: %3$s"
    355357msgstr ""
    356358
    357 #: admin/class-diagnostics-page.php:306
     359#: admin/class-diagnostics-page.php:308
    358360msgid "== Data size =="
    359361msgstr ""
    360362
    361363#. translators: 1: table label (already translated), 2: row count
    362 #: admin/class-diagnostics-page.php:310
     364#: admin/class-diagnostics-page.php:312
    363365#, php-format
    364366msgid "%1$s: %2$d"
    365367msgstr ""
    366368
    367 #: admin/class-diagnostics-page.php:331
     369#: admin/class-diagnostics-page.php:336
    368370msgid "Recorded value history"
    369371msgstr ""
    370372
    371 #: admin/class-diagnostics-page.php:332
     373#: admin/class-diagnostics-page.php:339
    372374msgid "Widget usage tracking"
    373375msgstr ""
    374376
    375 #: admin/class-diagnostics-page.php:333
    376 #: admin/views/diagnostics.php:235
     377#: admin/class-diagnostics-page.php:342
     378#: admin/views/diagnostics.php:236
    377379#: includes/premium/admin-views/alert-rules-page.php:36
    378380msgid "Alert rules"
    379381msgstr ""
    380382
    381 #: admin/class-diagnostics-page.php:334
     383#: admin/class-diagnostics-page.php:346
    382384msgid "MQTT cache"
    383385msgstr ""
    384386
    385 #: admin/class-diagnostics-page.php:335
     387#: admin/class-diagnostics-page.php:349
    386388msgid "Error log"
    387389msgstr ""
    388390
    389 #: admin/class-diagnostics-page.php:481
     391#: admin/class-diagnostics-page.php:497
    390392msgid "Record value history"
    391393msgstr ""
    392394
    393 #: admin/class-diagnostics-page.php:482
     395#: admin/class-diagnostics-page.php:498
    394396msgid "Clean up old history & error log"
    395397msgstr ""
    396398
    397 #: admin/class-diagnostics-page.php:483
     399#: admin/class-diagnostics-page.php:499
    398400msgid "Poll MQTT brokers"
    399401msgstr ""
    400402
    401 #: admin/class-diagnostics-page.php:487
     403#: admin/class-diagnostics-page.php:500
    402404msgid "Send due email reports"
    403405msgstr ""
    … …  
    421423
    422424#: admin/class-settings-page.php:101
    423 #: admin/views/general-settings-page.php:25
     425#: admin/views/general-settings-page.php:27
    424426msgid "IoT Data Widget — Settings"
    425427msgstr ""
    … …  
    429431msgstr ""
    430432
    431 #: admin/class-settings-page.php:288
     433#: admin/class-settings-page.php:295
    432434msgid "No file was uploaded, or the upload failed."
    433435msgstr ""
    434436
    435 #: admin/class-settings-page.php:292
     437#: admin/class-settings-page.php:299
    436438msgid "The uploaded file is too large to be a valid export."
    437439msgstr ""
    438440
    439 #: admin/class-settings-page.php:298
     441#: admin/class-settings-page.php:305
    440442msgid "Invalid upload."
    441443msgstr ""
    442444
    443 #: admin/class-settings-page.php:304
     445#: admin/class-settings-page.php:311
    444446msgid "Could not read the uploaded file."
    445447msgstr ""
    446448
    447 #: admin/class-settings-page.php:424
     449#: admin/class-settings-page.php:431
    448450msgid "Please choose a valid widget style."
    449451msgstr ""
    … …  
    649651
    650652#: admin/views/connector-form.php:18
    651 #: admin/views/general-settings-page.php:264
     653#: admin/views/general-settings-page.php:281
    652654#: admin/views/wizard/step-push-2-channel.php:35
    653655#: includes/premium/admin-views/alert-rule-form.php:20
    … …  
    663665
    664666#: admin/views/connector-form.php:32
    665 #: admin/views/settings-page.php:139
     667#: admin/views/settings-page.php:141
    666668msgid "Add connection"
    667669msgstr ""
    … …  
    723725#: admin/views/diagnostics.php:93
    724726#: admin/views/diagnostics.php:154
    725 #: admin/views/diagnostics.php:241
     727#: admin/views/diagnostics.php:242
    726728#: includes/premium/admin-views/alert-rules-page.php:44
    727729msgid "Status"
    … …  
    750752#: admin/views/diagnostics.php:171
    751753#: admin/views/diagnostics.php:202
    752 #: admin/views/diagnostics.php:257
    753 #: admin/views/diagnostics.php:290
     754#: admin/views/diagnostics.php:258
     755#: admin/views/diagnostics.php:292
    754756#, php-format
    755757msgid "%s UTC"
    … …  
    761763
    762764#: admin/views/diagnostics.php:151
    763 #: admin/views/diagnostics.php:238
    764 #: admin/views/general-settings-page.php:169
    765 #: admin/views/general-settings-page.php:227
    766 #: admin/views/general-settings-page.php:277
     765#: admin/views/diagnostics.php:239
     766#: admin/views/general-settings-page.php:186
     767#: admin/views/general-settings-page.php:244
     768#: admin/views/general-settings-page.php:294
    767769#: includes/premium/admin-views/alert-rule-form.php:33
    768770#: includes/premium/admin-views/alert-rules-page.php:40
    … …  
    775777
    776778#: admin/views/diagnostics.php:152
    777 #: admin/views/settings-page.php:67
     779#: admin/views/settings-page.php:69
    778780msgid "Type"
    779781msgstr ""
    … …  
    804806
    805807#: admin/views/diagnostics.php:230
    806 #: admin/views/settings-page.php:128
     808#: admin/views/settings-page.php:130
    807809msgid "No connections configured yet."
    808810msgstr ""
    809811
    810 #: admin/views/diagnostics.php:239
    811 #: admin/views/general-settings-page.php:182
    812 #: admin/views/general-settings-page.php:193
    813 #: admin/views/general-settings-page.php:228
    814 #: admin/views/general-settings-page.php:293
    815 #: admin/views/general-settings-page.php:304
     812#: admin/views/diagnostics.php:240
     813#: admin/views/general-settings-page.php:199
     814#: admin/views/general-settings-page.php:210
     815#: admin/views/general-settings-page.php:245
     816#: admin/views/general-settings-page.php:310
     817#: admin/views/general-settings-page.php:321
    816818#: admin/views/wizard/step-5-done.php:34
    817819#: admin/views/wizard/step-push-3-snippet.php:117
    … …  
    831833msgstr[1] ""
    832834
    833 #: admin/views/diagnostics.php:240
     835#: admin/views/diagnostics.php:241
    834836#: includes/premium/admin-views/alert-rule-form.php:81
    835837#: includes/premium/admin-views/alert-rules-page.php:42
    … …  
    837839msgstr ""
    838840
    839 #: admin/views/diagnostics.php:242
     841#: admin/views/diagnostics.php:243
    840842msgid "Last triggered"
    841843msgstr ""
    842844
    843 #: admin/views/diagnostics.php:271
     845#: admin/views/diagnostics.php:272
    844846#: includes/premium/admin-views/alert-rules-page.php:81
    845847msgid "No alert rules configured yet."
    846848msgstr ""
    847849
    848 #: admin/views/diagnostics.php:276
     850#: admin/views/diagnostics.php:278
    849851msgid "Recent errors"
    850852msgstr ""
    851853
    852 #: admin/views/diagnostics.php:279
     854#: admin/views/diagnostics.php:281
    853855msgid "When"
    854856msgstr ""
    855857
    856 #: admin/views/diagnostics.php:280
     858#: admin/views/diagnostics.php:282
    857859msgid "Source"
    858860msgstr ""
    859861
    860 #: admin/views/diagnostics.php:281
     862#: admin/views/diagnostics.php:283
    861863msgid "Message"
    862864msgstr ""
    863865
    864 #: admin/views/diagnostics.php:300
     866#: admin/views/diagnostics.php:302
    865867msgid "No errors recorded — everything is working."
    866868msgstr ""
    867869
    868 #: admin/views/diagnostics.php:305
     870#: admin/views/diagnostics.php:307
    869871msgid "Data size"
    870872msgstr ""
    871873
    872 #: admin/views/diagnostics.php:311
     874#: admin/views/diagnostics.php:313
    873875msgid "Table"
    874876msgstr ""
    875877
    876 #: admin/views/diagnostics.php:312
     878#: admin/views/diagnostics.php:314
    877879msgid "Rows"
    878880msgstr ""
    879881
    880 #: admin/views/general-settings-page.php:29
     882#: admin/views/general-settings-page.php:31
    881883msgid "Settings saved."
    882884msgstr ""
    883885
    884 #: admin/views/general-settings-page.php:30
     886#: admin/views/general-settings-page.php:32
    885887msgid "Manually tracked entity added."
    886888msgstr ""
    887889
    888 #: admin/views/general-settings-page.php:31
     890#: admin/views/general-settings-page.php:33
    889891msgid "Manually tracked entity removed."
    890892msgstr ""
    891893
    892894#. translators: %d: number of imported connections
    893 #: admin/views/general-settings-page.php:50
     895#: admin/views/general-settings-page.php:52
    894896#, php-format
    895897msgid "Imported %d connection."
    … …  
    899901
    900902#. translators: %d: number of skipped entries
    901 #: admin/views/general-settings-page.php:61
     903#: admin/views/general-settings-page.php:63
    902904#, php-format
    903905msgid "Skipped %d entry:"
    … …  
    906908msgstr[1] ""
    907909
    908 #: admin/views/general-settings-page.php:74
     910#: admin/views/general-settings-page.php:76
    909911msgid "Imported connections need their password/authorization re-entered before use — these are never included in an export, by design."
    910912msgstr ""
    911913
    912 #: admin/views/general-settings-page.php:79
     914#: admin/views/general-settings-page.php:81
    913915msgid "Import failed:"
    914916msgstr ""
    915917
    916 #: admin/views/general-settings-page.php:86
     918#: admin/views/general-settings-page.php:88
    917919msgid "Branding"
    918920msgstr ""
    919921
    920 #: admin/views/general-settings-page.php:92
     922#: admin/views/general-settings-page.php:94
    921923msgid "Front-end attribution"
    922924msgstr ""
    923925
    924 #: admin/views/general-settings-page.php:96
     926#: admin/views/general-settings-page.php:98
    925927msgid "Show \"Powered by IoT Data Widget\" beneath widgets"
    926928msgstr ""
    927929
    928 #: admin/views/general-settings-page.php:99
     930#: admin/views/general-settings-page.php:101
    929931msgid "Off by default. Turn this on if you'd like to credit the plugin beneath your widgets."
    930932msgstr ""
    931933
    932 #: admin/views/general-settings-page.php:104
     934#: admin/views/general-settings-page.php:106
    933935msgid "Stale data threshold"
    934936msgstr ""
    935937
    936 #: admin/views/general-settings-page.php:114
     938#: admin/views/general-settings-page.php:116
    937939msgid "minutes"
    938940msgstr ""
    939941
    940 #: admin/views/general-settings-page.php:116
     942#: admin/views/general-settings-page.php:118
    941943msgid "Past this age, widgets show the value grayed out with a timestamp instead of implying it's current — mainly relevant for push channels, where a device can stop sending data without any explicit connection error."
    942944msgstr ""
    943945
    944 #: admin/views/general-settings-page.php:121
     946#: admin/views/general-settings-page.php:123
     947msgid "Local network"
     948msgstr ""
     949
     950#: admin/views/general-settings-page.php:127
     951msgid "Allow local network addresses"
     952msgstr ""
     953
     954#: admin/views/general-settings-page.php:130
     955msgid "Turn this on only if WordPress runs on the same network as your devices (e.g. a home server) and connects to addresses like 192.168.x.x or homeassistant.local. Off by default: it stops a misconfigured or malicious URL from reaching other services on your server's network. Cloud metadata and link-local addresses are always blocked."
     956msgstr ""
     957
     958#: admin/views/general-settings-page.php:133
     959msgid "On a multisite network, only a network administrator can change this."
     960msgstr ""
     961
     962#: admin/views/general-settings-page.php:138
    945963msgid "Uninstall"
    946964msgstr ""
    947965
    948 #: admin/views/general-settings-page.php:125
     966#: admin/views/general-settings-page.php:142
    949967msgid "Remove all plugin data when the plugin is deleted"
    950968msgstr ""
    951969
    952 #: admin/views/general-settings-page.php:128
     970#: admin/views/general-settings-page.php:145
    953971msgid "Off by default. Deactivating the plugin (e.g. during an update) never touches your data either way — only checking this box AND THEN deleting the plugin from the Plugins screen removes connections, history, alerts and every other setting. Leave this unchecked if you might reinstall later."
    954972msgstr ""
    955973
    956 #: admin/views/general-settings-page.php:133
     974#: admin/views/general-settings-page.php:150
    957975msgid "Save settings"
    958976msgstr ""
    959977
    960 #: admin/views/general-settings-page.php:136
     978#: admin/views/general-settings-page.php:153
    961979msgid "Export connections"
    962980msgstr ""
    963981
    964 #: admin/views/general-settings-page.php:137
     982#: admin/views/general-settings-page.php:154
    965983msgid "Downloads a JSON file with all your connections. Passwords and authorization tokens are never included — re-enter them after importing."
    966984msgstr ""
    967985
    968 #: admin/views/general-settings-page.php:141
     986#: admin/views/general-settings-page.php:158
    969987msgid "Download export"
    970988msgstr ""
    971989
    972 #: admin/views/general-settings-page.php:144
     990#: admin/views/general-settings-page.php:161
    973991msgid "Import connections"
    974992msgstr ""
    975993
    976 #: admin/views/general-settings-page.php:145
     994#: admin/views/general-settings-page.php:162
    977995msgid "Imports connections from a previously exported JSON file. Connections with a matching ID are overwritten; others are added. Existing connections not present in the file are left untouched."
    978996msgstr ""
    979997
    980 #: admin/views/general-settings-page.php:150
     998#: admin/views/general-settings-page.php:167
    981999msgid "Import"
    9821000msgstr ""
    9831001
    984 #: admin/views/general-settings-page.php:153
     1002#: admin/views/general-settings-page.php:170
    9851003msgid "Export history data"
    9861004msgstr ""
    9871005
    988 #: admin/views/general-settings-page.php:156
     1006#: admin/views/general-settings-page.php:173
    9891007#: includes/premium/class-history-csv-export.php:71
    9901008msgid "CSV export of recorded history requires the Pro version."
    9911009msgstr ""
    9921010
    993 #: admin/views/general-settings-page.php:160
     1011#: admin/views/general-settings-page.php:177
    9941012msgid "Downloads the raw recorded history for one connection/entity as a CSV file, for further analysis outside the plugin. Only entities used by a Chart/Sparkline widget (or manually tracked below) have history recorded."
    9951013msgstr ""
    9961014
    997 #: admin/views/general-settings-page.php:172
    998 #: admin/views/general-settings-page.php:280
     1015#: admin/views/general-settings-page.php:189
     1016#: admin/views/general-settings-page.php:297
    9991017#: includes/premium/admin-views/alert-rule-form.php:40
    10001018#: includes/premium/admin-views/email-report-form.php:40
    … …  
    10031021msgstr ""
    10041022
    1005 #: admin/views/general-settings-page.php:187
    1006 #: admin/views/general-settings-page.php:298
     1023#: admin/views/general-settings-page.php:204
     1024#: admin/views/general-settings-page.php:315
    10071025#: includes/premium/admin-views/alert-rule-form.php:58
    10081026#: includes/premium/admin-views/email-report-form.php:58
    … …  
    10121030msgstr ""
    10131031
    1014 #: admin/views/general-settings-page.php:197
     1032#: admin/views/general-settings-page.php:214
    10151033msgid "Useful if the entity was renamed/removed from the live connection since history was recorded."
    10161034msgstr ""
    10171035
    1018 #: admin/views/general-settings-page.php:201
    1019 #: admin/views/general-settings-page.php:315
     1036#: admin/views/general-settings-page.php:218
     1037#: admin/views/general-settings-page.php:332
    10201038#: includes/premium/admin-views/alert-rule-form.php:75
    10211039#: includes/premium/admin-views/email-report-form.php:75
    … …  
    10251043msgstr ""
    10261044
    1027 #: admin/views/general-settings-page.php:207
     1045#: admin/views/general-settings-page.php:224
    10281046msgid "From"
    10291047msgstr ""
    10301048
    1031 #: admin/views/general-settings-page.php:211
     1049#: admin/views/general-settings-page.php:228
    10321050msgid "To"
    10331051msgstr ""
    10341052
    1035 #: admin/views/general-settings-page.php:216
     1053#: admin/views/general-settings-page.php:233
    10361054msgid "Download CSV"
    10371055msgstr ""
    10381056
    1039 #: admin/views/general-settings-page.php:220
     1057#: admin/views/general-settings-page.php:237
    10401058msgid "Manually tracked entities"
    10411059msgstr ""
    10421060
    1043 #: admin/views/general-settings-page.php:222
     1061#: admin/views/general-settings-page.php:239
    10441062msgid "Widgets placed in post/page content are detected automatically. If you output a widget from a theme template instead (e.g. in a widget area or a custom page template), add it here so it still gets polled for history/alerts even though there's no post content to scan."
    10451063msgstr ""
    10461064
    1047 #: admin/views/general-settings-page.php:229
     1065#: admin/views/general-settings-page.php:246
    10481066#: blocks/iot-widget/src/edit.js:478
    10491067msgid "Style"
    10501068msgstr ""
    10511069
    1052 #: admin/views/general-settings-page.php:241
     1070#: admin/views/general-settings-page.php:258
    10531071msgid "Stop tracking this entity?"
    10541072msgstr ""
    10551073
    1056 #: admin/views/general-settings-page.php:242
     1074#: admin/views/general-settings-page.php:259
    10571075#: blocks/iot-widget/src/grid-item-editor.js:117
    10581076msgid "Remove"
    10591077msgstr ""
    10601078
    1061 #: admin/views/general-settings-page.php:255
     1079#: admin/views/general-settings-page.php:272
    10621080msgid "No manually tracked entities yet."
    10631081msgstr ""
    10641082
    1065 #: admin/views/general-settings-page.php:256
     1083#: admin/views/general-settings-page.php:273
    10661084msgid "Track your first entity"
    10671085msgstr ""
    10681086
    1069 #: admin/views/general-settings-page.php:288
     1087#: admin/views/general-settings-page.php:305
    10701088#: includes/premium/admin-views/alert-rule-form.php:48
    10711089#: includes/premium/admin-views/email-report-form.php:48
    … …  
    10731091msgstr ""
    10741092
    1075 #: admin/views/general-settings-page.php:321
     1093#: admin/views/general-settings-page.php:338
    10761094msgid "Widget style"
    10771095msgstr ""
    10781096
    1079 #: admin/views/general-settings-page.php:324
     1097#: admin/views/general-settings-page.php:341
    10801098#: blocks/iot-widget/src/edit.js:75
    10811099msgid "Card"
    10821100msgstr ""
    10831101
    1084 #: admin/views/general-settings-page.php:325
     1102#: admin/views/general-settings-page.php:342
    10851103#: blocks/iot-widget/src/edit.js:76
    10861104msgid "Status list"
    10871105msgstr ""
    10881106
    1089 #: admin/views/general-settings-page.php:326
     1107#: admin/views/general-settings-page.php:343
    10901108#: blocks/iot-widget/src/edit.js:77
    10911109msgid "Gauge"
    10921110msgstr ""
    10931111
    1094 #: admin/views/general-settings-page.php:327
     1112#: admin/views/general-settings-page.php:344
    10951113#: blocks/iot-widget/src/edit.js:78
    10961114msgid "Chart"
    10971115msgstr ""
    10981116
    1099 #: admin/views/general-settings-page.php:328
     1117#: admin/views/general-settings-page.php:345
    11001118#: blocks/iot-widget/src/edit.js:79
    11011119msgid "Mini trend (sparkline)"
    11021120msgstr ""
    11031121
    1104 #: admin/views/general-settings-page.php:331
     1122#: admin/views/general-settings-page.php:348
    11051123msgid "Must match the style actually rendered by your theme template — Chart/Sparkline are what determine whether this entity gets recorded into history."
    11061124msgstr ""
    11071125
    1108 #: admin/views/general-settings-page.php:337
     1126#: admin/views/general-settings-page.php:354
    11091127msgid "Add manually tracked entity"
    11101128msgstr ""
    11111129
    11121130#: admin/views/partials/connector-fields.php:30
    1113 #: admin/views/settings-page.php:66
     1131#: admin/views/settings-page.php:68
    11141132msgid "Name"
    11151133msgstr ""
    … …  
    11201138
    11211139#: admin/views/partials/connector-fields.php:73
    1122 #: admin/views/settings-page.php:68
     1140#: admin/views/settings-page.php:70
    11231141msgid "URL"
    11241142msgstr ""
    … …  
    11821200msgstr ""
    11831201
    1184 #: admin/views/settings-page.php:36
    1185 msgid "The free version supports one connection. Upgrade to Pro for unlimited connections."
    1186 msgstr ""
    1187 
    1188 #: admin/views/settings-page.php:40
     1202#: admin/views/settings-page.php:38
     1203msgid "The demo connection could not be saved. Please try again, or add a connection manually below."
     1204msgstr ""
     1205
     1206#: admin/views/settings-page.php:42
    11891207msgid "Connection saved."
    11901208msgstr ""
    11911209
    1192 #: admin/views/settings-page.php:41
     1210#: admin/views/settings-page.php:43
    11931211msgid "Connection deleted."
    11941212msgstr ""
    11951213
    1196 #: admin/views/settings-page.php:51
     1214#: admin/views/settings-page.php:53
    11971215msgid "Now that you have a connection, try these next:"
    11981216msgstr ""
    11991217
    1200 #: admin/views/settings-page.php:57
     1218#: admin/views/settings-page.php:59
    12011219msgid "Dismiss this notice."
    12021220msgstr ""
    12031221
    1204 #: admin/views/settings-page.php:62
     1222#: admin/views/settings-page.php:64
    12051223msgid "Saved connections"
    12061224msgstr ""
    12071225
    1208 #: admin/views/settings-page.php:69
     1226#: admin/views/settings-page.php:71
    12091227msgid "Shortcode"
    12101228msgstr ""
    12111229
    1212 #: admin/views/settings-page.php:108
     1230#: admin/views/settings-page.php:110
    12131231#: includes/premium/admin-views/alert-rules-page.php:60
    12141232#: includes/premium/admin-views/email-reports-page.php:56
    … …  
    12161234msgstr ""
    12171235
    1218 #: admin/views/settings-page.php:114
     1236#: admin/views/settings-page.php:116
    12191237msgid "Are you sure you want to delete this connection?"
    12201238msgstr ""
    12211239
    1222 #: admin/views/settings-page.php:129
     1240#: admin/views/settings-page.php:131
    12231241msgid "Add your first connection"
    12241242msgstr ""
    12251243
    1226 #: admin/views/settings-page.php:138
     1244#: admin/views/settings-page.php:140
    12271245msgid "Edit connection"
    12281246msgstr ""
    … …  
    12901308msgstr ""
    12911309
    1292 #: admin/views/wizard/step-0-location.php:31
     1310#: admin/views/wizard/step-0-location.php:32
    12931311msgid "Same network as my devices (Raspberry Pi, NAS, home server) — my site fetches data from the device"
    12941312msgstr ""
    12951313
    1296 #: admin/views/wizard/step-0-location.php:47
     1314#: admin/views/wizard/step-0-location.php:51
    12971315msgid "On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there"
    12981316msgstr ""
    12991317
    1300 #: admin/views/wizard/step-0-location.php:52
     1318#: admin/views/wizard/step-0-location.php:53
     1319msgid "On external/shared hosting, but my device has its own cloud service (a publicly-reachable Home Assistant; Shelly Cloud and Supla in Pro) — my site fetches data from there"
     1320msgstr ""
     1321
     1322#: admin/views/wizard/step-0-location.php:60
    13011323msgid "Not sure? Choose the first option — it's the safer default."
    13021324msgstr ""
    13031325
    1304 #: admin/views/wizard/step-0-location.php:57
     1326#: admin/views/wizard/step-0-location.php:65
    13051327msgid "Devices behind a home router (NAT) almost never accept incoming connections from the internet — if your WordPress is hosted externally, it typically cannot reach a device address like 192.168.x.x at all. Sending data out from the device instead always works, and so does fetching from a cloud service the device already reports to."
    13061328msgstr ""
    13071329
    1308 #: admin/views/wizard/step-0-location.php:71
     1330#: admin/views/wizard/step-0-location.php:79
    13091331msgid "Security note: in push mode, this plugin never stores any credentials to your devices — the device sends data out, authenticated by a token this plugin generates that only allows writing one entity to one channel. In pull mode, a copy of your device's own credentials (e.g. a Home Assistant access token) is stored in your WordPress database."
    13101332msgstr ""
    13111333
    1312 #: admin/views/wizard/step-0-location.php:75
     1334#: admin/views/wizard/step-0-location.php:83
    13131335#: admin/views/wizard/step-1-connection-type.php:92
    13141336#: admin/views/wizard/step-2-configure.php:87
    … …  
    15821604msgstr ""
    15831605
     1606#: includes/class-http-client.php:98
     1607msgid "The server redirected too many times."
     1608msgstr ""
     1609
     1610#: includes/class-http-client.php:133
     1611msgid "The configured URL has no valid host (only http:// and https:// addresses are supported)."
     1612msgstr ""
     1613
     1614#. translators: %s: the host that was blocked
     1615#: includes/class-http-client.php:151
     1616#: includes/class-http-client.php:171
     1617#, php-format
     1618msgid "Refusing to connect to \"%s\" — it resolves to a link-local or reserved address (such as a cloud metadata service). This is always blocked."
     1619msgstr ""
     1620
     1621#. translators: %s: the host that was blocked
     1622#: includes/class-http-client.php:252
     1623#, php-format
     1624msgid "Refusing to connect to \"%s\" — it is a local network address. If WordPress runs on the same network as the device, enable \"Allow local network addresses\" under IoT Data Widget → Settings. Otherwise, use a push channel instead."
     1625msgstr ""
     1626
     1627#: includes/class-local-network.php:205
     1628msgid "Local network addresses are now allowed, so this site can reach devices on your network. You can change this under IoT Data Widget → Settings."
     1629msgstr ""
     1630
     1631#: includes/class-local-network.php:210
     1632msgid "Connections to local network addresses are blocked on this site, and only a network administrator can allow them (IoT Data Widget → Settings). Until then, choose push mode instead."
     1633msgstr ""
     1634
     1635#: includes/class-local-network.php:222
     1636msgid "Security update: IoT Data Widget now refuses connections to local network addresses unless \"Allow local network addresses\" is turned on. This site already connects to a device on your local network, so the setting was turned on automatically — nothing stopped working."
     1637msgstr ""
     1638
     1639#: includes/class-local-network.php:224
     1640msgid "Review settings"
     1641msgstr ""
     1642
     1643#: includes/class-local-network.php:226
     1644msgid "Dismiss"
     1645msgstr ""
     1646
    15841647#: includes/class-push-ingest.php:120
    15851648msgid "Request body is too large."
    … …  
    17021765
    17031766#: includes/class-renderer.php:226
    1704 #: includes/class-shortcode.php:133
     1767#: includes/class-shortcode.php:153
    17051768msgid "This style requires the Pro version."
    17061769msgstr ""
    … …  
    17731836msgstr[1] ""
    17741837
    1775 #: includes/class-rest-api.php:726
     1838#: includes/class-rest-api.php:732
    17761839msgid "Not found."
    17771840msgstr ""
    17781841
    1779 #: includes/class-shortcode.php:146
     1842#: includes/class-shortcode.php:166
    17801843msgid "Fill in the \"connection\" and \"entity\" attributes in the [iotdw_widget] shortcode."
    17811844msgstr ""
    17821845
    17831846#. translators: %s: connection name or ID given in the shortcode
    1784 #: includes/class-shortcode.php:159
    1785 #: includes/class-shortcode.php:461
    1786 #: includes/class-shortcode.php:531
     1847#: includes/class-shortcode.php:179
     1848#: includes/class-shortcode.php:481
     1849#: includes/class-shortcode.php:551
    17871850#, php-format
    17881851msgid "Connection \"%s\" was not found."
    17891852msgstr ""
    17901853
    1791 #: includes/class-shortcode.php:434
     1854#: includes/class-shortcode.php:454
    17921855msgid "Powered by IoT Data Widget"
    17931856msgstr ""
    17941857
    1795 #: includes/class-shortcode.php:448
     1858#: includes/class-shortcode.php:468
    17961859msgid "Fill in the \"connection\" and \"entities\" attributes (comma-separated entity IDs) in the [iotdw_widget style=\"list\"] shortcode."
    17971860msgstr ""
    17981861
    1799 #: includes/class-shortcode.php:512
     1862#: includes/class-shortcode.php:532
    18001863msgid "Add at least one entity to the [iotdw_widget style=\"grid\"] shortcode via the \"items\" attribute."
    18011864msgstr ""
    18021865
    1803 #: includes/class-shortcode.php:513
     1866#: includes/class-shortcode.php:533
    18041867msgid "The \"items\" attribute of the [iotdw_widget style=\"grid\"] shortcode must be a comma-separated list of \"connection|entity\" pairs (e.g. items=\"Home|sensor.temperature,Workshop|relay_state\") — none of the given items matched that format."
    18051868msgstr ""
    18061869
    18071870#. translators: %s: PHP error message
    1808 #: includes/class-shortcode.php:714
    1809 #: includes/class-shortcode.php:746
     1871#: includes/class-shortcode.php:780
     1872#: includes/class-shortcode.php:812
    18101873#, php-format
    18111874msgid "Unexpected connector error: %s"
    18121875msgstr ""
    18131876
    1814 #: includes/class-shortcode.php:861
     1877#: includes/class-shortcode.php:927
     1878#: includes/class-value-normalizer.php:103
    18151879#: blocks/iot-widget/src/edit.js:641
    18161880msgid "On"
    18171881msgstr ""
    18181882
    1819 #: includes/class-shortcode.php:862
     1883#: includes/class-shortcode.php:928
     1884#: includes/class-value-normalizer.php:105
    18201885#: blocks/iot-widget/src/edit.js:651
    18211886msgid "Off"
    … …  
    18231888
    18241889#. translators: %s: the local-network host/IP that was configured
    1825 #: includes/connectors/abstract-connector.php:136
     1890#: includes/connectors/abstract-connector.php:141
    18261891#, php-format
    18271892msgid "This looks like a local network address (%s) — it will only work if your WordPress server is on the same network as the device. Most external/shared hosting can't reach it; consider Shelly Cloud, a publicly reachable Home Assistant instance, or a differently-reachable REST endpoint instead."
    … …  
    23372402msgstr ""
    23382403
    2339 #: includes/premium/class-mqtt-poll-engine.php:186
     2404#: includes/premium/class-mqtt-poll-engine.php:194
    23402405msgid "Missing MQTT broker host in the connection settings."
    23412406msgstr ""
    23422407
    2343 #: includes/premium/class-mqtt-poll-engine.php:261
     2408#: includes/premium/class-mqtt-poll-engine.php:269
    23442409msgid "Missing EMQX REST API URL in the connection settings."
    23452410msgstr ""
    23462411
    23472412#. translators: %d: HTTP status code
    2348 #: includes/premium/class-mqtt-poll-engine.php:298
     2413#: includes/premium/class-mqtt-poll-engine.php:306
    23492414#, php-format
    23502415msgid "EMQX REST API returned an unexpected response code: %d."
    23512416msgstr ""
    23522417
    2353 #: includes/premium/class-mqtt-poll-engine.php:308
     2418#: includes/premium/class-mqtt-poll-engine.php:316
    23542419msgid "Invalid or unexpected response shape from the EMQX REST API."
    23552420msgstr ""
  • iot-data-widget/tags/1.0.2/public/css/widget-styles.css

    r3730704 r3732074  
     1/*
     2 * Wrapper z IOTDW_Shortcode::render() — celowo bez szerokości/marginesów
     3 * poziomych: dostaje je od motywu jak każdy inny blok (kolumna treści).
     4 * Odstęp między dwoma widgetami pod rząd przez :where() (zerowa
     5 * specyficzność) — motywy blokowe (blockGap) i tak go nadpisują swoim.
     6 */
     7:where(.iot-dw-widget-wrap + .iot-dw-widget-wrap) {
     8        margin-top: 1em;
     9}
     10
    111.iot-dw-widget {
    212        display: inline-block;
    … …  
    137147        display: block;
    138148        list-style: none;
    139         /* Tylko poziome — pionowe zostawiamy motywowi/WP (odstęp między blokami). */
    140         margin-left: 0;
    141         margin-right: 0;
     149        margin: 0;
    142150        padding: 0;
    143151        box-sizing: border-box;
    … …  
    351359}
    352360
    353 /*
    354  * Motywy blokowe (np. Twenty Twenty-Five): layout "constrained" wyrównuje
    355  * dzieci do kolumny treści przez `margin-left/right: auto !important`, ale
    356  * na elementach inline-level `auto` liczy się jako 0 — card/gauge/sparkline
    357  * lądowały na lewej krawędzi strony zamiast w kolumnie. Tu: blokowe, szerokość
    358  * wg treści, lewa krawędź = lewa krawędź kolumny. !important, bo reguła WP też
    359  * go ma (wygrywa wyższa specyficzność). Poza layoutem constrained (motywy
    360  * klasyczne, grupa Row/flex) bez zmian — dalej inline, obok siebie.
    361  */
    362 .is-layout-constrained > .iot-dw-widget--card,
    363 .is-layout-constrained > .iot-dw-widget--gauge,
    364 .is-layout-constrained > .iot-dw-widget--sparkline {
    365         width: fit-content;
    366         margin-left: max( 0px, calc( ( 100% - var( --wp--style--global--content-size, 100% ) ) / 2 ) ) !important;
    367         margin-right: auto !important;
    368 }
    369 
    370 .is-layout-constrained > .iot-dw-widget--card {
    371         display: block;
    372 }
    373 
    374 .is-layout-constrained > .iot-dw-widget--gauge,
    375 .is-layout-constrained > .iot-dw-widget--sparkline {
    376         display: flex;
    377 }
  • iot-data-widget/tags/1.0.2/readme.txt

    r3730704 r3732074  
    55Tested up to: 7.1
    66Requires PHP: 8.1
    7 Stable tag: 1.0.1
     7Stable tag: 1.0.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    22222. Activate the plugin through the **Plugins** screen — this opens the **setup wizard** automatically (skip this step by activating several plugins at once, or by clicking "Skip the wizard" on its first screen).
    23233. Follow the wizard: pick a connection type, configure and test it live, choose an entity, and copy the ready-made shortcode from the preview screen — or configure a connection manually under **IoT Data Widget** in the admin menu instead:
    24    * **Home Assistant**: enter your instance URL (e.g. `http://homeassistant.local:8123`) and a Long-Lived Access Token. Generate one in Home Assistant under your profile page → **Security** → **Long-Lived Access Tokens** → **Create Token**.
     24   * **Home Assistant**: enter your instance URL (e.g. `http://homeassistant.local:8123`) and a Long-Lived Access Token. Generate one in Home Assistant under your profile page → **Security** → **Long-Lived Access Tokens** → **Create Token**. If the URL is a local network address, also turn on **Settings → Allow local network addresses** (the wizard's "Same network as my devices" option does this for you).
    2525   * **Push mode**: no URL/credentials to enter — the wizard generates a channel and a ready-to-paste snippet for your device instead.
    2626   * **Generic REST, advanced mode**: enter the full URL of a JSON endpoint. Leave Authorization empty for public APIs, or enter a full header (e.g. `Authorization: Bearer xxx` or `X-API-Key: xxx`) if the API requires one.
    … …  
    6666Generic REST (entity is a JSONPath into the JSON response): `[iotdw_widget connection="Weather API" entity="current_weather.temperature"]`
    6767
    68 Entities with a non-numeric state (a relay's on/off, a presence sensor's home/away...) show their real value ("ON", "true") in Card and Status list styles.
     68Entities with a non-numeric state (a relay's on/off, a presence sensor's home/away...) show their real state ("ON", "home") in Card and Status list styles; a raw true/false from a JSON API shows as "On"/"Off".
    6969
    7070= I don't want to use the setup wizard — can I skip it? =
    … …  
    9393
    9494Push mode sidesteps this entirely: the plugin never stores any credentials to your devices, because your device sends data to the plugin instead of the other way around.
     95
     96= My device is on the same network as WordPress, but the connection is refused =
     97
     98For security, the plugin doesn't connect to local network addresses (like 192.168.x.x or homeassistant.local) unless you allow it. If WordPress runs on the same network as your devices — a Raspberry Pi, NAS or home server — turn on **IoT Data Widget → Settings → Allow local network addresses** (on a multisite network, only a network administrator can). Choosing "Same network as my devices" in the setup wizard does this for you. If your site is hosted externally, use push mode instead: it can't reach your home network anyway.
    9599
    96100= Does this work with any REST API, or only Home Assistant? =
    … …  
    133137== Changelog ==
    134138
     139= 1.0.2 =
     140* Security: requests to local network addresses (192.168.x.x, 10.x.x.x, homeassistant.local…) are now blocked unless "Allow local network addresses" is turned on under Settings; cloud metadata and link-local addresses are always blocked, including via redirects. If this site already connects to a device on your local network, the setting is turned on automatically during the update — nothing stops working. Choosing "Same network as my devices" in the setup wizard turns it on too.
     141* Added: widgets placed in classic theme widget areas (sidebars, footers) now load their styles and refresh live — previously they appeared unstyled and never updated.
     142* Fixed: widgets line up with the content column in classic themes such as Twenty Twenty-One. Widgets placed one after another now stack vertically — use a Row or Columns block to place them side by side.
     143* Fixed: "Test connection" (and alert e-mails in Pro) show "On"/"Off" instead of "true"/"false" for on/off devices such as Shelly relays.
     144* Fixed: the free version no longer schedules or lists Pro-only background tasks (MQTT polling, email reports, value history); updating removes them.
     145* Fixed: the setup wizard labels Shelly Cloud and Supla as Pro, and shows an accurate message if the demo connection can't be saved.
     146
    135147= 1.0.1 =
    136148* Added: "Upgrade" link in the plugin menu and the plugin list, and an upgrade link next to Pro-only styles (visible to administrators only).
  • iot-data-widget/trunk/admin/class-diagnostics-page.php

    r3728389 r3732074  
    131131                $cron_jobs         = $this->cron_jobs_info();
    132132                $connections       = $this->connections_info();
    133                 $alert_rules       = $this->alert_manager->get_rules();
     133                // Null = sekcja ukryta: alerty są Pro (klasa fizycznie tylko w paczce Pro).
     134                $alert_rules       = class_exists( 'IOTDW_Alert_Evaluator' ) ? $this->alert_manager->get_rules() : null;
    134135                $connections_by_id = array_column( $this->connector_manager->get_connections(), null, 'id' );
    135136                $recent_errors     = IOTDW_Diagnostics_Logger::get_recent_errors();
    … …  
    152153         * @param array $cron_jobs         Z cron_jobs_info().
    153154         * @param array $connections       Z connections_info().
    154          * @param array $alert_rules       Z IOTDW_Alert_Manager::get_rules().
     155         * @param array|null $alert_rules  Z IOTDW_Alert_Manager::get_rules(); null = sekcja ukryta (wersja darmowa).
    155156         * @param array $connections_by_id Połączenia indeksowane po ID.
    156157         * @param array $recent_errors     Z IOTDW_Diagnostics_Logger::get_recent_errors().
    … …  
    158159         * @return string
    159160         */
    160         private function build_report_text( array $environment, array $cron_jobs, array $connections, array $alert_rules, array $connections_by_id, array $recent_errors, array $data_sizes ): string {
     161        private function build_report_text( array $environment, array $cron_jobs, array $connections, ?array $alert_rules, array $connections_by_id, array $recent_errors, array $data_sizes ): string {
    161162                // Code review v1.8: wcześniej wszystkie stringi w tej metodzie były
    162163                // gołym angielskim tekstem, niezgodnie z CLAUDE.md ("i18n od
    … …  
    264265                $lines[] = '';
    265266
    266                 $lines[] = __( '== Alert rules ==', 'iot-data-widget' );
    267                 if ( empty( $alert_rules ) ) {
    268                         $lines[] = __( 'None configured.', 'iot-data-widget' );
    269                 } else {
     267                if ( null !== $alert_rules ) {
     268                        $lines[] = __( '== Alert rules ==', 'iot-data-widget' );
     269                        if ( empty( $alert_rules ) ) {
     270                                $lines[] = __( 'None configured.', 'iot-data-widget' );
     271                        }
    270272                        foreach ( $alert_rules as $rule ) {
    271273                                $rule_connection = $connections_by_id[ $rule['connection_id'] ] ?? null;
    … …  
    285287                                );
    286288                        }
    287                 }
    288                 $lines[] = '';
     289                        $lines[] = '';
     290                }
    289291
    290292                $lines[] = __( '== Recent errors ==', 'iot-data-widget' );
    … …  
    328330                global $wpdb;
    329331
    330                 $tables = array(
    331                         __( 'Recorded value history', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_History_Recorder::TABLE,
    332                         __( 'Widget usage tracking', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_Widget_Usage_Tracker::TABLE,
    333                         __( 'Alert rules', 'iot-data-widget' ) => $wpdb->prefix . IOTDW_Alert_Manager::TABLE,
    334                         __( 'MQTT cache', 'iot-data-widget' )  => $wpdb->prefix . IOTDW_Cron_Value_Cache::CACHE_TABLE,
    335                         __( 'Error log', 'iot-data-widget' )   => $wpdb->prefix . IOTDW_Diagnostics_Logger::TABLE_ERROR_LOG,
    336                 );
     332                // Tabele funkcji Pro tylko wtedy, gdy klasa Pro istnieje w paczce.
     333                $tables = array();
     334
     335                if ( class_exists( 'IOTDW_Renderer_Chart' ) ) {
     336                        $tables[ __( 'Recorded value history', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_History_Recorder::TABLE;
     337                }
     338
     339                $tables[ __( 'Widget usage tracking', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Widget_Usage_Tracker::TABLE;
     340
     341                if ( class_exists( 'IOTDW_Alert_Evaluator' ) ) {
     342                        $tables[ __( 'Alert rules', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Alert_Manager::TABLE;
     343                }
     344
     345                if ( class_exists( 'IOTDW_MQTT_Poll_Engine' ) ) {
     346                        $tables[ __( 'MQTT cache', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Cron_Value_Cache::CACHE_TABLE;
     347                }
     348
     349                $tables[ __( 'Error log', 'iot-data-widget' ) ] = $wpdb->prefix . IOTDW_Diagnostics_Logger::TABLE_ERROR_LOG;
    337350
    338351                $rows = array();
    … …  
    478491         */
    479492        private function cron_jobs_info(): array {
     493                // 'requires_class' — crony funkcji Pro tylko wtedy, gdy klasa Pro
     494                // fizycznie istnieje w paczce (wersja darmowa ich nie ma, więc wiersz
     495                // "Poll MQTT brokers" itp. był tam mylący).
    480496                $hooks = array(
    481                         IOTDW_History_Recorder::SNAPSHOT_HOOK => __( 'Record value history', 'iot-data-widget' ),
    482                         IOTDW_History_Recorder::CLEANUP_HOOK  => __( 'Clean up old history & error log', 'iot-data-widget' ),
    483                         IOTDW_Cron_Value_Cache::POLL_HOOK     => __( 'Poll MQTT brokers', 'iot-data-widget' ),
    484                         // Dopisane (code review) — brakowało tu odkąd IOTDW_Email_Report_Sender
    485                         // powstał (v1.3 Tydzień 7); ten cron logował się już wtedy do
    486                         // IOTDW_Diagnostics_Logger, po prostu nikt nie dodał go na tej liście.
    487                         IOTDW_Email_Report_Sender::SEND_HOOK  => __( 'Send due email reports', 'iot-data-widget' ),
     497                        IOTDW_History_Recorder::SNAPSHOT_HOOK => array( __( 'Record value history', 'iot-data-widget' ), 'IOTDW_Renderer_Chart' ),
     498                        IOTDW_History_Recorder::CLEANUP_HOOK  => array( __( 'Clean up old history & error log', 'iot-data-widget' ), '' ),
     499                        IOTDW_Cron_Value_Cache::POLL_HOOK     => array( __( 'Poll MQTT brokers', 'iot-data-widget' ), 'IOTDW_MQTT_Poll_Engine' ),
     500                        IOTDW_Email_Report_Sender::SEND_HOOK  => array( __( 'Send due email reports', 'iot-data-widget' ), 'IOTDW_Email_Report_Dispatcher' ),
    488501                );
    489502
    490503                $jobs = array();
    491504
    492                 foreach ( $hooks as $hook => $label ) {
     505                foreach ( $hooks as $hook => list( $label, $requires_class ) ) {
     506                        if ( '' !== $requires_class && ! class_exists( $requires_class ) ) {
     507                                continue;
     508                        }
     509
    493510                        $jobs[] = array(
    494511                                'hook'         => $hook,
  • iot-data-widget/trunk/admin/class-settings-page.php

    r3728389 r3732074  
    171171                $remove_data_on_uninstall = (bool) ( $settings['remove_data_on_uninstall'] ?? false );
    172172                $stale_threshold_minutes  = (int) ( $settings[ IOTDW_Renderer::STALE_THRESHOLD_SETTING_KEY ] ?? IOTDW_Renderer::DEFAULT_STALE_MINUTES );
     173                $allow_local_network      = ! empty( $settings[ IOTDW_Http_Client::SETTING_KEY ] );
     174                $can_manage_local_network = IOTDW_Http_Client::can_manage_local_network();
    173175                $import_result            = get_transient( $this->import_result_transient_key() );
    174176
    … …  
    219221                $settings['show_branding']            = ! empty( $_POST['show_branding'] );
    220222                $settings['remove_data_on_uninstall'] = ! empty( $_POST['remove_data_on_uninstall'] );
     223
     224                // Na multisite tylko manage_network_options (patrz IOTDW_Http_Client::can_manage_local_network()).
     225                if ( IOTDW_Http_Client::can_manage_local_network() ) {
     226                        $settings[ IOTDW_Http_Client::SETTING_KEY ] = ! empty( $_POST['allow_local_network'] );
     227                }
    221228
    222229                if ( isset( $_POST['stale_threshold_minutes'] ) ) {
  • iot-data-widget/trunk/admin/class-setup-wizard.php

    r3730704 r3732074  
    898898         *
    899899         * @param string               $push_type  Wybrana platforma.
    900          * @param array<string,string> $connection Zapisany kanał (połączenie 'push').
     900         * @param array<string,string> $iotdw_connection Zapisany kanał (połączenie 'push').
    901901         */
    902902        private function render_push_snippet_step( string $push_type, array $iotdw_connection ): void {
    … …  
    975975                        // jednego, rzadkiego przypadku brzegowego.
    976976                        wp_safe_redirect(
    977                                 add_query_arg( 'iotdw_notice', 'connection_limit', admin_url( 'admin.php?page=iot-data-widget' ) )
     977                                add_query_arg( 'iotdw_notice', 'demo_failed', admin_url( 'admin.php?page=iot-data-widget' ) )
    978978                        );
    979979                        exit;
    … …  
    10511051         * przed obiema tymi ścieżkami.
    10521052         *
    1053          * @param string $type_key      Wybrany klucz typu (już zweryfikowany przez find_type() w render_page()).
     1053         * @param string $iotdw_type_key      Wybrany klucz typu (już zweryfikowany przez find_type() w render_page()).
    10541054         * @param string $connection_id ID istniejącego połączenia (powrót z Kroku 3), albo pusty string dla nowego.
    10551055         * @param string $mode          'pull'/'push'/'' — do linku "Back" (zgłoszenie 2026-09-07: bez tego "Back" gubił mode/group i wracał na Krok 0 zamiast na Krok 1).
    … …  
    10981098         * pojedynczym entity=..., bez zapisu, bez zmian w tym przepływie.
    10991099         *
    1100          * @param string                $type_key   Wybrany klucz typu (do linku "Back").
    1101          * @param array<string, string> $connection Zapisane połączenie (Krok 2).
     1100         * @param string                $iotdw_type_key   Wybrany klucz typu (do linku "Back").
     1101         * @param array<string, string> $iotdw_connection Zapisane połączenie (Krok 2).
    11021102         */
    11031103        private function render_step_3( string $iotdw_type_key, array $iotdw_connection ): void {
    … …  
    12351235         * krok 3), nie trzeba tego dublować w kreatorze.
    12361236         *
    1237          * @param string                $type_key    Wybrany klucz typu (do linku "Back").
    1238          * @param array<string, string> $connection  Zapisane połączenie.
     1237         * @param string                $iotdw_type_key    Wybrany klucz typu (do linku "Back").
     1238         * @param array<string, string> $iotdw_connection  Zapisane połączenie.
    12391239         * @param string[]              $entity_ids  Encje zaznaczone w Kroku 3 — jedna dla konektorów nie-Listable
    12401240         *                                            (bez zmian), kilka dla Listable (kreator multi-entity, v1.9 Tydzień 5).
    … …  
    13251325         *
    13261326         * @param string                $type_key   Wybrany klucz typu.
    1327          * @param array<string, string> $connection Zapisane połączenie — użyte przez wymagany niżej widok (step-5-done.php), nie bezpośrednio w tej metodzie.
     1327         * @param array<string, string> $iotdw_connection Zapisane połączenie — użyte przez wymagany niżej widok (step-5-done.php), nie bezpośrednio w tej metodzie.
    13281328         * @param string[]              $entity_ids Encje zaznaczone w Kroku 3 (jedna albo kilka, v1.9) — jak wyżej, użyte przez widok.
    13291329         */
  • iot-data-widget/trunk/admin/views/diagnostics.php

    r3728389 r3732074  
    1010 * @var array<int, array{hook: string, label: string, is_scheduled: bool, next_run: int|false, last_run: array{ran_at: string, success: bool, message: string}|null}> $cron_jobs Zaplanowane zadania.
    1111 * @var array<int, array{name: string, connector_label: string, last_checked_at: string|null, last_status: string|null, consecutive_errors: int, last_error_message: string|null, is_push: bool, push_active: bool|null, last_received_at: string|null}> $connections Skonfigurowane połączenia + ich status.
    12  * @var array<int, array<string, mixed>>     $alert_rules       Zapisane reguły alertów (IOTDW_Alert_Manager::get_rules()).
     12 * @var array<int, array<string, mixed>>|null $alert_rules       Zapisane reguły alertów (IOTDW_Alert_Manager::get_rules()); null = wersja darmowa, sekcja ukryta.
    1313 * @var array<string, array<string, string>> $connections_by_id Połączenia indeksowane po ID (do nazw w tabeli reguł alertów).
    1414 * @var array<int, array{id: int, occurred_at: string, source: string, message: string}> $recent_errors Najnowsze wpisy logu błędów.
    … …  
    233233        ?>
    234234
     235        <?php if ( null !== $alert_rules ) : // Alerty są Pro — na wersji darmowej sekcji nie ma. ?>
    235236        <h2><?php esc_html_e( 'Alert rules', 'iot-data-widget' ); ?></h2>
    236237        <?php
    … …  
    273274        require __DIR__ . '/partials/data-table.php';
    274275        ?>
     276        <?php endif; ?>
    275277
    276278        <h2><?php esc_html_e( 'Recent errors', 'iot-data-widget' ); ?></h2>
  • iot-data-widget/trunk/admin/views/general-settings-page.php

    r3728389 r3732074  
    1616 * @var string                             $export_default_to     Domyślna data końcowa formularza eksportu CSV (v1.3 Tydzień 6).
    1717 * @var int                                $stale_threshold_minutes Próg "stare dane" w minutach (v2.0, docs/mvp-v2-0-push-mode-plan.md §5).
     18 * @var bool                               $allow_local_network   "Allow local network addresses" (1.0.2, IOTDW_Http_Client).
     19 * @var bool                               $can_manage_local_network Czy bieżący użytkownik może to zmienić (multisite: manage_network_options).
    1820 */
    1921
    … …  
    119121                        </tr>
    120122                        <tr>
     123                                <th><?php esc_html_e( 'Local network', 'iot-data-widget' ); ?></th>
     124                                <td>
     125                                        <label>
     126                                                <input type="checkbox" name="allow_local_network" value="1" <?php checked( $allow_local_network ); ?> <?php disabled( ! $can_manage_local_network ); ?> />
     127                                                <?php esc_html_e( 'Allow local network addresses', 'iot-data-widget' ); ?>
     128                                        </label>
     129                                        <p class="description">
     130                                                <?php esc_html_e( 'Turn this on only if WordPress runs on the same network as your devices (e.g. a home server) and connects to addresses like 192.168.x.x or homeassistant.local. Off by default: it stops a misconfigured or malicious URL from reaching other services on your server\'s network. Cloud metadata and link-local addresses are always blocked.', 'iot-data-widget' ); ?>
     131                                        </p>
     132                                        <?php if ( ! $can_manage_local_network ) : ?>
     133                                                <p class="description"><?php esc_html_e( 'On a multisite network, only a network administrator can change this.', 'iot-data-widget' ); ?></p>
     134                                        <?php endif; ?>
     135                                </td>
     136                        </tr>
     137                        <tr>
    121138                                <th><?php esc_html_e( 'Uninstall', 'iot-data-widget' ); ?></th>
    122139                                <td>
  • iot-data-widget/trunk/admin/views/settings-page.php

    r3728389 r3732074  
    3232                $iotdw_current_notice = sanitize_key( wp_unslash( $_GET['iotdw_notice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    3333
    34                 if ( 'connection_limit' === $iotdw_current_notice ) {
     34                if ( 'demo_failed' === $iotdw_current_notice ) {
     35                        // Dawniej "The free version supports one connection" — pozostałość
     36                        // po zniesionym limicie; zapis połączenia demo po prostu się nie udał.
    3537                        $iotdw_notice_type    = 'error';
    36                         $iotdw_notice_message = __( 'The free version supports one connection. Upgrade to Pro for unlimited connections.', 'iot-data-widget' );
     38                        $iotdw_notice_message = __( 'The demo connection could not be saved. Please try again, or add a connection manually below.', 'iot-data-widget' );
    3739                } else {
    3840                        $iotdw_notice_type    = 'success';
  • iot-data-widget/trunk/admin/views/wizard/step-0-location.php

    r3728389 r3732074  
    2828                        <?php esc_html_e( 'On external/shared hosting (typical) — my device sends data to my site', 'iot-data-widget' ); ?>
    2929                </a>
    30                 <a class="button button-hero" href="<?php echo esc_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ); ?>">
     30                <?php // 1.0.2: ta opcja włącza też "Allow local network addresses" (IOTDW_Local_Network::handle_enable()). ?>
     31                <a class="button button-hero" href="<?php echo esc_url( IOTDW_Local_Network::enable_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ) ); ?>">
    3132                        <?php esc_html_e( 'Same network as my devices (Raspberry Pi, NAS, home server) — my site fetches data from the device', 'iot-data-widget' ); ?>
    3233                </a>
    … …  
    4546                ?>
    4647                <a class="button button-hero" href="<?php echo esc_url( add_query_arg( 'mode', 'pull', $iotdw_base_url ) ); ?>">
    47                         <?php esc_html_e( 'On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there', 'iot-data-widget' ); ?>
     48                        <?php
     49                        // Shelly Cloud i preset Supla są Pro — na wersji darmowej oznaczone.
     50                        if ( class_exists( 'IOTDW_Connector_Presets' ) ) {
     51                                esc_html_e( 'On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there', 'iot-data-widget' );
     52                        } else {
     53                                esc_html_e( 'On external/shared hosting, but my device has its own cloud service (a publicly-reachable Home Assistant; Shelly Cloud and Supla in Pro) — my site fetches data from there', 'iot-data-widget' );
     54                        }
     55                        ?>
    4856                </a>
    4957        </p>
  • iot-data-widget/trunk/includes/class-history-recorder.php

    r3728389 r3732074  
    219219                // phpcs:enable
    220220
     221                $keys = array();
     222
    221223                foreach ( $pairs as $pair ) {
    222                         $this->record_snapshot( $pair->connection_id, $pair->entity_id );
     224                        $keys[ $pair->connection_id . '|' . $pair->entity_id ] = array( $pair->connection_id, $pair->entity_id );
     225                }
     226
     227                // Wykresy w obszarach widgetów motywu (1.0.2) — nie ma ich w tabeli.
     228                foreach ( ( new IOTDW_Widget_Usage_Tracker( $this->connector_manager ) )->widget_area_usage() as $pair ) {
     229                        if ( in_array( $pair['style'], array( 'chart', 'sparkline' ), true ) ) {
     230                                $keys[ $pair['connection_id'] . '|' . $pair['entity_id'] ] = array( $pair['connection_id'], $pair['entity_id'] );
     231                        }
     232                }
     233
     234                foreach ( $keys as list( $connection_id, $entity_id ) ) {
     235                        $this->record_snapshot( $connection_id, $entity_id );
    223236                }
    224237        }
  • iot-data-widget/trunk/includes/class-plugin.php

    r3728389 r3732074  
    266266                $this->init_modules();
    267267                $this->maybe_migrate_credentials();
     268                // Osobno od maybe_upgrade_db() (ta nie czyta połączeń) — jednorazowo,
     269                // dopóki ustawienie "Allow local network addresses" nie ma wartości.
     270                IOTDW_Local_Network::maybe_migrate( $this->connector_manager );
    268271
    269272                add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_admin_toolkit_css' ) );
    … …  
    327330                IOTDW_Push_Channel_Manager::create_table();
    328331
     332                // 1.0.2: aktualizacja z wersji darmowej <= 1.0.1 zdejmuje z
     333                // harmonogramu crony funkcji Pro (patrz sync_cron_events()).
     334                self::sync_cron_events();
     335
    329336                update_option( self::DB_VERSION_OPTION, IOTDW_VERSION );
     337        }
     338
     339        /**
     340         * Planuje crony wtyczki — te funkcji Pro tylko wtedy, gdy plik Pro
     341         * fizycznie istnieje w paczce (ten sam test file_exists() co
     342         * init_modules()), w przeciwnym razie zdejmuje je z harmonogramu.
     343         * Wersja darmowa <= 1.0.1 planowała wszystkie cztery (zgłoszenie
     344         * 2026-10-06): MQTT/raporty nic nie robiły, a snapshot historii odpytywał
     345         * urządzenia dla wykresów, których wersja darmowa nie wyświetla.
     346         * Wołane przy aktywacji i przy każdej zmianie wersji.
     347         */
     348        public static function sync_cron_events(): void {
     349                $premium = IOTDW_PLUGIN_DIR . 'includes/premium/';
     350
     351                // Niestandardowe harmonogramy muszą być znane, zanim wp_schedule_event() ich użyje.
     352                add_filter( 'cron_schedules', array( 'IOTDW_History_Recorder', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
     353                add_filter( 'cron_schedules', array( 'IOTDW_Cron_Value_Cache', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
     354
     355                $events = array(
     356                        // Historia zasila wykresy/sparkline i alerty — obie funkcje Pro.
     357                        IOTDW_History_Recorder::SNAPSHOT_HOOK => array( IOTDW_History_Recorder::CRON_SCHEDULE, file_exists( $premium . 'class-renderer-chart.php' ) || file_exists( $premium . 'class-alert-evaluator.php' ) ),
     358                        // Czyści też dziennik błędów — potrzebny na obu wersjach.
     359                        IOTDW_History_Recorder::CLEANUP_HOOK  => array( 'daily', true ),
     360                        IOTDW_Cron_Value_Cache::POLL_HOOK     => array( IOTDW_Cron_Value_Cache::CRON_SCHEDULE, file_exists( $premium . 'class-mqtt-poll-engine.php' ) ),
     361                        IOTDW_Email_Report_Sender::SEND_HOOK  => array( 'hourly', file_exists( $premium . 'class-email-report-dispatcher.php' ) ),
     362                );
     363
     364                foreach ( $events as $hook => list( $recurrence, $wanted ) ) {
     365                        if ( ! $wanted ) {
     366                                wp_clear_scheduled_hook( $hook );
     367                        } elseif ( ! wp_next_scheduled( $hook ) ) {
     368                                wp_schedule_event( time(), $recurrence, $hook );
     369                        }
     370                }
    330371        }
    331372
    … …  
    373414                }
    374415
     416                require_once IOTDW_PLUGIN_DIR . 'includes/class-http-client.php';
     417                require_once IOTDW_PLUGIN_DIR . 'includes/class-local-network.php';
    375418                require_once IOTDW_PLUGIN_DIR . 'includes/class-value-normalizer.php';
    376419                require_once IOTDW_PLUGIN_DIR . 'includes/class-json-path.php';
    … …  
    557600                $this->mqtt_poller   = new IOTDW_Cron_Value_Cache( $this->connector_manager );
    558601                $this->settings_page = new IOTDW_Settings_Page( $this->connector_manager, $this->widget_usage_tracker );
     602                // 1.0.2 — "Allow local network addresses": powiadomienie + kreator (same hooki).
     603                new IOTDW_Local_Network();
    559604                // alert_manager (skeleton: CRUD + schemat) zostaje zawsze —
    560605                // IOTDW_Diagnostics_Page je wstrzykuje (raport "Alert rules" działa
    … …  
    623668                update_option( self::DB_VERSION_OPTION, IOTDW_VERSION );
    624669
    625                 // Rejestrujemy niestandardowe harmonogramy od razu — inaczej
    626                 // wp_schedule_event() poniżej nie znalazłby ich w wp_get_schedules().
    627                 add_filter( 'cron_schedules', array( 'IOTDW_History_Recorder', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
    628                 add_filter( 'cron_schedules', array( 'IOTDW_Cron_Value_Cache', 'register_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
    629 
    630                 if ( ! wp_next_scheduled( IOTDW_History_Recorder::SNAPSHOT_HOOK ) ) {
    631                         wp_schedule_event( time(), IOTDW_History_Recorder::CRON_SCHEDULE, IOTDW_History_Recorder::SNAPSHOT_HOOK );
    632                 }
    633 
    634                 if ( ! wp_next_scheduled( IOTDW_History_Recorder::CLEANUP_HOOK ) ) {
    635                         wp_schedule_event( time(), 'daily', IOTDW_History_Recorder::CLEANUP_HOOK );
    636                 }
    637 
    638                 if ( ! wp_next_scheduled( IOTDW_Cron_Value_Cache::POLL_HOOK ) ) {
    639                         wp_schedule_event( time(), IOTDW_Cron_Value_Cache::CRON_SCHEDULE, IOTDW_Cron_Value_Cache::POLL_HOOK );
    640                 }
    641 
    642                 // Co godzinę — częstotliwość dzienna/tygodniowa nie wymaga precyzji
    643                 // większej niż to (patrz IOTDW_Email_Report_Sender::is_due()), a
    644                 // wbudowany harmonogram 'hourly' istnieje zawsze, bez rejestrowania
    645                 // własnego (w przeciwieństwie do History Recordera/MQTT Pollera).
    646                 if ( ! wp_next_scheduled( IOTDW_Email_Report_Sender::SEND_HOOK ) ) {
    647                         wp_schedule_event( time(), 'hourly', IOTDW_Email_Report_Sender::SEND_HOOK );
    648                 }
     670                self::sync_cron_events();
    649671
    650672                // Sygnał "pokaż kreator" dla IOTDW_Setup_Wizard::maybe_redirect_after_activation()
  • iot-data-widget/trunk/includes/class-rest-api.php

    r3728389 r3732074  
    618618                }
    619619
     620                // Wynik pokazywany wprost adminowi ("✓ …") — "false" z surowego bool
     621                // JSON jako "Off", jak na karcie/liście.
     622                if ( ! empty( $result['display_value'] ) ) {
     623                        $result['display_value'] = IOTDW_Value_Normalizer::human_display( (string) $result['display_value'] );
     624                }
     625
    620626                return rest_ensure_response(
    621627                        array(
  • iot-data-widget/trunk/includes/class-shortcode.php

    r3730704 r3732074  
    9191         */
    9292        public function render( $atts ): string {
     93                $html = $this->render_widget( $atts );
     94
     95                // Blokowy wrapper (1.0.2): motywy wyrównują swoje bloki do kolumny
     96                // treści przez margin: auto, co nie działa na elementach inline-block
     97                // (card/gauge stały przy krawędzi strony, np. w Twenty Twenty-One).
     98                // Wrapper dostaje szerokość kolumny, widget stoi w nim do lewej.
     99                // Pusty wynik (błąd ukryty przed gościem) — bez wrappera, żeby nie
     100                // zostawiał pustego odstępu.
     101                return '' === trim( $html ) ? $html : '<div class="iot-dw-widget-wrap">' . $html . '</div>';
     102        }
     103
     104        /**
     105         * Właściwe renderowanie widgetu (bez wrappera, patrz render()).
     106         *
     107         * @param array|string $atts Atrybuty shortcode.
     108         * @return string
     109         */
     110        private function render_widget( $atts ): string {
    93111                $this->rendered_widget = true;
    94112
    … …  
    119137                );
    120138
     139                $this->enqueue_late( (string) $atts['style'] );
     140
    121141                if ( 'list' === $atts['style'] ) {
    122142                        return $this->render_list( $atts );
    … …  
    256276         *
    257277         * @param IOTDW_Abstract_Connector $connector     Instancja konektora dla tego połączenia.
    258          * @param array<string, mixed>      $connection_row Zapisane połączenie (musi zawierać 'id'/'connector', opcjonalnie 'preset') — do wyliczenia $min_ttl, patrz enforce_min_ttl().
    259          * @param string                    $entity_id     ID/ścieżka encji.
    260          * @param int                       $ttl           TTL cache dla udanej odpowiedzi (sekundy), zażądany przez shortcode/blok.
     278         * @param array<string, mixed>     $connection_row Zapisane połączenie (musi zawierać 'id'/'connector', opcjonalnie 'preset') — do wyliczenia $min_ttl, patrz enforce_min_ttl().
     279         * @param string                   $entity_id     ID/ścieżka encji.
     280         * @param int                      $ttl           TTL cache dla udanej odpowiedzi (sekundy), zażądany przez shortcode/blok.
    261281         * @return array Znormalizowana struktura danych konektora, ze stemplowanymi connection_id/entity_id.
    262282         */
    … …  
    578598                }
    579599
    580                 // v1.8 Tydzień 6 — tokeny (typografia/odstępy/cienie z Tygodnia 5)
    581                 // muszą się załadować przed widget-styles.css, które teraz z nich
    582                 // korzysta (var(--iot-dw-text-xl) itd.) — stąd jako zależność, nie
    583                 // tylko osobny enqueue obok.
     600                $this->enqueue_styles();
     601
     602                // class_exists() (Guideline 5, 2026-09-24) — chart-widget.js/css są
     603                // plikami premium-only, w paczce darmowej fizycznie nie istnieją.
     604                if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'chart' ) ) ) {
     605                        $this->enqueue_chart_assets();
     606                }
     607
     608                // v1.8 Tydzień 3 — hybrydowe doładowanie. "chart" celowo poza listą
     609                // stylów (patrz komentarz nagłówkowy public/js/widget-refresh.js).
     610                if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'card', 'gauge', 'sparkline', 'list', 'grid' ) ) ) {
     611                        $this->enqueue_refresh_script();
     612                }
     613        }
     614
     615        /**
     616         * Późne ładowanie zasobów dla widgetu wyrenderowanego POZA treścią
     617         * pojedynczego posta (1.0.2: obszar widgetów motywu klasycznego, lista
     618         * wpisów, szablon) — maybe_enqueue_assets() widzi tylko post_content
     619         * strony pojedynczej. WordPress drukuje style/skrypty dodane po wp_head
     620         * w stopce. Tylko front-end — podglądy w adminie/REST mają własne.
     621         *
     622         * @param string $style Styl widgetu.
     623         */
     624        private function enqueue_late( string $style ): void {
     625                if ( is_admin() || ! did_action( 'wp_enqueue_scripts' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
     626                        return;
     627                }
     628
     629                $this->enqueue_styles();
     630
     631                if ( 'chart' === $style ) {
     632                        $this->enqueue_chart_assets();
     633                } else {
     634                        $this->enqueue_refresh_script();
     635                }
     636        }
     637
     638        /**
     639         * CSS widgetu + zmienne wyglądu (raz na żądanie).
     640         */
     641        private function enqueue_styles(): void {
     642                if ( wp_style_is( 'iot-data-widget-theme', 'enqueued' ) ) {
     643                        return;
     644                }
     645
     646                // v1.8 Tydzień 6 — tokeny muszą się załadować przed widget-styles.css.
    584647                wp_enqueue_style(
    585648                        'iot-data-widget-tokens',
    … …  
    607670                        IOTDW_Style_Settings::build_css_rule( IOTDW_Style_Settings::css_variables( IOTDW_Style_Settings::get_settings() ) )
    608671                );
    609 
    610                 // class_exists() (Guideline 5, 2026-09-24; dawniej dodatkowo
    611                 // iotdw_is_premium(), usunięte jako runtime license-check) — bez
    612                 // tego warunku strona z style="chart" na wersji darmowej ładowała
    613                 // Chart.js + chart-widget.js/css na próżno (renderer i tak pokazuje
    614                 // komunikat "wymaga Pro", nigdy nie odwołuje się do tych plików) —
    615                 // zbędny ciężar, a chart-widget.js/css są plikami premium-only
    616                 // (patrz @fs_premium_only w iot-data-widget.php), więc w paczce
    617                 // darmowej fizycznie nie istnieją.
    618                 if ( class_exists( 'IOTDW_Renderer_Chart' ) && IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'chart' ) ) ) {
    619                         wp_enqueue_script(
    620                                 'chart-js',
    621                                 IOTDW_PLUGIN_URL . 'public/js/vendor/chart.umd.min.js',
    622                                 array(),
    623                                 '4.5.1',
    624                                 true
    625                         );
    626 
    627                         wp_enqueue_script(
    628                                 'iot-data-widget-chart',
    629                                 IOTDW_PLUGIN_URL . 'public/js/chart-widget.js',
    630                                 array( 'chart-js' ),
    631                                 IOTDW_VERSION,
    632                                 true
    633                         );
    634 
    635                         wp_enqueue_style(
    636                                 'iot-data-widget-chart',
    637                                 IOTDW_PLUGIN_URL . 'public/css/chart-widget.css',
    638                                 array( 'iot-data-widget' ),
    639                                 IOTDW_VERSION
    640                         );
    641                 }
    642 
    643                 // v1.8 Tydzień 3 — hybrydowe doładowanie. "chart" celowo poza listą
    644                 // stylów (patrz komentarz nagłówkowy public/js/widget-refresh.js) —
    645                 // strona z samym wykresem nie ładuje tego skryptu w ogóle.
    646                 if ( IOTDW_Widget_Usage_Tracker::content_has_style( $post->post_content, array( 'card', 'gauge', 'sparkline', 'list', 'grid' ) ) ) {
    647                         wp_enqueue_script(
    648                                 'iot-data-widget-widget-refresh',
    649                                 IOTDW_PLUGIN_URL . 'public/js/widget-refresh.js',
    650                                 array(),
    651                                 IOTDW_VERSION,
    652                                 true
    653                         );
    654 
    655                         wp_localize_script(
    656                                 'iot-data-widget-widget-refresh',
    657                                 'iotdwWidgetRefresh',
    658                                 array( 'restUrl' => esc_url_raw( rest_url( IOTDW_Rest_Api::NAMESPACE ) ) )
    659                         );
    660                 }
     672        }
     673
     674        /**
     675         * Chart.js + chart-widget.js/css (tylko Pro — pliki istnieją tylko tam).
     676         */
     677        private function enqueue_chart_assets(): void {
     678                if ( ! class_exists( 'IOTDW_Renderer_Chart' ) || wp_script_is( 'iot-data-widget-chart', 'enqueued' ) ) {
     679                        return;
     680                }
     681
     682                wp_enqueue_script(
     683                        'chart-js',
     684                        IOTDW_PLUGIN_URL . 'public/js/vendor/chart.umd.min.js',
     685                        array(),
     686                        '4.5.1',
     687                        true
     688                );
     689
     690                wp_enqueue_script(
     691                        'iot-data-widget-chart',
     692                        IOTDW_PLUGIN_URL . 'public/js/chart-widget.js',
     693                        array( 'chart-js' ),
     694                        IOTDW_VERSION,
     695                        true
     696                );
     697
     698                wp_enqueue_style(
     699                        'iot-data-widget-chart',
     700                        IOTDW_PLUGIN_URL . 'public/css/chart-widget.css',
     701                        array( 'iot-data-widget' ),
     702                        IOTDW_VERSION
     703                );
     704        }
     705
     706        /**
     707         * Skrypt jednorazowego odświeżenia wartości po załadowaniu strony.
     708         */
     709        private function enqueue_refresh_script(): void {
     710                if ( wp_script_is( 'iot-data-widget-widget-refresh', 'enqueued' ) ) {
     711                        return;
     712                }
     713
     714                wp_enqueue_script(
     715                        'iot-data-widget-widget-refresh',
     716                        IOTDW_PLUGIN_URL . 'public/js/widget-refresh.js',
     717                        array(),
     718                        IOTDW_VERSION,
     719                        true
     720                );
     721
     722                wp_localize_script(
     723                        'iot-data-widget-widget-refresh',
     724                        'iotdwWidgetRefresh',
     725                        array( 'restUrl' => esc_url_raw( rest_url( IOTDW_Rest_Api::NAMESPACE ) ) )
     726                );
    661727        }
    662728
    … …  
    702768         *
    703769         * @param IOTDW_Abstract_Connector $connector Instancja konektora.
    704          * @param string                    $entity_id ID encji / ścieżka do wartości.
     770         * @param string                   $entity_id ID encji / ścieżka do wartości.
    705771         * @return array
    706772         */
    … …  
    738804                                'entities' => $connector->list_entities(),
    739805                                'error'    => null,
     806                        );
     807                } catch ( \RuntimeException $e ) {
     808                        // Celowe błędy konektora (brak połączenia, 401, adres zablokowany)
     809                        // — komunikat już jest dla użytkownika, bez "Unexpected".
     810                        return array(
     811                                'entities' => array(),
     812                                'error'    => $e->getMessage(),
    740813                        );
    741814                } catch ( \Throwable $e ) {
  • iot-data-widget/trunk/includes/class-value-normalizer.php

    r3728389 r3732074  
    8787                );
    8888        }
     89
     90        /**
     91         * Tekst stanu do pokazania człowiekowi: dosłowne "true"/"false" (surowy
     92         * bool z JSON, np. Shelly switch:0.output) → przetłumaczone "On"/"Off",
     93         * tak jak robi to już IOTDW_Shortcode dla karty/listy. Każdy inny tekst
     94         * (HA "home", Tasmota "ON"...) wraca bez zmian. Nie dla webhooków —
     95         * tam display_value zostaje surowe (kontrakt dla odbiorców maszynowych).
     96         *
     97         * @param string $display_value display_value z konektora.
     98         * @return string
     99         */
     100        public static function human_display( string $display_value ): string {
     101                switch ( strtolower( trim( $display_value ) ) ) {
     102                        case 'true':
     103                                return __( 'On', 'iot-data-widget' );
     104                        case 'false':
     105                                return __( 'Off', 'iot-data-widget' );
     106                        default:
     107                                return $display_value;
     108                }
     109        }
    89110}
  • iot-data-widget/trunk/includes/class-widget-usage-tracker.php

    r3730704 r3732074  
    5151                add_action( 'save_post', array( $this, 'sync_post' ) );
    5252                add_action( 'before_delete_post', array( $this, 'remove_post' ) );
     53
     54                foreach ( array( 'sidebars_widgets', 'widget_block', 'widget_text', 'widget_custom_html', IOTDW_Connector_Manager::CONNECTIONS_OPTION ) as $option ) {
     55                        add_action( 'update_option_' . $option, array( __CLASS__, 'flush_widget_area_usage' ) );
     56                        add_action( 'add_option_' . $option, array( __CLASS__, 'flush_widget_area_usage' ) );
     57                }
     58        }
     59
     60        /**
     61         * Transient z parami użytymi w obszarach widgetów motywu klasycznego.
     62         */
     63        const WIDGET_AREA_TRANSIENT = 'iotdw_widget_area_usage';
     64
     65        /**
     66         * Typy widgetów, w których treści szukamy [iotdw_widget] (id_base => pole treści).
     67         */
     68        const WIDGET_CONTENT_FIELDS = array(
     69                'block'       => 'content',
     70                'text'        => 'text',
     71                'custom_html' => 'content',
     72        );
     73
     74        /**
     75         * Kasuje cache par z obszarów widgetów (zmiana widgetów/pasków/połączeń).
     76         */
     77        public static function flush_widget_area_usage(): void {
     78                delete_transient( self::WIDGET_AREA_TRANSIENT );
     79        }
     80
     81        /**
     82         * Użycie widgetów w AKTYWNYCH obszarach widgetów motywu klasycznego —
     83         * odpowiednik wierszy tabeli użycia dla postów (to samo: connection_id,
     84         * entity_id, style), ale liczony z opcji widgetów zamiast zapisywany,
     85         * żeby nie zmieniać schematu tabeli (post_id jest kluczem jej
     86         * czyszczenia). Widgety nieaktywne (wp_inactive_widgets) się nie
     87         * liczą — nie są nigdzie wyświetlane. Cache w transiencie, kasowany przy
     88         * każdej zmianie widgetów/pasków/połączeń (patrz konstruktor).
     89         *
     90         * Konsumenci: is_publicly_used() (/widget-data), odpytywanie MQTT
     91         * (IOTDW_MQTT_Poll_Engine) i zapis historii (IOTDW_History_Recorder).
     92         *
     93         * @return array<int, array{connection_id: string, entity_id: string, style: string}>
     94         */
     95        public function widget_area_usage(): array {
     96                $cached = get_transient( self::WIDGET_AREA_TRANSIENT );
     97
     98                if ( is_array( $cached ) && ( array() === $cached || isset( $cached[0]['style'] ) ) ) {
     99                        return $cached;
     100                }
     101
     102                $usage = array();
     103                // Opcja wprost — wp_get_sidebars_widgets() poza adminem zapamiętuje
     104                // wynik w globalnej zmiennej na całe żądanie.
     105                $sidebars = get_option( 'sidebars_widgets', array() );
     106                $sidebars = is_array( $sidebars ) ? $sidebars : array();
     107
     108                foreach ( $sidebars as $sidebar_id => $widget_ids ) {
     109                        if ( 'wp_inactive_widgets' === $sidebar_id || 'array_version' === $sidebar_id || ! is_array( $widget_ids ) ) {
     110                                continue;
     111                        }
     112
     113                        foreach ( $widget_ids as $widget_id ) {
     114                                if ( ! preg_match( '/^(.+)-(\d+)$/', (string) $widget_id, $m ) || ! isset( self::WIDGET_CONTENT_FIELDS[ $m[1] ] ) ) {
     115                                        continue;
     116                                }
     117
     118                                $instances = get_option( 'widget_' . $m[1], array() );
     119                                $content   = $instances[ (int) $m[2] ][ self::WIDGET_CONTENT_FIELDS[ $m[1] ] ] ?? '';
     120
     121                                if ( ! is_string( $content ) || '' === $content ) {
     122                                        continue;
     123                                }
     124
     125                                foreach ( $this->extract_tracked_pairs( $content ) as $pair ) {
     126                                        $usage[ $pair['connection_id'] . '|' . $pair['entity_id'] . '|' . $pair['style'] ] = $pair;
     127                                }
     128                        }
     129                }
     130
     131                $usage = array_values( $usage );
     132
     133                set_transient( self::WIDGET_AREA_TRANSIENT, $usage, DAY_IN_SECONDS );
     134
     135                return $usage;
    53136        }
    54137
    … …  
    201284         * kwalifikują się — nie ma dla nich żadnego posta do zweryfikowania.
    202285         *
     286         * 1.0.2: kwalifikuje się też widget w AKTYWNYM obszarze widgetów motywu
     287         * klasycznego (patrz widget_area_usage()) — wyświetlany publicznie tak
     288         * samo jak opublikowany post. Wcześniej taki widget nigdy się nie
     289         * odświeżał (404 z /widget-data).
     290         *
    203291         * @param string $connection_id ID połączenia (kanoniczne, nie nazwa).
    204292         * @param string $entity_id     ID encji.
    … …  
    228316                        // ma, ale żądający je zna (poprawne ciasteczko).
    229317                        if ( null !== $post && ! post_password_required( $post ) ) {
     318                                return true;
     319                        }
     320                }
     321
     322                foreach ( $this->widget_area_usage() as $pair ) {
     323                        if ( $pair['connection_id'] === $connection_id && $pair['entity_id'] === $entity_id ) {
    230324                                return true;
    231325                        }
  • iot-data-widget/trunk/includes/connectors/abstract-connector.php

    r3728389 r3732074  
    132132                }
    133133
     134                if ( null !== IOTDW_Http_Client::check_blocked( $base_url ) ) {
     135                        // Odrzucone przed wysłaniem — komunikat IOTDW_Http_Client już mówi dlaczego.
     136                        return $message;
     137                }
     138
    134139                return $message . ' ' . sprintf(
    135140                        /* translators: %s: the local-network host/IP that was configured */
  • iot-data-widget/trunk/includes/connectors/class-ha-connector.php

    r3728389 r3732074  
    5454         */
    5555        public function fetch_data( string $entity_id ): array {
    56                 $response = wp_remote_get(
     56                $response = IOTDW_Http_Client::get(
    5757                        $this->base_url . '/api/states/' . rawurlencode( $entity_id ),
    5858                        array(
    … …  
    137137                // zwykle znacznie więcej danych niż pojedynczy odczyt encji, więc
    138138                // dłuższy zapas jest tu uzasadniony bez kosztu dla front-endu.
    139                 $response = wp_remote_get(
     139                $response = IOTDW_Http_Client::get(
    140140                        $this->base_url . '/api/states',
    141141                        array(
  • iot-data-widget/trunk/includes/connectors/class-rest-connector.php

    r3728389 r3732074  
    153153                }
    154154
    155                 $response = wp_remote_get(
     155                $response = IOTDW_Http_Client::get(
    156156                        $request_url,
    157157                        array(
  • iot-data-widget/trunk/iot-data-widget.php

    r3730704 r3732074  
    44 * Plugin Name:       IoT Data Widget
    55 * Description:       Displays live data from smart home devices (Home Assistant, REST API, MQTT, Shelly/Tasmota/ESPHome presets, Shelly Cloud) on your website via shortcode and Gutenberg block, with a guided setup wizard, a redesigned and auto-refreshing appearance, and a built-in diagnostics page. Pro adds threshold alerts, email reports, and a public embed mode.
    6  * Version:           1.0.1
     6 * Version:           1.0.2
    77 * Requires at least: 6.0
    88 * Requires PHP:      8.1
    … …  
    175175        // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals
    176176    }
    177     define( 'IOTDW_VERSION', '1.0.1' );
     177    define( 'IOTDW_VERSION', '1.0.2' );
    178178    define( 'IOTDW_PLUGIN_FILE', __FILE__ );
    179179    define( 'IOTDW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • iot-data-widget/trunk/languages/iot-data-widget.pot

    r3730704 r3732074  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: IoT Data Widget 1.0.1\n"
     5"Project-Id-Version: IoT Data Widget 1.0.2\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/iot-data-widget\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-06T10:46:23+00:00\n"
     12"POT-Creation-Date: 2026-10-06T17:55:40+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    8282#: admin/class-email-reports-page.php:151
    8383#: admin/class-email-reports-page.php:230
    84 #: admin/class-settings-page.php:213
    85 #: admin/class-settings-page.php:239
    86 #: admin/class-settings-page.php:261
    87 #: admin/class-settings-page.php:367
    88 #: admin/class-settings-page.php:435
     84#: admin/class-settings-page.php:215
     85#: admin/class-settings-page.php:246
     86#: admin/class-settings-page.php:268
     87#: admin/class-settings-page.php:374
     88#: admin/class-settings-page.php:442
    8989#: admin/class-style-settings-page.php:157
     90#: includes/class-local-network.php:140
     91#: includes/class-local-network.php:173
    9092#: includes/premium/class-history-csv-export.php:59
    9193msgid "Insufficient permissions."
    … …  
    140142#: admin/class-alert-rules-page.php:216
    141143#: admin/class-email-reports-page.php:207
    142 #: admin/class-settings-page.php:416
     144#: admin/class-settings-page.php:423
    143145msgid "Please choose a valid connection."
    144146msgstr ""
    … …  
    146148#: admin/class-alert-rules-page.php:220
    147149#: admin/class-email-reports-page.php:211
    148 #: admin/class-settings-page.php:420
     150#: admin/class-settings-page.php:427
    149151msgid "Entity is required."
    150152msgstr ""
    … …  
    172174msgstr ""
    173175
    174 #: admin/class-diagnostics-page.php:169
     176#: admin/class-diagnostics-page.php:170
    175177msgid "IoT Data Widget — Diagnostic Report"
    176178msgstr ""
    177179
    178180#. translators: %s: current date/time in UTC, e.g. "2026-08-13 12:00:00"
    179 #: admin/class-diagnostics-page.php:171
     181#: admin/class-diagnostics-page.php:172
    180182#, php-format
    181183msgid "Generated: %s UTC"
    182184msgstr ""
    183185
    184 #: admin/class-diagnostics-page.php:174
     186#: admin/class-diagnostics-page.php:175
    185187msgid "== Environment =="
    186188msgstr ""
    187189
    188190#. translators: %s: PHP version, e.g. "8.1.34"
    189 #: admin/class-diagnostics-page.php:176
     191#: admin/class-diagnostics-page.php:177
    190192#, php-format
    191193msgid "PHP version: %s"
    … …  
    193195
    194196#. translators: %s: WordPress version, e.g. "7.0.3"
    195 #: admin/class-diagnostics-page.php:178
     197#: admin/class-diagnostics-page.php:179
    196198#, php-format
    197199msgid "WordPress version: %s"
    … …  
    199201
    200202#. translators: %s: active theme name and version, e.g. "Twenty Twenty-Five 1.4"
    201 #: admin/class-diagnostics-page.php:180
     203#: admin/class-diagnostics-page.php:181
    202204#, php-format
    203205msgid "Active theme: %s"
    … …  
    205207
    206208#. translators: %s: PHP memory limit, e.g. "256M"
    207 #: admin/class-diagnostics-page.php:182
     209#: admin/class-diagnostics-page.php:183
    208210#, php-format
    209211msgid "Memory limit: %s"
    … …  
    211213
    212214#. translators: %s: PHP max execution time in seconds, e.g. "30"
    213 #: admin/class-diagnostics-page.php:184
     215#: admin/class-diagnostics-page.php:185
    214216#, php-format
    215217msgid "Max execution time: %ss"
    … …  
    217219
    218220#. translators: %s: comma-separated list of other active plugin names, or "None"
    219 #: admin/class-diagnostics-page.php:187
     221#: admin/class-diagnostics-page.php:188
    220222#, php-format
    221223msgid "Other active plugins: %s"
    222224msgstr ""
    223225
    224 #: admin/class-diagnostics-page.php:188
     226#: admin/class-diagnostics-page.php:189
    225227#: admin/views/diagnostics.php:76
    226228msgid "None"
    227229msgstr ""
    228230
    229 #: admin/class-diagnostics-page.php:192
     231#: admin/class-diagnostics-page.php:193
    230232msgid "== Scheduled tasks =="
    231233msgstr ""
    232234
    233 #: admin/class-diagnostics-page.php:195
     235#: admin/class-diagnostics-page.php:196
    234236#: admin/views/diagnostics.php:102
    235237msgid "Not scheduled"
    236238msgstr ""
    237239
    238 #: admin/class-diagnostics-page.php:197
     240#: admin/class-diagnostics-page.php:198
    239241#: admin/views/diagnostics.php:104
    240242msgid "Scheduled, never run yet"
    241243msgstr ""
    242244
    243 #: admin/class-diagnostics-page.php:199
     245#: admin/class-diagnostics-page.php:200
    244246#: admin/views/diagnostics.php:106
    245247msgid "Scheduled & running"
    … …  
    247249
    248250#. translators: %s: reason the last cron run failed
    249 #: admin/class-diagnostics-page.php:203
     251#: admin/class-diagnostics-page.php:204
    250252#, php-format
    251253msgid "Scheduled, last run failed: %s"
    … …  
    253255
    254256#. translators: 1: task label, 2: status text, 3: next scheduled run, 4: last run time
    255 #: admin/class-diagnostics-page.php:213
     257#: admin/class-diagnostics-page.php:214
    256258#, php-format
    257259msgid "%1$s: %2$s (next: %3$s, last: %4$s)"
    258260msgstr ""
    259261
    260 #: admin/class-diagnostics-page.php:222
     262#: admin/class-diagnostics-page.php:223
    261263msgid "== Connections =="
    262264msgstr ""
    263265
    264 #: admin/class-diagnostics-page.php:224
    265 #: admin/class-diagnostics-page.php:268
     266#: admin/class-diagnostics-page.php:225
     267#: admin/class-diagnostics-page.php:270
    266268msgid "None configured."
    267269msgstr ""
    268270
    269 #: admin/class-diagnostics-page.php:229
     271#: admin/class-diagnostics-page.php:230
    270272msgid "no data received yet"
    271273msgstr ""
    272274
    273 #: admin/class-diagnostics-page.php:230
     275#: admin/class-diagnostics-page.php:231
    274276msgid "active"
    275277msgstr ""
    276278
    277 #: admin/class-diagnostics-page.php:230
     279#: admin/class-diagnostics-page.php:231
    278280msgid "inactive"
    279281msgstr ""
    280282
    281283#. translators: 1: connection name, 2: connector type label, 3: push status text, 4: last received time
    282 #: admin/class-diagnostics-page.php:234
     284#: admin/class-diagnostics-page.php:235
    283285#, php-format
    284286msgid "%1$s (%2$s): %3$s, last received %4$s"
    285287msgstr ""
    286288
    287 #: admin/class-diagnostics-page.php:245
     289#: admin/class-diagnostics-page.php:246
    288290#: admin/views/diagnostics.php:208
    289 #: admin/views/diagnostics.php:327
     291#: admin/views/diagnostics.php:329
    290292msgid "No data yet"
    291293msgstr ""
    292294
    293 #: admin/class-diagnostics-page.php:246
     295#: admin/class-diagnostics-page.php:247
    294296#: admin/views/diagnostics.php:210
    295297msgid "OK"
    296298msgstr ""
    297299
    298 #: admin/class-diagnostics-page.php:246
     300#: admin/class-diagnostics-page.php:247
    299301#: admin/views/diagnostics.php:212
    300302msgid "Error"
    … …  
    302304
    303305#. translators: 1: connection name, 2: connector type label, 3: status text, 4: consecutive error count, 5: last checked time
    304 #: admin/class-diagnostics-page.php:250
     306#: admin/class-diagnostics-page.php:251
    305307#, php-format
    306308msgid "%1$s (%2$s): %3$s, %4$d consecutive errors, last checked %5$s"
    … …  
    308310
    309311#. translators: %s: last recorded error message for this connection
    310 #: admin/class-diagnostics-page.php:260
     312#: admin/class-diagnostics-page.php:261
    311313#, php-format
    312314msgid "  Last error: %s"
    313315msgstr ""
    314316
    315 #: admin/class-diagnostics-page.php:266
     317#: admin/class-diagnostics-page.php:268
    316318msgid "== Alert rules =="
    317319msgstr ""
    318320
    319321#. translators: 1: connection name, 2: entity ID, 3: comparison operator (> or <), 4: threshold value, 5: "Triggered" or "Normal", 6: optional " (last triggered: ...)" note or empty string
    320 #: admin/class-diagnostics-page.php:275
     322#: admin/class-diagnostics-page.php:277
    321323#, php-format
    322324msgid "%1$s / %2$s: %3$s %4$s — %5$s%6$s"
    323325msgstr ""
    324326
    325 #: admin/class-diagnostics-page.php:280
    326 #: admin/views/diagnostics.php:251
     327#: admin/class-diagnostics-page.php:282
     328#: admin/views/diagnostics.php:252
    327329#: includes/premium/admin-views/alert-rules-page.php:54
    328330msgid "Triggered"
    329331msgstr ""
    330332
    331 #: admin/class-diagnostics-page.php:280
    332 #: admin/views/diagnostics.php:252
     333#: admin/class-diagnostics-page.php:282
     334#: admin/views/diagnostics.php:253
    333335#: includes/premium/admin-views/alert-rules-page.php:55
    334336msgid "Normal"
    … …  
    336338
    337339#. translators: %s: last-triggered date/time in UTC
    338 #: admin/class-diagnostics-page.php:283
     340#: admin/class-diagnostics-page.php:285
    339341#, php-format
    340342msgid " (last triggered: %s UTC)"
    341343msgstr ""
    342344
    343 #: admin/class-diagnostics-page.php:290
     345#: admin/class-diagnostics-page.php:292
    344346msgid "== Recent errors =="
    345347msgstr ""
    346348
    347 #: admin/class-diagnostics-page.php:292
     349#: admin/class-diagnostics-page.php:294
    348350msgid "None recorded."
    349351msgstr ""
    350352
    351353#. translators: 1: date/time in UTC, 2: error source, 3: error message
    352 #: admin/class-diagnostics-page.php:297
     354#: admin/class-diagnostics-page.php:299
    353355#, php-format
    354356msgid "[%1$s UTC] %2$s: %3$s"
    355357msgstr ""
    356358
    357 #: admin/class-diagnostics-page.php:306
     359#: admin/class-diagnostics-page.php:308
    358360msgid "== Data size =="
    359361msgstr ""
    360362
    361363#. translators: 1: table label (already translated), 2: row count
    362 #: admin/class-diagnostics-page.php:310
     364#: admin/class-diagnostics-page.php:312
    363365#, php-format
    364366msgid "%1$s: %2$d"
    365367msgstr ""
    366368
    367 #: admin/class-diagnostics-page.php:331
     369#: admin/class-diagnostics-page.php:336
    368370msgid "Recorded value history"
    369371msgstr ""
    370372
    371 #: admin/class-diagnostics-page.php:332
     373#: admin/class-diagnostics-page.php:339
    372374msgid "Widget usage tracking"
    373375msgstr ""
    374376
    375 #: admin/class-diagnostics-page.php:333
    376 #: admin/views/diagnostics.php:235
     377#: admin/class-diagnostics-page.php:342
     378#: admin/views/diagnostics.php:236
    377379#: includes/premium/admin-views/alert-rules-page.php:36
    378380msgid "Alert rules"
    379381msgstr ""
    380382
    381 #: admin/class-diagnostics-page.php:334
     383#: admin/class-diagnostics-page.php:346
    382384msgid "MQTT cache"
    383385msgstr ""
    384386
    385 #: admin/class-diagnostics-page.php:335
     387#: admin/class-diagnostics-page.php:349
    386388msgid "Error log"
    387389msgstr ""
    388390
    389 #: admin/class-diagnostics-page.php:481
     391#: admin/class-diagnostics-page.php:497
    390392msgid "Record value history"
    391393msgstr ""
    392394
    393 #: admin/class-diagnostics-page.php:482
     395#: admin/class-diagnostics-page.php:498
    394396msgid "Clean up old history & error log"
    395397msgstr ""
    396398
    397 #: admin/class-diagnostics-page.php:483
     399#: admin/class-diagnostics-page.php:499
    398400msgid "Poll MQTT brokers"
    399401msgstr ""
    400402
    401 #: admin/class-diagnostics-page.php:487
     403#: admin/class-diagnostics-page.php:500
    402404msgid "Send due email reports"
    403405msgstr ""
    … …  
    421423
    422424#: admin/class-settings-page.php:101
    423 #: admin/views/general-settings-page.php:25
     425#: admin/views/general-settings-page.php:27
    424426msgid "IoT Data Widget — Settings"
    425427msgstr ""
    … …  
    429431msgstr ""
    430432
    431 #: admin/class-settings-page.php:288
     433#: admin/class-settings-page.php:295
    432434msgid "No file was uploaded, or the upload failed."
    433435msgstr ""
    434436
    435 #: admin/class-settings-page.php:292
     437#: admin/class-settings-page.php:299
    436438msgid "The uploaded file is too large to be a valid export."
    437439msgstr ""
    438440
    439 #: admin/class-settings-page.php:298
     441#: admin/class-settings-page.php:305
    440442msgid "Invalid upload."
    441443msgstr ""
    442444
    443 #: admin/class-settings-page.php:304
     445#: admin/class-settings-page.php:311
    444446msgid "Could not read the uploaded file."
    445447msgstr ""
    446448
    447 #: admin/class-settings-page.php:424
     449#: admin/class-settings-page.php:431
    448450msgid "Please choose a valid widget style."
    449451msgstr ""
    … …  
    649651
    650652#: admin/views/connector-form.php:18
    651 #: admin/views/general-settings-page.php:264
     653#: admin/views/general-settings-page.php:281
    652654#: admin/views/wizard/step-push-2-channel.php:35
    653655#: includes/premium/admin-views/alert-rule-form.php:20
    … …  
    663665
    664666#: admin/views/connector-form.php:32
    665 #: admin/views/settings-page.php:139
     667#: admin/views/settings-page.php:141
    666668msgid "Add connection"
    667669msgstr ""
    … …  
    723725#: admin/views/diagnostics.php:93
    724726#: admin/views/diagnostics.php:154
    725 #: admin/views/diagnostics.php:241
     727#: admin/views/diagnostics.php:242
    726728#: includes/premium/admin-views/alert-rules-page.php:44
    727729msgid "Status"
    … …  
    750752#: admin/views/diagnostics.php:171
    751753#: admin/views/diagnostics.php:202
    752 #: admin/views/diagnostics.php:257
    753 #: admin/views/diagnostics.php:290
     754#: admin/views/diagnostics.php:258
     755#: admin/views/diagnostics.php:292
    754756#, php-format
    755757msgid "%s UTC"
    … …  
    761763
    762764#: admin/views/diagnostics.php:151
    763 #: admin/views/diagnostics.php:238
    764 #: admin/views/general-settings-page.php:169
    765 #: admin/views/general-settings-page.php:227
    766 #: admin/views/general-settings-page.php:277
     765#: admin/views/diagnostics.php:239
     766#: admin/views/general-settings-page.php:186
     767#: admin/views/general-settings-page.php:244
     768#: admin/views/general-settings-page.php:294
    767769#: includes/premium/admin-views/alert-rule-form.php:33
    768770#: includes/premium/admin-views/alert-rules-page.php:40
    … …  
    775777
    776778#: admin/views/diagnostics.php:152
    777 #: admin/views/settings-page.php:67
     779#: admin/views/settings-page.php:69
    778780msgid "Type"
    779781msgstr ""
    … …  
    804806
    805807#: admin/views/diagnostics.php:230
    806 #: admin/views/settings-page.php:128
     808#: admin/views/settings-page.php:130
    807809msgid "No connections configured yet."
    808810msgstr ""
    809811
    810 #: admin/views/diagnostics.php:239
    811 #: admin/views/general-settings-page.php:182
    812 #: admin/views/general-settings-page.php:193
    813 #: admin/views/general-settings-page.php:228
    814 #: admin/views/general-settings-page.php:293
    815 #: admin/views/general-settings-page.php:304
     812#: admin/views/diagnostics.php:240
     813#: admin/views/general-settings-page.php:199
     814#: admin/views/general-settings-page.php:210
     815#: admin/views/general-settings-page.php:245
     816#: admin/views/general-settings-page.php:310
     817#: admin/views/general-settings-page.php:321
    816818#: admin/views/wizard/step-5-done.php:34
    817819#: admin/views/wizard/step-push-3-snippet.php:117
    … …  
    831833msgstr[1] ""
    832834
    833 #: admin/views/diagnostics.php:240
     835#: admin/views/diagnostics.php:241
    834836#: includes/premium/admin-views/alert-rule-form.php:81
    835837#: includes/premium/admin-views/alert-rules-page.php:42
    … …  
    837839msgstr ""
    838840
    839 #: admin/views/diagnostics.php:242
     841#: admin/views/diagnostics.php:243
    840842msgid "Last triggered"
    841843msgstr ""
    842844
    843 #: admin/views/diagnostics.php:271
     845#: admin/views/diagnostics.php:272
    844846#: includes/premium/admin-views/alert-rules-page.php:81
    845847msgid "No alert rules configured yet."
    846848msgstr ""
    847849
    848 #: admin/views/diagnostics.php:276
     850#: admin/views/diagnostics.php:278
    849851msgid "Recent errors"
    850852msgstr ""
    851853
    852 #: admin/views/diagnostics.php:279
     854#: admin/views/diagnostics.php:281
    853855msgid "When"
    854856msgstr ""
    855857
    856 #: admin/views/diagnostics.php:280
     858#: admin/views/diagnostics.php:282
    857859msgid "Source"
    858860msgstr ""
    859861
    860 #: admin/views/diagnostics.php:281
     862#: admin/views/diagnostics.php:283
    861863msgid "Message"
    862864msgstr ""
    863865
    864 #: admin/views/diagnostics.php:300
     866#: admin/views/diagnostics.php:302
    865867msgid "No errors recorded — everything is working."
    866868msgstr ""
    867869
    868 #: admin/views/diagnostics.php:305
     870#: admin/views/diagnostics.php:307
    869871msgid "Data size"
    870872msgstr ""
    871873
    872 #: admin/views/diagnostics.php:311
     874#: admin/views/diagnostics.php:313
    873875msgid "Table"
    874876msgstr ""
    875877
    876 #: admin/views/diagnostics.php:312
     878#: admin/views/diagnostics.php:314
    877879msgid "Rows"
    878880msgstr ""
    879881
    880 #: admin/views/general-settings-page.php:29
     882#: admin/views/general-settings-page.php:31
    881883msgid "Settings saved."
    882884msgstr ""
    883885
    884 #: admin/views/general-settings-page.php:30
     886#: admin/views/general-settings-page.php:32
    885887msgid "Manually tracked entity added."
    886888msgstr ""
    887889
    888 #: admin/views/general-settings-page.php:31
     890#: admin/views/general-settings-page.php:33
    889891msgid "Manually tracked entity removed."
    890892msgstr ""
    891893
    892894#. translators: %d: number of imported connections
    893 #: admin/views/general-settings-page.php:50
     895#: admin/views/general-settings-page.php:52
    894896#, php-format
    895897msgid "Imported %d connection."
    … …  
    899901
    900902#. translators: %d: number of skipped entries
    901 #: admin/views/general-settings-page.php:61
     903#: admin/views/general-settings-page.php:63
    902904#, php-format
    903905msgid "Skipped %d entry:"
    … …  
    906908msgstr[1] ""
    907909
    908 #: admin/views/general-settings-page.php:74
     910#: admin/views/general-settings-page.php:76
    909911msgid "Imported connections need their password/authorization re-entered before use — these are never included in an export, by design."
    910912msgstr ""
    911913
    912 #: admin/views/general-settings-page.php:79
     914#: admin/views/general-settings-page.php:81
    913915msgid "Import failed:"
    914916msgstr ""
    915917
    916 #: admin/views/general-settings-page.php:86
     918#: admin/views/general-settings-page.php:88
    917919msgid "Branding"
    918920msgstr ""
    919921
    920 #: admin/views/general-settings-page.php:92
     922#: admin/views/general-settings-page.php:94
    921923msgid "Front-end attribution"
    922924msgstr ""
    923925
    924 #: admin/views/general-settings-page.php:96
     926#: admin/views/general-settings-page.php:98
    925927msgid "Show \"Powered by IoT Data Widget\" beneath widgets"
    926928msgstr ""
    927929
    928 #: admin/views/general-settings-page.php:99
     930#: admin/views/general-settings-page.php:101
    929931msgid "Off by default. Turn this on if you'd like to credit the plugin beneath your widgets."
    930932msgstr ""
    931933
    932 #: admin/views/general-settings-page.php:104
     934#: admin/views/general-settings-page.php:106
    933935msgid "Stale data threshold"
    934936msgstr ""
    935937
    936 #: admin/views/general-settings-page.php:114
     938#: admin/views/general-settings-page.php:116
    937939msgid "minutes"
    938940msgstr ""
    939941
    940 #: admin/views/general-settings-page.php:116
     942#: admin/views/general-settings-page.php:118
    941943msgid "Past this age, widgets show the value grayed out with a timestamp instead of implying it's current — mainly relevant for push channels, where a device can stop sending data without any explicit connection error."
    942944msgstr ""
    943945
    944 #: admin/views/general-settings-page.php:121
     946#: admin/views/general-settings-page.php:123
     947msgid "Local network"
     948msgstr ""
     949
     950#: admin/views/general-settings-page.php:127
     951msgid "Allow local network addresses"
     952msgstr ""
     953
     954#: admin/views/general-settings-page.php:130
     955msgid "Turn this on only if WordPress runs on the same network as your devices (e.g. a home server) and connects to addresses like 192.168.x.x or homeassistant.local. Off by default: it stops a misconfigured or malicious URL from reaching other services on your server's network. Cloud metadata and link-local addresses are always blocked."
     956msgstr ""
     957
     958#: admin/views/general-settings-page.php:133
     959msgid "On a multisite network, only a network administrator can change this."
     960msgstr ""
     961
     962#: admin/views/general-settings-page.php:138
    945963msgid "Uninstall"
    946964msgstr ""
    947965
    948 #: admin/views/general-settings-page.php:125
     966#: admin/views/general-settings-page.php:142
    949967msgid "Remove all plugin data when the plugin is deleted"
    950968msgstr ""
    951969
    952 #: admin/views/general-settings-page.php:128
     970#: admin/views/general-settings-page.php:145
    953971msgid "Off by default. Deactivating the plugin (e.g. during an update) never touches your data either way — only checking this box AND THEN deleting the plugin from the Plugins screen removes connections, history, alerts and every other setting. Leave this unchecked if you might reinstall later."
    954972msgstr ""
    955973
    956 #: admin/views/general-settings-page.php:133
     974#: admin/views/general-settings-page.php:150
    957975msgid "Save settings"
    958976msgstr ""
    959977
    960 #: admin/views/general-settings-page.php:136
     978#: admin/views/general-settings-page.php:153
    961979msgid "Export connections"
    962980msgstr ""
    963981
    964 #: admin/views/general-settings-page.php:137
     982#: admin/views/general-settings-page.php:154
    965983msgid "Downloads a JSON file with all your connections. Passwords and authorization tokens are never included — re-enter them after importing."
    966984msgstr ""
    967985
    968 #: admin/views/general-settings-page.php:141
     986#: admin/views/general-settings-page.php:158
    969987msgid "Download export"
    970988msgstr ""
    971989
    972 #: admin/views/general-settings-page.php:144
     990#: admin/views/general-settings-page.php:161
    973991msgid "Import connections"
    974992msgstr ""
    975993
    976 #: admin/views/general-settings-page.php:145
     994#: admin/views/general-settings-page.php:162
    977995msgid "Imports connections from a previously exported JSON file. Connections with a matching ID are overwritten; others are added. Existing connections not present in the file are left untouched."
    978996msgstr ""
    979997
    980 #: admin/views/general-settings-page.php:150
     998#: admin/views/general-settings-page.php:167
    981999msgid "Import"
    9821000msgstr ""
    9831001
    984 #: admin/views/general-settings-page.php:153
     1002#: admin/views/general-settings-page.php:170
    9851003msgid "Export history data"
    9861004msgstr ""
    9871005
    988 #: admin/views/general-settings-page.php:156
     1006#: admin/views/general-settings-page.php:173
    9891007#: includes/premium/class-history-csv-export.php:71
    9901008msgid "CSV export of recorded history requires the Pro version."
    9911009msgstr ""
    9921010
    993 #: admin/views/general-settings-page.php:160
     1011#: admin/views/general-settings-page.php:177
    9941012msgid "Downloads the raw recorded history for one connection/entity as a CSV file, for further analysis outside the plugin. Only entities used by a Chart/Sparkline widget (or manually tracked below) have history recorded."
    9951013msgstr ""
    9961014
    997 #: admin/views/general-settings-page.php:172
    998 #: admin/views/general-settings-page.php:280
     1015#: admin/views/general-settings-page.php:189
     1016#: admin/views/general-settings-page.php:297
    9991017#: includes/premium/admin-views/alert-rule-form.php:40
    10001018#: includes/premium/admin-views/email-report-form.php:40
    … …  
    10031021msgstr ""
    10041022
    1005 #: admin/views/general-settings-page.php:187
    1006 #: admin/views/general-settings-page.php:298
     1023#: admin/views/general-settings-page.php:204
     1024#: admin/views/general-settings-page.php:315
    10071025#: includes/premium/admin-views/alert-rule-form.php:58
    10081026#: includes/premium/admin-views/email-report-form.php:58
    … …  
    10121030msgstr ""
    10131031
    1014 #: admin/views/general-settings-page.php:197
     1032#: admin/views/general-settings-page.php:214
    10151033msgid "Useful if the entity was renamed/removed from the live connection since history was recorded."
    10161034msgstr ""
    10171035
    1018 #: admin/views/general-settings-page.php:201
    1019 #: admin/views/general-settings-page.php:315
     1036#: admin/views/general-settings-page.php:218
     1037#: admin/views/general-settings-page.php:332
    10201038#: includes/premium/admin-views/alert-rule-form.php:75
    10211039#: includes/premium/admin-views/email-report-form.php:75
    … …  
    10251043msgstr ""
    10261044
    1027 #: admin/views/general-settings-page.php:207
     1045#: admin/views/general-settings-page.php:224
    10281046msgid "From"
    10291047msgstr ""
    10301048
    1031 #: admin/views/general-settings-page.php:211
     1049#: admin/views/general-settings-page.php:228
    10321050msgid "To"
    10331051msgstr ""
    10341052
    1035 #: admin/views/general-settings-page.php:216
     1053#: admin/views/general-settings-page.php:233
    10361054msgid "Download CSV"
    10371055msgstr ""
    10381056
    1039 #: admin/views/general-settings-page.php:220
     1057#: admin/views/general-settings-page.php:237
    10401058msgid "Manually tracked entities"
    10411059msgstr ""
    10421060
    1043 #: admin/views/general-settings-page.php:222
     1061#: admin/views/general-settings-page.php:239
    10441062msgid "Widgets placed in post/page content are detected automatically. If you output a widget from a theme template instead (e.g. in a widget area or a custom page template), add it here so it still gets polled for history/alerts even though there's no post content to scan."
    10451063msgstr ""
    10461064
    1047 #: admin/views/general-settings-page.php:229
     1065#: admin/views/general-settings-page.php:246
    10481066#: blocks/iot-widget/src/edit.js:478
    10491067msgid "Style"
    10501068msgstr ""
    10511069
    1052 #: admin/views/general-settings-page.php:241
     1070#: admin/views/general-settings-page.php:258
    10531071msgid "Stop tracking this entity?"
    10541072msgstr ""
    10551073
    1056 #: admin/views/general-settings-page.php:242
     1074#: admin/views/general-settings-page.php:259
    10571075#: blocks/iot-widget/src/grid-item-editor.js:117
    10581076msgid "Remove"
    10591077msgstr ""
    10601078
    1061 #: admin/views/general-settings-page.php:255
     1079#: admin/views/general-settings-page.php:272
    10621080msgid "No manually tracked entities yet."
    10631081msgstr ""
    10641082
    1065 #: admin/views/general-settings-page.php:256
     1083#: admin/views/general-settings-page.php:273
    10661084msgid "Track your first entity"
    10671085msgstr ""
    10681086
    1069 #: admin/views/general-settings-page.php:288
     1087#: admin/views/general-settings-page.php:305
    10701088#: includes/premium/admin-views/alert-rule-form.php:48
    10711089#: includes/premium/admin-views/email-report-form.php:48
    … …  
    10731091msgstr ""
    10741092
    1075 #: admin/views/general-settings-page.php:321
     1093#: admin/views/general-settings-page.php:338
    10761094msgid "Widget style"
    10771095msgstr ""
    10781096
    1079 #: admin/views/general-settings-page.php:324
     1097#: admin/views/general-settings-page.php:341
    10801098#: blocks/iot-widget/src/edit.js:75
    10811099msgid "Card"
    10821100msgstr ""
    10831101
    1084 #: admin/views/general-settings-page.php:325
     1102#: admin/views/general-settings-page.php:342
    10851103#: blocks/iot-widget/src/edit.js:76
    10861104msgid "Status list"
    10871105msgstr ""
    10881106
    1089 #: admin/views/general-settings-page.php:326
     1107#: admin/views/general-settings-page.php:343
    10901108#: blocks/iot-widget/src/edit.js:77
    10911109msgid "Gauge"
    10921110msgstr ""
    10931111
    1094 #: admin/views/general-settings-page.php:327
     1112#: admin/views/general-settings-page.php:344
    10951113#: blocks/iot-widget/src/edit.js:78
    10961114msgid "Chart"
    10971115msgstr ""
    10981116
    1099 #: admin/views/general-settings-page.php:328
     1117#: admin/views/general-settings-page.php:345
    11001118#: blocks/iot-widget/src/edit.js:79
    11011119msgid "Mini trend (sparkline)"
    11021120msgstr ""
    11031121
    1104 #: admin/views/general-settings-page.php:331
     1122#: admin/views/general-settings-page.php:348
    11051123msgid "Must match the style actually rendered by your theme template — Chart/Sparkline are what determine whether this entity gets recorded into history."
    11061124msgstr ""
    11071125
    1108 #: admin/views/general-settings-page.php:337
     1126#: admin/views/general-settings-page.php:354
    11091127msgid "Add manually tracked entity"
    11101128msgstr ""
    11111129
    11121130#: admin/views/partials/connector-fields.php:30
    1113 #: admin/views/settings-page.php:66
     1131#: admin/views/settings-page.php:68
    11141132msgid "Name"
    11151133msgstr ""
    … …  
    11201138
    11211139#: admin/views/partials/connector-fields.php:73
    1122 #: admin/views/settings-page.php:68
     1140#: admin/views/settings-page.php:70
    11231141msgid "URL"
    11241142msgstr ""
    … …  
    11821200msgstr ""
    11831201
    1184 #: admin/views/settings-page.php:36
    1185 msgid "The free version supports one connection. Upgrade to Pro for unlimited connections."
    1186 msgstr ""
    1187 
    1188 #: admin/views/settings-page.php:40
     1202#: admin/views/settings-page.php:38
     1203msgid "The demo connection could not be saved. Please try again, or add a connection manually below."
     1204msgstr ""
     1205
     1206#: admin/views/settings-page.php:42
    11891207msgid "Connection saved."
    11901208msgstr ""
    11911209
    1192 #: admin/views/settings-page.php:41
     1210#: admin/views/settings-page.php:43
    11931211msgid "Connection deleted."
    11941212msgstr ""
    11951213
    1196 #: admin/views/settings-page.php:51
     1214#: admin/views/settings-page.php:53
    11971215msgid "Now that you have a connection, try these next:"
    11981216msgstr ""
    11991217
    1200 #: admin/views/settings-page.php:57
     1218#: admin/views/settings-page.php:59
    12011219msgid "Dismiss this notice."
    12021220msgstr ""
    12031221
    1204 #: admin/views/settings-page.php:62
     1222#: admin/views/settings-page.php:64
    12051223msgid "Saved connections"
    12061224msgstr ""
    12071225
    1208 #: admin/views/settings-page.php:69
     1226#: admin/views/settings-page.php:71
    12091227msgid "Shortcode"
    12101228msgstr ""
    12111229
    1212 #: admin/views/settings-page.php:108
     1230#: admin/views/settings-page.php:110
    12131231#: includes/premium/admin-views/alert-rules-page.php:60
    12141232#: includes/premium/admin-views/email-reports-page.php:56
    … …  
    12161234msgstr ""
    12171235
    1218 #: admin/views/settings-page.php:114
     1236#: admin/views/settings-page.php:116
    12191237msgid "Are you sure you want to delete this connection?"
    12201238msgstr ""
    12211239
    1222 #: admin/views/settings-page.php:129
     1240#: admin/views/settings-page.php:131
    12231241msgid "Add your first connection"
    12241242msgstr ""
    12251243
    1226 #: admin/views/settings-page.php:138
     1244#: admin/views/settings-page.php:140
    12271245msgid "Edit connection"
    12281246msgstr ""
    … …  
    12901308msgstr ""
    12911309
    1292 #: admin/views/wizard/step-0-location.php:31
     1310#: admin/views/wizard/step-0-location.php:32
    12931311msgid "Same network as my devices (Raspberry Pi, NAS, home server) — my site fetches data from the device"
    12941312msgstr ""
    12951313
    1296 #: admin/views/wizard/step-0-location.php:47
     1314#: admin/views/wizard/step-0-location.php:51
    12971315msgid "On external/shared hosting, but my device has its own cloud service (Shelly Cloud, Supla, a publicly-reachable Home Assistant) — my site fetches data from there"
    12981316msgstr ""
    12991317
    1300 #: admin/views/wizard/step-0-location.php:52
     1318#: admin/views/wizard/step-0-location.php:53
     1319msgid "On external/shared hosting, but my device has its own cloud service (a publicly-reachable Home Assistant; Shelly Cloud and Supla in Pro) — my site fetches data from there"
     1320msgstr ""
     1321
     1322#: admin/views/wizard/step-0-location.php:60
    13011323msgid "Not sure? Choose the first option — it's the safer default."
    13021324msgstr ""
    13031325
    1304 #: admin/views/wizard/step-0-location.php:57
     1326#: admin/views/wizard/step-0-location.php:65
    13051327msgid "Devices behind a home router (NAT) almost never accept incoming connections from the internet — if your WordPress is hosted externally, it typically cannot reach a device address like 192.168.x.x at all. Sending data out from the device instead always works, and so does fetching from a cloud service the device already reports to."
    13061328msgstr ""
    13071329
    1308 #: admin/views/wizard/step-0-location.php:71
     1330#: admin/views/wizard/step-0-location.php:79
    13091331msgid "Security note: in push mode, this plugin never stores any credentials to your devices — the device sends data out, authenticated by a token this plugin generates that only allows writing one entity to one channel. In pull mode, a copy of your device's own credentials (e.g. a Home Assistant access token) is stored in your WordPress database."
    13101332msgstr ""
    13111333
    1312 #: admin/views/wizard/step-0-location.php:75
     1334#: admin/views/wizard/step-0-location.php:83
    13131335#: admin/views/wizard/step-1-connection-type.php:92
    13141336#: admin/views/wizard/step-2-configure.php:87
    … …  
    15821604msgstr ""
    15831605
     1606#: includes/class-http-client.php:98
     1607msgid "The server redirected too many times."
     1608msgstr ""
     1609
     1610#: includes/class-http-client.php:133
     1611msgid "The configured URL has no valid host (only http:// and https:// addresses are supported)."
     1612msgstr ""
     1613
     1614#. translators: %s: the host that was blocked
     1615#: includes/class-http-client.php:151
     1616#: includes/class-http-client.php:171
     1617#, php-format
     1618msgid "Refusing to connect to \"%s\" — it resolves to a link-local or reserved address (such as a cloud metadata service). This is always blocked."
     1619msgstr ""
     1620
     1621#. translators: %s: the host that was blocked
     1622#: includes/class-http-client.php:252
     1623#, php-format
     1624msgid "Refusing to connect to \"%s\" — it is a local network address. If WordPress runs on the same network as the device, enable \"Allow local network addresses\" under IoT Data Widget → Settings. Otherwise, use a push channel instead."
     1625msgstr ""
     1626
     1627#: includes/class-local-network.php:205
     1628msgid "Local network addresses are now allowed, so this site can reach devices on your network. You can change this under IoT Data Widget → Settings."
     1629msgstr ""
     1630
     1631#: includes/class-local-network.php:210
     1632msgid "Connections to local network addresses are blocked on this site, and only a network administrator can allow them (IoT Data Widget → Settings). Until then, choose push mode instead."
     1633msgstr ""
     1634
     1635#: includes/class-local-network.php:222
     1636msgid "Security update: IoT Data Widget now refuses connections to local network addresses unless \"Allow local network addresses\" is turned on. This site already connects to a device on your local network, so the setting was turned on automatically — nothing stopped working."
     1637msgstr ""
     1638
     1639#: includes/class-local-network.php:224
     1640msgid "Review settings"
     1641msgstr ""
     1642
     1643#: includes/class-local-network.php:226
     1644msgid "Dismiss"
     1645msgstr ""
     1646
    15841647#: includes/class-push-ingest.php:120
    15851648msgid "Request body is too large."
    … …  
    17021765
    17031766#: includes/class-renderer.php:226
    1704 #: includes/class-shortcode.php:133
     1767#: includes/class-shortcode.php:153
    17051768msgid "This style requires the Pro version."
    17061769msgstr ""
    … …  
    17731836msgstr[1] ""
    17741837
    1775 #: includes/class-rest-api.php:726
     1838#: includes/class-rest-api.php:732
    17761839msgid "Not found."
    17771840msgstr ""
    17781841
    1779 #: includes/class-shortcode.php:146
     1842#: includes/class-shortcode.php:166
    17801843msgid "Fill in the \"connection\" and \"entity\" attributes in the [iotdw_widget] shortcode."
    17811844msgstr ""
    17821845
    17831846#. translators: %s: connection name or ID given in the shortcode
    1784 #: includes/class-shortcode.php:159
    1785 #: includes/class-shortcode.php:461
    1786 #: includes/class-shortcode.php:531
     1847#: includes/class-shortcode.php:179
     1848#: includes/class-shortcode.php:481
     1849#: includes/class-shortcode.php:551
    17871850#, php-format
    17881851msgid "Connection \"%s\" was not found."
    17891852msgstr ""
    17901853
    1791 #: includes/class-shortcode.php:434
     1854#: includes/class-shortcode.php:454
    17921855msgid "Powered by IoT Data Widget"
    17931856msgstr ""
    17941857
    1795 #: includes/class-shortcode.php:448
     1858#: includes/class-shortcode.php:468
    17961859msgid "Fill in the \"connection\" and \"entities\" attributes (comma-separated entity IDs) in the [iotdw_widget style=\"list\"] shortcode."
    17971860msgstr ""
    17981861
    1799 #: includes/class-shortcode.php:512
     1862#: includes/class-shortcode.php:532
    18001863msgid "Add at least one entity to the [iotdw_widget style=\"grid\"] shortcode via the \"items\" attribute."
    18011864msgstr ""
    18021865
    1803 #: includes/class-shortcode.php:513
     1866#: includes/class-shortcode.php:533
    18041867msgid "The \"items\" attribute of the [iotdw_widget style=\"grid\"] shortcode must be a comma-separated list of \"connection|entity\" pairs (e.g. items=\"Home|sensor.temperature,Workshop|relay_state\") — none of the given items matched that format."
    18051868msgstr ""
    18061869
    18071870#. translators: %s: PHP error message
    1808 #: includes/class-shortcode.php:714
    1809 #: includes/class-shortcode.php:746
     1871#: includes/class-shortcode.php:780
     1872#: includes/class-shortcode.php:812
    18101873#, php-format
    18111874msgid "Unexpected connector error: %s"
    18121875msgstr ""
    18131876
    1814 #: includes/class-shortcode.php:861
     1877#: includes/class-shortcode.php:927
     1878#: includes/class-value-normalizer.php:103
    18151879#: blocks/iot-widget/src/edit.js:641
    18161880msgid "On"
    18171881msgstr ""
    18181882
    1819 #: includes/class-shortcode.php:862
     1883#: includes/class-shortcode.php:928
     1884#: includes/class-value-normalizer.php:105
    18201885#: blocks/iot-widget/src/edit.js:651
    18211886msgid "Off"
    … …  
    18231888
    18241889#. translators: %s: the local-network host/IP that was configured
    1825 #: includes/connectors/abstract-connector.php:136
     1890#: includes/connectors/abstract-connector.php:141
    18261891#, php-format
    18271892msgid "This looks like a local network address (%s) — it will only work if your WordPress server is on the same network as the device. Most external/shared hosting can't reach it; consider Shelly Cloud, a publicly reachable Home Assistant instance, or a differently-reachable REST endpoint instead."
    … …  
    23372402msgstr ""
    23382403
    2339 #: includes/premium/class-mqtt-poll-engine.php:186
     2404#: includes/premium/class-mqtt-poll-engine.php:194
    23402405msgid "Missing MQTT broker host in the connection settings."
    23412406msgstr ""
    23422407
    2343 #: includes/premium/class-mqtt-poll-engine.php:261
     2408#: includes/premium/class-mqtt-poll-engine.php:269
    23442409msgid "Missing EMQX REST API URL in the connection settings."
    23452410msgstr ""
    23462411
    23472412#. translators: %d: HTTP status code
    2348 #: includes/premium/class-mqtt-poll-engine.php:298
     2413#: includes/premium/class-mqtt-poll-engine.php:306
    23492414#, php-format
    23502415msgid "EMQX REST API returned an unexpected response code: %d."
    23512416msgstr ""
    23522417
    2353 #: includes/premium/class-mqtt-poll-engine.php:308
     2418#: includes/premium/class-mqtt-poll-engine.php:316
    23542419msgid "Invalid or unexpected response shape from the EMQX REST API."
    23552420msgstr ""
  • iot-data-widget/trunk/public/css/widget-styles.css

    r3730704 r3732074  
     1/*
     2 * Wrapper z IOTDW_Shortcode::render() — celowo bez szerokości/marginesów
     3 * poziomych: dostaje je od motywu jak każdy inny blok (kolumna treści).
     4 * Odstęp między dwoma widgetami pod rząd przez :where() (zerowa
     5 * specyficzność) — motywy blokowe (blockGap) i tak go nadpisują swoim.
     6 */
     7:where(.iot-dw-widget-wrap + .iot-dw-widget-wrap) {
     8        margin-top: 1em;
     9}
     10
    111.iot-dw-widget {
    212        display: inline-block;
    … …  
    137147        display: block;
    138148        list-style: none;
    139         /* Tylko poziome — pionowe zostawiamy motywowi/WP (odstęp między blokami). */
    140         margin-left: 0;
    141         margin-right: 0;
     149        margin: 0;
    142150        padding: 0;
    143151        box-sizing: border-box;
    … …  
    351359}
    352360
    353 /*
    354  * Motywy blokowe (np. Twenty Twenty-Five): layout "constrained" wyrównuje
    355  * dzieci do kolumny treści przez `margin-left/right: auto !important`, ale
    356  * na elementach inline-level `auto` liczy się jako 0 — card/gauge/sparkline
    357  * lądowały na lewej krawędzi strony zamiast w kolumnie. Tu: blokowe, szerokość
    358  * wg treści, lewa krawędź = lewa krawędź kolumny. !important, bo reguła WP też
    359  * go ma (wygrywa wyższa specyficzność). Poza layoutem constrained (motywy
    360  * klasyczne, grupa Row/flex) bez zmian — dalej inline, obok siebie.
    361  */
    362 .is-layout-constrained > .iot-dw-widget--card,
    363 .is-layout-constrained > .iot-dw-widget--gauge,
    364 .is-layout-constrained > .iot-dw-widget--sparkline {
    365         width: fit-content;
    366         margin-left: max( 0px, calc( ( 100% - var( --wp--style--global--content-size, 100% ) ) / 2 ) ) !important;
    367         margin-right: auto !important;
    368 }
    369 
    370 .is-layout-constrained > .iot-dw-widget--card {
    371         display: block;
    372 }
    373 
    374 .is-layout-constrained > .iot-dw-widget--gauge,
    375 .is-layout-constrained > .iot-dw-widget--sparkline {
    376         display: flex;
    377 }
  • iot-data-widget/trunk/readme.txt

    r3730704 r3732074  
    55Tested up to: 7.1
    66Requires PHP: 8.1
    7 Stable tag: 1.0.1
     7Stable tag: 1.0.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    22222. Activate the plugin through the **Plugins** screen — this opens the **setup wizard** automatically (skip this step by activating several plugins at once, or by clicking "Skip the wizard" on its first screen).
    23233. Follow the wizard: pick a connection type, configure and test it live, choose an entity, and copy the ready-made shortcode from the preview screen — or configure a connection manually under **IoT Data Widget** in the admin menu instead:
    24    * **Home Assistant**: enter your instance URL (e.g. `http://homeassistant.local:8123`) and a Long-Lived Access Token. Generate one in Home Assistant under your profile page → **Security** → **Long-Lived Access Tokens** → **Create Token**.
     24   * **Home Assistant**: enter your instance URL (e.g. `http://homeassistant.local:8123`) and a Long-Lived Access Token. Generate one in Home Assistant under your profile page → **Security** → **Long-Lived Access Tokens** → **Create Token**. If the URL is a local network address, also turn on **Settings → Allow local network addresses** (the wizard's "Same network as my devices" option does this for you).
    2525   * **Push mode**: no URL/credentials to enter — the wizard generates a channel and a ready-to-paste snippet for your device instead.
    2626   * **Generic REST, advanced mode**: enter the full URL of a JSON endpoint. Leave Authorization empty for public APIs, or enter a full header (e.g. `Authorization: Bearer xxx` or `X-API-Key: xxx`) if the API requires one.
    … …  
    6666Generic REST (entity is a JSONPath into the JSON response): `[iotdw_widget connection="Weather API" entity="current_weather.temperature"]`
    6767
    68 Entities with a non-numeric state (a relay's on/off, a presence sensor's home/away...) show their real value ("ON", "true") in Card and Status list styles.
     68Entities with a non-numeric state (a relay's on/off, a presence sensor's home/away...) show their real state ("ON", "home") in Card and Status list styles; a raw true/false from a JSON API shows as "On"/"Off".
    6969
    7070= I don't want to use the setup wizard — can I skip it? =
    … …  
    9393
    9494Push mode sidesteps this entirely: the plugin never stores any credentials to your devices, because your device sends data to the plugin instead of the other way around.
     95
     96= My device is on the same network as WordPress, but the connection is refused =
     97
     98For security, the plugin doesn't connect to local network addresses (like 192.168.x.x or homeassistant.local) unless you allow it. If WordPress runs on the same network as your devices — a Raspberry Pi, NAS or home server — turn on **IoT Data Widget → Settings → Allow local network addresses** (on a multisite network, only a network administrator can). Choosing "Same network as my devices" in the setup wizard does this for you. If your site is hosted externally, use push mode instead: it can't reach your home network anyway.
    9599
    96100= Does this work with any REST API, or only Home Assistant? =
    … …  
    133137== Changelog ==
    134138
     139= 1.0.2 =
     140* Security: requests to local network addresses (192.168.x.x, 10.x.x.x, homeassistant.local…) are now blocked unless "Allow local network addresses" is turned on under Settings; cloud metadata and link-local addresses are always blocked, including via redirects. If this site already connects to a device on your local network, the setting is turned on automatically during the update — nothing stops working. Choosing "Same network as my devices" in the setup wizard turns it on too.
     141* Added: widgets placed in classic theme widget areas (sidebars, footers) now load their styles and refresh live — previously they appeared unstyled and never updated.
     142* Fixed: widgets line up with the content column in classic themes such as Twenty Twenty-One. Widgets placed one after another now stack vertically — use a Row or Columns block to place them side by side.
     143* Fixed: "Test connection" (and alert e-mails in Pro) show "On"/"Off" instead of "true"/"false" for on/off devices such as Shelly relays.
     144* Fixed: the free version no longer schedules or lists Pro-only background tasks (MQTT polling, email reports, value history); updating removes them.
     145* Fixed: the setup wizard labels Shelly Cloud and Supla as Pro, and shows an accurate message if the demo connection can't be saved.
     146
    135147= 1.0.1 =
    136148* Added: "Upgrade" link in the plugin menu and the plugin list, and an upgrade link next to Pro-only styles (visible to administrators only).
Note: See TracChangeset for help on using the changeset viewer.