@@ -348,6 +348,14 @@ export class WidgetUniversal extends WidgetGeneric<WidgetUniversalState, WidgetU
348348 private opacityHandler : StateChangeListener | null = null ;
349349 private iconHandlers : ( StateChangeListener | null ) [ ] = [ ] ;
350350
351+ /**
352+ * Type used to convert the primary raw value. Kept in an instance field (not state) so the
353+ * value handler reads it synchronously, independent of React's async setState.
354+ */
355+ private primaryCommonType : ioBroker . CommonType = 'mixed' ;
356+ /** Last raw value received for the primary state, so it can be re-derived once the type is known */
357+ private lastPrimaryRaw : ioBroker . StateValue | null | undefined = undefined ;
358+
351359 /** Cached object metadata keyed by state ID */
352360 private objectCache = new Map < string , ioBroker . StateObject > ( ) ;
353361
@@ -587,16 +595,15 @@ export class WidgetUniversal extends WidgetGeneric<WidgetUniversalState, WidgetU
587595
588596 // Primary value
589597 if ( this . props . settings . stateId ) {
598+ const stateId = this . props . settings . stateId ;
599+ // Reset per-subscription state (e.g. when the stateId changed)
600+ this . primaryCommonType = 'mixed' ;
601+ this . lastPrimaryRaw = undefined ;
602+
590603 this . primaryHandler = ( _id , state ) => {
591604 const val = state ?. val ;
592- let value : number | string | boolean | null ;
593- if ( this . state . commonType === 'boolean' ) {
594- value = val === true || val === 'true' || val === 1 || val === '1' || val === 'ON' || val === 'AN' ;
595- } else if ( this . state . commonType === 'number' ) {
596- value = val != null ? Number ( val ) : null ;
597- } else {
598- value = ( val || '' ) . toString ( ) ;
599- }
605+ this . lastPrimaryRaw = val ;
606+ const value = this . convertPrimaryValue ( val ) ;
600607 this . setState ( { value } ) ;
601608 // Capture immediately on every state change when QuickChart is on,
602609 // so the chart populates without waiting for the next interval tick.
@@ -608,18 +615,40 @@ export class WidgetUniversal extends WidgetGeneric<WidgetUniversalState, WidgetU
608615 this . captureQuickSample ( value ) ;
609616 }
610617 } ;
611- ctx . getState ( this . props . settings . stateId , this . primaryHandler ) ;
612- void this . getCachedObject ( this . props . settings . stateId ) . then ( obj => {
613- if ( obj ?. common ?. unit ) {
614- this . setState ( {
615- unit : obj . common . unit ,
616- commonType : obj . common . type ,
617- commonName : this . getText ( obj . common . name ) ,
618- } ) ;
619- } else if ( obj ?. common ?. type ) {
620- this . setState ( { commonType : obj . common . type , commonName : this . getText ( obj . common . name ) } ) ;
618+
619+ // Option 2: resolve the object (→ commonType) FIRST, then subscribe to the value, so the
620+ // very first value is already converted with the correct type instead of being stringified raw.
621+ void ( async ( ) => {
622+ try {
623+ const obj = await this . getCachedObject ( stateId ) ;
624+ if ( obj ?. common ?. type ) {
625+ this . primaryCommonType = obj . common . type ;
626+ if ( obj . common . unit ) {
627+ this . setState ( {
628+ unit : obj . common . unit ,
629+ commonType : obj . common . type ,
630+ commonName : this . getText ( obj . common . name ) ,
631+ } ) ;
632+ } else {
633+ this . setState ( {
634+ commonType : obj . common . type ,
635+ commonName : this . getText ( obj . common . name ) ,
636+ } ) ;
637+ }
638+ // Option 3: a value may already have arrived before the type was known —
639+ // re-derive it now with the correct type so it gets formatted properly.
640+ if ( this . lastPrimaryRaw !== undefined ) {
641+ this . setState ( { value : this . convertPrimaryValue ( this . lastPrimaryRaw ) } ) ;
642+ }
643+ }
644+ } catch {
645+ // Object not found / fetch failed — fall back to 'mixed' handling
621646 }
622- } ) ;
647+ // Subscribe only after we attempted to resolve the type (guard against a fast unmount)
648+ if ( this . primaryHandler ) {
649+ ctx . getState ( stateId , this . primaryHandler ) ;
650+ }
651+ } ) ( ) ;
623652 }
624653
625654 // Secondary value
@@ -807,6 +836,17 @@ export class WidgetUniversal extends WidgetGeneric<WidgetUniversalState, WidgetU
807836 return undefined ;
808837 }
809838
839+ /** Convert a raw state value into the typed value used for display, based on the resolved common type. */
840+ private convertPrimaryValue ( val : ioBroker . StateValue | null | undefined ) : number | string | boolean | null {
841+ if ( this . primaryCommonType === 'boolean' ) {
842+ return val === true || val === 'true' || val === 1 || val === '1' || val === 'ON' || val === 'AN' ;
843+ }
844+ if ( this . primaryCommonType === 'number' ) {
845+ return val != null ? Number ( val ) : null ;
846+ }
847+ return ( val ?? '' ) . toString ( ) ;
848+ }
849+
810850 private formatValue ( val : number | boolean | string , hasIcon : boolean ) : string | null {
811851 if ( typeof val === 'boolean' ) {
812852 if ( val ) {
0 commit comments