Commit a17c5b77578e8bfed9545f91ea39796adfd864d2

Authored by Igor Kulikov
1 parent 4b0cf383

Improve timezone offset handling for latest values and alarm widgets. Improve ti…

…mezone selector - add local browser time info on empty value.
... ... @@ -130,6 +130,7 @@ export class AlarmDataSubscription {
130 130 this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs;
131 131 }
132 132
  133 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
133 134 this.subscriber.subscriptionCommands.push(this.alarmDataCommand);
134 135
135 136 this.subscriber.alarmData$.subscribe((alarmDataUpdate) => {
... ... @@ -143,8 +144,11 @@ export class AlarmDataSubscription {
143 144 this.subscriber.subscribe();
144 145
145 146 } else if (this.datasourceType === DatasourceType.function) {
  147 + const alarm = deepClone(simulatedAlarm);
  148 + alarm.createdTime += this.subsTw.tsOffset;
  149 + alarm.startTs += this.subsTw.tsOffset;
146 150 const pageData: PageData<AlarmData> = {
147   - data: [{...simulatedAlarm, entityId: '1', latest: {}}],
  151 + data: [{...alarm, entityId: '1', latest: {}}],
148 152 hasNext: false,
149 153 totalElements: 1,
150 154 totalPages: 1
... ...
... ... @@ -15,7 +15,7 @@
15 15 ///
16 16
17 17 import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
18   -import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models';
  18 +import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
19 19 import {
20 20 EntityData,
21 21 EntityDataPageLink,
... ... @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions {
74 74 keyFilters?: Array<KeyFilter>;
75 75 additionalKeyFilters?: Array<KeyFilter>;
76 76 subscriptionTimewindow?: SubscriptionTimewindow;
  77 + latestTsOffset?: number;
77 78 }
78 79
79 80 export class EntityDataSubscription {
... ... @@ -95,6 +96,7 @@ export class EntityDataSubscription {
95 96 private entityDataResolveSubject: Subject<EntityDataLoadResult>;
96 97 private pageData: PageData<EntityData>;
97 98 private subsTw: SubscriptionTimewindow;
  99 + private latestTsOffset: number;
98 100 private dataAggregators: Array<DataAggregator>;
99 101 private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
100 102 private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
... ... @@ -177,6 +179,7 @@ export class EntityDataSubscription {
177 179 this.started = true;
178 180 this.dataResolved = true;
179 181 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  182 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
180 183 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
181 184 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
182 185 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
... ... @@ -237,7 +240,12 @@ export class EntityDataSubscription {
237 240 };
238 241
239 242 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
240   - this.prepareSubscriptionCommands();
  243 + this.prepareSubscriptionCommands(this.dataCommand);
  244 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  245 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
  246 + } else {
  247 + this.subscriber.setTsOffset(this.latestTsOffset);
  248 + }
241 249 }
242 250
243 251 this.subscriber.subscriptionCommands.push(this.dataCommand);
... ... @@ -276,9 +284,15 @@ export class EntityDataSubscription {
276 284 this.subscriber.subscriptionCommands = [this.dataCommand];
277 285 }
278 286 });
279   -
280 287 this.subscriber.subscribe();
281 288 } else if (this.datasourceType === DatasourceType.function) {
  289 + let tsOffset = 0;
  290 + if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  291 + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
  292 + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) {
  293 + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  294 + }
  295 +
282 296 const entityData: EntityData = {
283 297 entityId: {
284 298 id: NULL_UUID,
... ... @@ -289,7 +303,7 @@ export class EntityDataSubscription {
289 303 };
290 304 const name = DatasourceType.function;
291 305 entityData.latest[EntityKeyType.ENTITY_FIELD] = {
292   - name: {ts: Date.now(), value: name}
  306 + name: {ts: Date.now() + tsOffset, value: name}
293 307 };
294 308 const pageData: PageData<EntityData> = {
295 309 data: [entityData],
... ... @@ -299,7 +313,9 @@ export class EntityDataSubscription {
299 313 };
300 314 this.onPageData(pageData);
301 315 } else if (this.datasourceType === DatasourceType.entityCount) {
  316 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
302 317 this.subscriber = new TelemetrySubscriber(this.telemetryService);
  318 + this.subscriber.setTsOffset(this.latestTsOffset);
303 319 this.countCommand = new EntityCountCmd();
304 320 let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
305 321 if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
... ... @@ -332,13 +348,13 @@ export class EntityDataSubscription {
332 348 latest: {
333 349 [EntityKeyType.ENTITY_FIELD]: {
334 350 name: {
335   - ts: Date.now(),
  351 + ts: Date.now() + this.latestTsOffset,
336 352 value: DatasourceType.entityCount
337 353 }
338 354 },
339 355 [EntityKeyType.COUNT]: {
340 356 [countKey.name]: {
341   - ts: Date.now(),
  357 + ts: Date.now() + this.latestTsOffset,
342 358 value: entityCountUpdate.count + ''
343 359 }
344 360 }
... ... @@ -359,7 +375,7 @@ export class EntityDataSubscription {
359 375 latest: {
360 376 [EntityKeyType.COUNT]: {
361 377 [countKey.name]: {
362   - ts: Date.now(),
  378 + ts: Date.now() + this.latestTsOffset,
363 379 value: entityCountUpdate.count + ''
364 380 }
365 381 }
... ... @@ -384,6 +400,7 @@ export class EntityDataSubscription {
384 400 return;
385 401 }
386 402 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  403 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
387 404 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
388 405 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
389 406 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
... ... @@ -394,22 +411,38 @@ export class EntityDataSubscription {
394 411 if (this.datasourceType === DatasourceType.entity) {
395 412 this.subsCommand = new EntityDataCmd();
396 413 this.subsCommand.cmdId = this.dataCommand.cmdId;
397   - this.prepareSubscriptionCommands();
398   - if (!this.subsCommand.isEmpty()) {
  414 + this.prepareSubscriptionCommands(this.subsCommand);
  415 + let latestTsOffsetChanged = false;
  416 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  417 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
  418 + } else {
  419 + latestTsOffsetChanged = this.subscriber.setTsOffset(this.latestTsOffset);
  420 + }
  421 + if (latestTsOffsetChanged) {
  422 + if (this.listener.initialPageDataChanged) {
  423 + this.listener.initialPageDataChanged(this.pageData);
  424 + }
  425 + } else if (!this.subsCommand.isEmpty()) {
399 426 this.subscriber.subscriptionCommands = [this.subsCommand];
400 427 this.subscriber.update();
401 428 }
  429 + } else if (this.datasourceType === DatasourceType.entityCount) {
  430 + if (this.subscriber.setTsOffset(this.latestTsOffset)) {
  431 + if (this.listener.initialPageDataChanged) {
  432 + this.listener.initialPageDataChanged(this.pageData);
  433 + }
  434 + }
402 435 } else if (this.datasourceType === DatasourceType.function) {
403 436 this.startFunction();
404 437 }
405 438 this.started = true;
406 439 }
407 440
408   - private prepareSubscriptionCommands() {
  441 + private prepareSubscriptionCommands(cmd: EntityDataCmd) {
409 442 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
410 443 if (this.tsFields.length > 0) {
411 444 if (this.history) {
412   - this.subsCommand.historyCmd = {
  445 + cmd.historyCmd = {
413 446 keys: this.tsFields.map(key => key.key),
414 447 startTs: this.subsTw.fixedWindow.startTimeMs,
415 448 endTs: this.subsTw.fixedWindow.endTimeMs,
... ... @@ -419,7 +452,7 @@ export class EntityDataSubscription {
419 452 fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
420 453 };
421 454 } else {
422   - this.subsCommand.tsCmd = {
  455 + cmd.tsCmd = {
423 456 keys: this.tsFields.map(key => key.key),
424 457 startTs: this.subsTw.startTs,
425 458 timeWindow: this.subsTw.aggregation.timeWindow,
... ... @@ -430,10 +463,9 @@ export class EntityDataSubscription {
430 463 };
431 464 }
432 465 }
433   - this.subscriber.setTsOffset(this.subsTw.tsOffset);
434 466 } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
435 467 if (this.latestValues.length > 0) {
436   - this.subsCommand.latestCmd = {
  468 + cmd.latestCmd = {
437 469 keys: this.latestValues
438 470 };
439 471 }
... ... @@ -783,7 +815,7 @@ export class EntityDataSubscription {
783 815 } else {
784 816 prevSeries = [0, 0];
785 817 }
786   - const time = Date.now();
  818 + const time = Date.now() + this.latestTsOffset;
787 819 const value = dataKey.func(time, prevSeries[1]);
788 820 const series: [number, any] = [time, value];
789 821 this.datasourceData[0][dataKey.key].data = [series];
... ... @@ -838,6 +870,10 @@ export class EntityDataSubscription {
838 870 endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
839 871 this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
840 872 }
  873 + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) {
  874 + const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  875 + endTime = Math.min(currentTime, endTime);
  876 + }
841 877 }
842 878 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
843 879 }
... ...
... ... @@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs';
32 32 export interface EntityDataListener {
33 33 subscriptionType: widgetType;
34 34 subscriptionTimewindow?: SubscriptionTimewindow;
  35 + latestTsOffset?: number;
35 36 configDatasource: Datasource;
36 37 configDatasourceIndex: number;
37 38 dataLoaded: (pageData: PageData<EntityData>,
... ... @@ -92,6 +93,8 @@ export class EntityDataService {
92 93 if (listener.subscription) {
93 94 if (listener.subscriptionType === widgetType.timeseries) {
94 95 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  96 + } else if (listener.subscriptionType === widgetType.latest) {
  97 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
95 98 }
96 99 listener.subscription.start();
97 100 }
... ... @@ -118,6 +121,8 @@ export class EntityDataService {
118 121 listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
119 122 if (listener.subscriptionType === widgetType.timeseries) {
120 123 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  124 + } else if (listener.subscriptionType === widgetType.latest) {
  125 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
121 126 }
122 127 return listener.subscription.subscribe();
123 128 }
... ...
... ... @@ -39,8 +39,10 @@ import { HttpErrorResponse } from '@angular/common/http';
39 39 import {
40 40 calculateIntervalEndTime,
41 41 calculateIntervalStartTime,
  42 + calculateTsOffset,
42 43 createSubscriptionTimewindow,
43   - createTimewindowForComparison, getCurrentTime,
  44 + createTimewindowForComparison,
  45 + getCurrentTime,
44 46 SubscriptionTimewindow,
45 47 Timewindow,
46 48 toHistoryTimewindow,
... ... @@ -79,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription {
79 81 timeWindow: WidgetTimewindow;
80 82 originalTimewindow: Timewindow;
81 83 timeWindowConfig: Timewindow;
  84 + timezone: string;
82 85 subscriptionTimewindow: SubscriptionTimewindow;
83 86 useDashboardTimewindow: boolean;
  87 + tsOffset = 0;
84 88
85 89 hasDataPageLink: boolean;
86 90 singleEntity: boolean;
... ... @@ -213,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription {
213 217 this.timeWindow = {};
214 218 this.useDashboardTimewindow = options.useDashboardTimewindow;
215 219 this.stateData = options.stateData;
  220 + if (this.type === widgetType.latest) {
  221 + this.timezone = options.dashboardTimewindow.timezone;
  222 + this.updateTsOffset();
  223 + }
216 224 if (this.useDashboardTimewindow) {
217 225 this.timeWindowConfig = deepClone(options.dashboardTimewindow);
218 226 } else {
... ... @@ -578,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription {
578 586 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
579 587 this.timeWindowConfig = deepClone(newDashboardTimewindow);
580 588 this.update();
581   - return true;
  589 + }
  590 + }
  591 + } else if (this.type === widgetType.latest) {
  592 + if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
  593 + this.timezone = newDashboardTimewindow.timezone;
  594 + if (this.updateTsOffset()) {
  595 + this.update();
582 596 }
583 597 }
584 598 }
585   - return false;
586 599 }
587 600
588 601 updateDataVisibility(index: number): void {
... ... @@ -815,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription {
815 828 configDatasource: datasource,
816 829 configDatasourceIndex: datasourceIndex,
817 830 subscriptionTimewindow: this.subscriptionTimewindow,
  831 + latestTsOffset: this.tsOffset,
818 832 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
819 833 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true);
820 834 },
... ... @@ -882,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription {
882 896 }
883 897
884 898 private dataSubscribe() {
  899 + this.updateDataTimewindow();
885 900 if (!this.hasDataPageLink) {
886   - if (this.type === widgetType.timeseries && this.timeWindowConfig) {
887   - this.updateDataTimewindow();
888   - if (this.subscriptionTimewindow.fixedWindow) {
  901 + if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
889 902 this.onDataUpdated();
890   - }
891 903 }
892 904 const forceUpdate = !this.datasources.length;
  905 + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length;
893 906 this.entityDataListeners.forEach((listener) => {
894 907 if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
895 908 listener.subscriptionTimewindow = this.timewindowForComparison;
896 909 } else {
897 910 listener.subscriptionTimewindow = this.subscriptionTimewindow;
  911 + listener.latestTsOffset = this.tsOffset;
898 912 }
899 913 this.ctx.entityDataService.startSubscription(listener);
900 914 });
901 915 if (forceUpdate) {
902 916 this.onDataUpdated();
903 917 }
  918 + if (notifyDataLoaded) {
  919 + this.notifyDataLoaded();
  920 + }
904 921 }
905 922 }
906 923
... ... @@ -1102,6 +1119,15 @@ export class WidgetSubscription implements IWidgetSubscription {
1102 1119 }
1103 1120 }
1104 1121
  1122 + private updateTsOffset(): boolean {
  1123 + const newOffset = calculateTsOffset(this.timezone);
  1124 + if (this.tsOffset !== newOffset) {
  1125 + this.tsOffset = newOffset;
  1126 + return true;
  1127 + }
  1128 + return false;
  1129 + }
  1130 +
1105 1131 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow {
1106 1132 if (subscriptionTimewindow) {
1107 1133 this.subscriptionTimewindow = subscriptionTimewindow;
... ...
... ... @@ -133,7 +133,7 @@
133 133 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
134 134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
135 135 </section>
136   - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval">
  136 + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
137 137 <div class="limit-slider-container"
138 138 fxLayout="row" fxLayoutAlign="start center">
139 139 <span translate>aggregation.limit</span>
... ... @@ -184,7 +184,8 @@
184 184 (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
185 185 </section>
186 186 <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
187   - formControlName="timezone">
  187 + localBrowserTimezonePlaceholderOnEmpty="true"
  188 + formControlName="timezone">
188 189 </tb-timezone-select>
189 190 </div>
190 191 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
... ...
... ... @@ -23,7 +23,8 @@ import { AppState } from '@app/core/core.state';
23 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { coerceBooleanProperty } from '@angular/cdk/coercion';
25 25 import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
26   -import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
  26 +import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
  27 +import { deepClone } from '@core/utils';
27 28
28 29 @Component({
29 30 selector: 'tb-timezone-select',
... ... @@ -68,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
68 69 this.userTimezoneByDefaultValue = coerceBooleanProperty(value);
69 70 }
70 71
  72 + private localBrowserTimezonePlaceholderOnEmptyValue: boolean;
  73 + get localBrowserTimezonePlaceholderOnEmpty(): boolean {
  74 + return this.localBrowserTimezonePlaceholderOnEmptyValue;
  75 + }
  76 + @Input()
  77 + set localBrowserTimezonePlaceholderOnEmpty(value: boolean) {
  78 + this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value);
  79 + }
  80 +
71 81 @Input()
72 82 disabled: boolean;
73 83
... ... @@ -81,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
81 91
82 92 private dirty = false;
83 93
  94 + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
  95 +
  96 + private timezones: Array<TimezoneInfo>;
  97 +
84 98 private propagateChange = (v: any) => { };
85 99
86 100 constructor(private store: Store<AppState>,
... ... @@ -146,7 +160,11 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
146 160 }
147 161 } else {
148 162 this.modelValue = null;
149   - this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
  163 + if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
  164 + this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false});
  165 + } else {
  166 + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
  167 + }
150 168 }
151 169 this.dirty = true;
152 170 }
... ... @@ -162,11 +180,17 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
162 180 if (this.ignoreClosePanel) {
163 181 this.ignoreClosePanel = false;
164 182 } else {
165   - if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) {
166   - const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
167   - if (defaultTimezoneInfo !== null) {
  183 + if (!this.modelValue) {
  184 + if (this.defaultTimezoneId || this.userTimezoneByDefaultValue) {
  185 + const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
  186 + if (defaultTimezoneInfo !== null) {
  187 + this.ngZone.run(() => {
  188 + this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
  189 + });
  190 + }
  191 + } else if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
168 192 this.ngZone.run(() => {
169   - this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
  193 + this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true});
170 194 });
171 195 }
172 196 }
... ... @@ -187,10 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
187 211 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
188 212 this.searchText = searchText;
189 213 if (searchText && searchText.length) {
190   - return of(getTimezones().filter((timezoneInfo) =>
  214 + return of(this.loadTimezones().filter((timezoneInfo) =>
191 215 timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
192 216 }
193   - return of(getTimezones());
  217 + return of(this.loadTimezones());
194 218 }
195 219
196 220 clear() {
... ... @@ -200,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
200 224 }, 0);
201 225 }
202 226
  227 + private loadTimezones(): Array<TimezoneInfo> {
  228 + if (!this.timezones) {
  229 + this.timezones = [];
  230 + if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
  231 + this.timezones.push(this.getLocalBrowserTimezoneInfoPlaceholder());
  232 + }
  233 + this.timezones.push(...getTimezones());
  234 + }
  235 + return this.timezones;
  236 + }
  237 +
  238 + private getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
  239 + if (!this.localBrowserTimezoneInfoPlaceholder) {
  240 + this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
  241 + this.localBrowserTimezoneInfoPlaceholder.id = null;
  242 + this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
  243 + }
  244 + return this.localBrowserTimezoneInfoPlaceholder;
  245 + }
203 246 }
... ...
... ... @@ -30,6 +30,9 @@ import {
30 30 TsValue
31 31 } from '@shared/models/query/query.models';
32 32 import { PageData } from '@shared/models/page/page-data';
  33 +import { alarmFields } from '@shared/models/alarm.models';
  34 +import { entityFields } from '@shared/models/entity.models';
  35 +import { isUndefined } from '@core/utils';
33 36
34 37 export enum DataKeyType {
35 38 timeseries = 'timeseries',
... ... @@ -446,7 +449,9 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
446 449 for (const key of Object.keys(entityData.timeseries)) {
447 450 const tsValues = entityData.timeseries[key];
448 451 for (const tsValue of tsValues) {
449   - tsValue.ts += tsOffset;
  452 + if (tsValue.ts) {
  453 + tsValue.ts += tsOffset;
  454 + }
450 455 }
451 456 }
452 457 }
... ... @@ -455,13 +460,17 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
455 460 const keyTypeValues = entityData.latest[entityKeyType];
456 461 for (const key of Object.keys(keyTypeValues)) {
457 462 const tsValue = keyTypeValues[key];
458   - tsValue.ts += tsOffset;
  463 + if (tsValue.ts) {
  464 + tsValue.ts += tsOffset;
  465 + }
  466 + if (key === entityFields.createdTime.keyName && tsValue.value) {
  467 + tsValue.value = (Number(tsValue.value) + tsOffset) + '';
  468 + }
459 469 }
460 470 }
461 471 }
462 472 }
463 473 }
464   -
465 474 }
466 475
467 476 export class AlarmDataUpdate extends DataUpdate<AlarmData> {
... ... @@ -473,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
473 482 this.allowedEntities = msg.allowedEntities;
474 483 this.totalEntities = msg.totalEntities;
475 484 }
  485 +
  486 + public prepareData(tsOffset: number) {
  487 + if (this.data) {
  488 + this.processAlarmData(this.data.data, tsOffset);
  489 + }
  490 + if (this.update) {
  491 + this.processAlarmData(this.update, tsOffset);
  492 + }
  493 + }
  494 +
  495 + private processAlarmData(data: Array<AlarmData>, tsOffset: number) {
  496 + for (const alarmData of data) {
  497 + alarmData.createdTime += tsOffset;
  498 + if (alarmData.ackTs) {
  499 + alarmData.ackTs += tsOffset;
  500 + }
  501 + if (alarmData.clearTs) {
  502 + alarmData.clearTs += tsOffset;
  503 + }
  504 + if (alarmData.endTs) {
  505 + alarmData.endTs += tsOffset;
  506 + }
  507 + if (alarmData.latest) {
  508 + for (const entityKeyType of Object.keys(alarmData.latest)) {
  509 + const keyTypeValues = alarmData.latest[entityKeyType];
  510 + for (const key of Object.keys(keyTypeValues)) {
  511 + const tsValue = keyTypeValues[key];
  512 + if (tsValue.ts) {
  513 + tsValue.ts += tsOffset;
  514 + }
  515 + if (key in [entityFields.createdTime.keyName,
  516 + alarmFields.startTime.keyName,
  517 + alarmFields.endTime.keyName,
  518 + alarmFields.ackTime.keyName,
  519 + alarmFields.clearTime.keyName] && tsValue.value) {
  520 + tsValue.value = (Number(tsValue.value) + tsOffset) + '';
  521 + }
  522 + }
  523 + }
  524 + }
  525 + }
  526 + }
476 527 }
477 528
478 529 export class EntityCountUpdate extends CmdUpdate {
... ... @@ -500,7 +551,7 @@ export class TelemetrySubscriber {
500 551
501 552 private zone: NgZone;
502 553
503   - private tsOffset = 0;
  554 + private tsOffset = undefined;
504 555
505 556 public subscriptionCommands: Array<WebsocketCmd>;
506 557
... ... @@ -556,8 +607,14 @@ export class TelemetrySubscriber {
556 607 this.reconnectSubject.complete();
557 608 }
558 609
559   - public setTsOffset(tsOffset: number) {
560   - this.tsOffset = tsOffset;
  610 + public setTsOffset(tsOffset: number): boolean {
  611 + if (this.tsOffset !== tsOffset) {
  612 + const changed = !isUndefined(this.tsOffset);
  613 + this.tsOffset = tsOffset;
  614 + return changed;
  615 + } else {
  616 + return false;
  617 + }
561 618 }
562 619
563 620 public onData(message: SubscriptionUpdate) {
... ... @@ -598,6 +655,9 @@ export class TelemetrySubscriber {
598 655 }
599 656
600 657 public onAlarmData(message: AlarmDataUpdate) {
  658 + if (this.tsOffset) {
  659 + message.prepareData(this.tsOffset);
  660 + }
601 661 if (this.zone) {
602 662 this.zone.run(
603 663 () => {
... ...
... ... @@ -318,6 +318,16 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
318 318 return historyTimewindow;
319 319 }
320 320
  321 +export function calculateTsOffset(timezone?: string): number {
  322 + if (timezone) {
  323 + const tz = getTimezone(timezone);
  324 + const localOffset = moment().utcOffset();
  325 + return (tz.utcOffset() - localOffset) * 60 * 1000;
  326 + } else {
  327 + return 0;
  328 + }
  329 +}
  330 +
321 331 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
322 332 timeService: TimeService): SubscriptionTimewindow {
323 333 const subscriptionTimewindow: SubscriptionTimewindow = {
... ... @@ -329,13 +339,8 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
329 339 type: AggregationType.AVG
330 340 },
331 341 timezone: timewindow.timezone,
332   - tsOffset: 0
  342 + tsOffset: calculateTsOffset(timewindow.timezone)
333 343 };
334   - if (timewindow.timezone) {
335   - const tz = getTimezone(timewindow.timezone);
336   - const localOffset = moment().utcOffset();
337   - subscriptionTimewindow.tsOffset = (tz.utcOffset() - localOffset) * 60 * 1000;
338   - }
339 344 let aggTimewindow = 0;
340 345 if (stateData) {
341 346 subscriptionTimewindow.aggregation.type = AggregationType.NONE;
... ... @@ -407,6 +412,7 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
407 412 endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
408 413 };
409 414 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  415 + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval;
410 416 } else {
411 417 subscriptionTimewindow.fixedWindow = {
412 418 startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
... ... @@ -768,6 +774,11 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string,
768 774 return foundTimezone;
769 775 }
770 776
  777 +export function getDefaultTimezoneInfo(): TimezoneInfo {
  778 + const userTimezone = getDefaultTimezone();
  779 + return getTimezoneInfo(userTimezone);
  780 +}
  781 +
771 782 export function getDefaultTimezone(): string {
772 783 if (!defaultTimezone) {
773 784 defaultTimezone = monentTz.tz.guess();
... ...
... ... @@ -1988,7 +1988,8 @@
1988 1988 "timezone": "Timezone",
1989 1989 "select-timezone": "Select timezone",
1990 1990 "no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
1991   - "timezone-required": "Timezone is required."
  1991 + "timezone-required": "Timezone is required.",
  1992 + "browser-time": "Browser Time"
1992 1993 },
1993 1994 "queue": {
1994 1995 "select_name": "Select queue name",
... ...