Explorar o código

feat(devices): add Novadigital MidPanel8 control panel

- add new language selector translations for English and French variants and Hindi
- add metadata support to media_player platform
  Rather than implementing every read-only property individually, this
  overrides the on_receive to set attributes in the parent class for
  these read-only attributes. Setting media_position the way automatically
  sets the corresponding timestamp.

Issue #6129
Jason Rumney hai 13 horas
pai
achega
2d209a9529
Modificáronse 26 ficheiros con 779 adicións e 12 borrados
  1. 559 0
      custom_components/tuya_local/devices/novadigital_midpanel8_controller.yaml
  2. 31 12
      custom_components/tuya_local/media_player.py
  3. 7 0
      custom_components/tuya_local/translations/bg.json
  4. 7 0
      custom_components/tuya_local/translations/ca.json
  5. 7 0
      custom_components/tuya_local/translations/cs.json
  6. 7 0
      custom_components/tuya_local/translations/de.json
  7. 7 0
      custom_components/tuya_local/translations/el.json
  8. 7 0
      custom_components/tuya_local/translations/en.json
  9. 7 0
      custom_components/tuya_local/translations/es.json
  10. 7 0
      custom_components/tuya_local/translations/fr.json
  11. 7 0
      custom_components/tuya_local/translations/hu.json
  12. 7 0
      custom_components/tuya_local/translations/id.json
  13. 7 0
      custom_components/tuya_local/translations/it.json
  14. 7 0
      custom_components/tuya_local/translations/ja.json
  15. 7 0
      custom_components/tuya_local/translations/no-NB.json
  16. 7 0
      custom_components/tuya_local/translations/pl.json
  17. 7 0
      custom_components/tuya_local/translations/pt-BR.json
  18. 7 0
      custom_components/tuya_local/translations/pt-PT.json
  19. 7 0
      custom_components/tuya_local/translations/ro.json
  20. 7 0
      custom_components/tuya_local/translations/ru.json
  21. 7 0
      custom_components/tuya_local/translations/sv.json
  22. 7 0
      custom_components/tuya_local/translations/uk.json
  23. 7 0
      custom_components/tuya_local/translations/ur.json
  24. 7 0
      custom_components/tuya_local/translations/zh-Hans.json
  25. 7 0
      custom_components/tuya_local/translations/zh-Hant.json
  26. 28 0
      tests/test_media_player.py

+ 559 - 0
custom_components/tuya_local/devices/novadigital_midpanel8_controller.yaml

@@ -0,0 +1,559 @@
+name: Control panel
+products:
+  - id: o4h9aftufavvqjcm
+    manufacturer: Nova Digital
+    model: MidPanel 8
+entities:
+  - entity: text
+    translation_key: scene
+    category: config
+    hidden: true
+    dps:
+      - id: 1
+        type: string
+        optional: true
+        name: value
+  - entity: text
+    name: Device
+    category: config
+    icon: "mdi:devices"
+    hidden: true
+    dps:
+      - id: 2
+        type: string
+        optional: true
+        name: value
+  - entity: switch
+    translation_key: switch_x
+    translation_placeholders:
+      x: "1"
+    dps:
+      - id: 3
+        type: boolean
+        name: switch
+  - entity: switch
+    translation_key: switch_x
+    translation_placeholders:
+      x: "2"
+    dps:
+      - id: 4
+        type: boolean
+        name: switch
+  - entity: media_player
+    dps:
+      - id: 5
+        type: boolean
+        optional: true
+        name: mute
+      - id: 6
+        type: string
+        name: source
+        optional: true
+        mapping:
+          - dps_val: cloud
+            value: Cloud
+          - dps_val: local
+            value: Local
+          - dps_val: aux
+            value: Aux-In
+          - dps_val: bluetooth
+            value: Bluetooth
+      - id: 7
+        type: json
+        name: playback_info
+      - id: 8
+        type: string
+        optional: true
+        name: shuffle
+        mapping:
+          - dps_val: random
+            value: true
+          - dps_val: order
+            value: false
+          - value: false
+            hidden: true
+      - id: 8
+        type: string
+        optional: true
+        name: repeat
+        mapping:
+          - dps_val: repeat_one
+            value: one
+          - dps_val: repeat_all
+            value: all
+          - dps_val: order
+            value: "off"
+          - value: "off"
+            hidden: true
+      - id: 9
+        type: string
+        optional: true
+        name: sound_mode
+        mapping:
+          - dps_val: normal
+            value: Normal
+          - dps_val: pop
+            value: Pop
+          - dps_val: opera
+            value: Opera
+          - dps_val: classical
+            value: Classical
+          - dps_val: jazz
+            value: Jazz
+          - dps_val: rock
+            value: Rock
+          - dps_val: folk
+            value: Folk
+          - dps_val: heavy_metal
+            value: Heavy metal
+          - dps_val: hip_hop
+            value: Hip hop
+          - dps_val: wave
+            value: New wave
+      - id: 12
+        type: integer
+        optional: true
+        name: media_position
+      - id: 13
+        type: integer
+        optional: true
+        name: media_duration
+  - entity: number
+    name: Treble
+    icon: "mdi:music-clef-treble"
+    dps:
+      - id: 10
+        type: integer
+        optional: true
+        name: value
+        range:
+          min: 0
+          max: 15
+  - entity: number
+    name: Bass
+    icon: "mdi:music-clef-bass"
+    dps:
+      - id: 11
+        type: integer
+        optional: true
+        name: value
+        range:
+          min: 0
+          max: 15
+  - entity: binary_sensor
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 14
+        type: bitfield
+        optional: true
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - dps_val: null
+            value: false
+          - value: true
+      - id: 14
+        type: bitfield
+        optional: true
+        name: fault_code
+      - id: 17
+        type: boolean
+        name: play
+      - id: 80
+        type: integer
+        name: volume
+        range:
+          min: 0
+          max: 100
+        mapping:
+          - scale: 100
+      - id: 82
+        type: boolean
+        optional: true
+        name: pause
+      - id: 85
+        type: boolean
+        optional: true
+        name: prev
+      - id: 86
+        type: boolean
+        optional: true
+        name: next
+  - entity: switch
+    name: Welcome message
+    category: config
+    icon: "mdi:message-arrow-left"
+    dps:
+      - id: 15
+        type: boolean
+        optional: true
+        name: switch
+  - entity: switch
+    name: Background music
+    icon: "mdi:music-note"
+    category: config
+    dps:
+      - id: 18
+        type: boolean
+        optional: true
+        name: switch
+  - entity: select
+    name: Preset
+    icon: "mdi:bookmark-music"
+    category: config
+    dps:
+      - id: 19
+        type: string
+        optional: true
+        name: option
+        mapping:
+          - dps_val: onlinemusic1
+            value: Cloud 1
+          - dps_val: onlinemusic2
+            value: Cloud 2
+          - dps_val: onlinemusic3
+            value: Cloud 3
+          - dps_val: onlinemusic4
+            value: Cloud 4
+          - dps_val: onlinemusic5
+            value: Cloud 5
+          - dps_val: onlinemusic6
+            value: Cloud 6
+          - dps_val: localmusic1
+            value: Local 1
+          - dps_val: localmusic2
+            value: Local 2
+          - dps_val: localmusic3
+            value: Local 3
+          - dps_val: localmusic4
+            value: Local 4
+          - dps_val: localmusic5
+            value: Local 5
+          - dps_val: localmusic6
+            value: Local 6
+          - dps_val: localmusic7
+            value: Local 7
+          - dps_val: localmusic8
+            value: Local 8
+          - dps_val: localmusic9
+            value: Local 9
+          - dps_val: localmusic10
+            value: Local 10
+          - dps_val: localmusic11
+            value: Local 11
+          - dps_val: localmusic12
+            value: Local 12
+  - entity: text
+    name: Voice settings
+    category: config
+    icon: "mdi:message-settings"
+    hidden: true
+    dps:
+      - id: 75
+        type: json
+        name: value
+  - entity: text
+    name: Alexa client secret
+    category: config
+    icon: "mdi:form-textbox-password"
+    hidden: true
+    dps:
+      - id: 76
+        type: string
+        optional: true
+        sensitive: true
+        name: value
+  - entity: select
+    name: Alexa login method
+    icon: "mdi:login"
+    category: config
+    hidden: true
+    dps:
+      - id: 77
+        type: string
+        name: option
+        mapping:
+          - dps_val: login_website
+            value: Web
+          - dps_val: login_app
+            value: App
+      - id: 90
+        type: string
+        name: product
+      - id: 91
+        type: string
+        name: serialnumber
+        sensitive: true
+      - id: 92
+        type: string
+        name: code_challenge
+        sensitive: true
+      - id: 94
+        type: string
+        optional: true
+        name: redirect_url
+      - id: 95
+        type: string
+        optional: true
+        sensitive: true
+        name: client_id
+      - id: 96
+        type: string
+        optional: true
+        name: alexa_status
+      - id: 97
+        type: string
+        name: binding_status
+  - entity: button
+    name: Wake
+    icon: "mdi:sleep-off"
+    dps:
+      - id: 78
+        type: boolean
+        optional: true
+        name: button
+  - entity: select
+    name: Alexa wakeword language
+    translation_key: language
+    category: config
+    hidden: true
+    dps:
+      - id: 79
+        type: string
+        optional: true
+        name: option
+        mapping:
+          - dps_val: english_us
+            value: english_us
+          - dps_val: portuguese_br
+            value: portuguese
+          - dps_val: english_au
+            value: english_au
+          - dps_val: french_ca
+            value: french_ca
+          - dps_val: french_fr
+            value: french
+          - dps_val: german_de
+            value: german
+          - dps_val: english_uk
+            value: english
+          - dps_val: english_in
+            value: english_in
+          - dps_val: spanish_es
+            value: spanish
+          - dps_val: spanish_mx
+            value: latin
+          - dps_val: italian_it
+            value: italian
+          - dps_val: japanese_jp
+            value: japanese
+  - entity: switch
+    name: Privacy mode
+    icon: "mdi:microphone-off"
+    category: config
+    dps:
+      - id: 81
+        type: boolean
+        name: switch
+  - entity: select
+    name: Speaker mode
+    icon: "mdi:speaker-message"
+    category: config
+    dps:
+      - id: 83
+        type: string
+        optional: true
+        name: option
+        mapping:
+          - dps_val: smart_speaker
+            value: Smart speaker
+          - dps_val: bluetooth
+            value: Bluetooth
+  - entity: switch
+    name: Bluetooth
+    category: config
+    icon: "mdi:bluetooth"
+    dps:
+      - id: 84
+        type: boolean
+        optional: true
+        name: switch
+  - entity: select
+    name: Smart speaker playback mode
+    category: config
+    dps:
+      - id: 87
+        type: string
+        optional: true
+        name: option
+        mapping:
+          - dps_val: random
+            value: Shuffle
+            icon: "mdi:shuffle"
+          - dps_val: order
+            value: Normal
+            icon: "mdi:shuffle-disabled"
+          - dps_val: repeat_all
+            value: Repeat all
+            icon: "mdi:repeat"
+          - dps_val: repeat_one
+            value: Repeat track
+            icon: "mdi:repeat-once"
+  - entity: text
+    name: Alarm clock
+    category: config
+    icon: "mdi:alarm"
+    hidden: true
+    dps:
+      - id: 88
+        type: string
+        optional: true
+        name: value
+  - entity: text
+    name: Reminder
+    category: config
+    icon: "mdi:message-alert"
+    hidden: true
+    dps:
+      - id: 89
+        type: string
+        optional: true
+        name: value
+  - entity: text
+    name: Alexa auth code
+    category: config
+    icon: "mdi:form-textbox-password"
+    hidden: true
+    dps:
+      - id: 93
+        type: string
+        optional: true
+        sensitive: true
+        name: value
+  - entity: button
+    name: Alexa disconnect
+    category: config
+    icon: "mdi:cloud-remove"
+    hidden: true
+    dps:
+      - id: 98
+        type: boolean
+        optional: true
+        name: button
+  - entity: select
+    name: Alexa language
+    translation_key: language
+    category: config
+    hidden: true
+    dps:
+      - id: 99
+        type: string
+        optional: true
+        name: option
+        mapping:
+          - dps_val: english_au
+            value: english_au
+          - dps_val: english_ca
+            value: english_ca
+          - dps_val: english_in
+            value: english_in
+          - dps_val: english_uk
+            value: english
+          - dps_val: english_us
+            value: english_us
+          - dps_val: french_ca
+            value: french_ca
+          - dps_val: french_fr
+            value: french
+          - dps_val: german_de
+            value: german
+          - dps_val: hindi_in
+            value: hindi
+          - dps_val: italian_it
+            value: italian
+          - dps_val: japanese_jp
+            value: japanese
+          - dps_val: portuguese_br
+            value: portuguese
+          - dps_val: spanish_es
+            value: spanish
+          - dps_val: spanish_mx
+            value: latin
+          - dps_val: spanish_us
+            value: spanish_us
+  - entity: remote
+    dps:
+      - id: 201
+        name: send
+        type: string
+        optional: true
+      - id: 202
+        name: receive
+        type: string
+        optional: true
+        persist: false
+  - entity: infrared
+    dps:
+      - id: 201
+        type: string
+        optional: true
+        name: send
+  - entity: number
+    name: Voice volume
+    category: config
+    translation_key: volume
+    dps:
+      - id: 203
+        type: integer
+        name: value
+        unit: "%"
+        range:
+          min: 0
+          max: 100
+  - entity: switch
+    name: Voice mic
+    icon: "mdi:microphone"
+    category: config
+    dps:
+      - id: 204
+        type: boolean
+        optional: true
+        name: switch
+  - entity: switch
+    name: Voice play
+    category: config
+    dps:
+      - id: 205
+        type: boolean
+        name: switch
+        mapping:
+          - dps_val: false
+            icon: "mdi:pause-box"
+          - dps_val: true
+            icon: "mdi:play-box"
+  - entity: switch
+    name: Bluetooth playback
+    icon: "mdi:bluetooth-audio"
+    category: config
+    dps:
+      - id: 206
+        type: boolean
+        optional: true
+        name: switch
+  - entity: event
+    name: Notification
+    dps:
+      - id: 212
+        type: utf16b64
+        name: event
+        optional: true
+        mapping:
+          - dps_val: null
+            value: null
+          - value: notify
+      - id: 212
+        type: utf16b64
+        optional: true
+        name: message

+ 31 - 12
custom_components/tuya_local/media_player.py

@@ -9,6 +9,8 @@ from homeassistant.components.media_player import (
     MediaPlayerEntityFeature,
     MediaPlayerState,
 )
+from homeassistant.components.media_player.significant_change import PROP_TO_ATTR
+from homeassistant.util.dt import utcnow
 
 from .device import TuyaLocalDevice
 from .entity import TuyaLocalEntity
@@ -52,6 +54,14 @@ class TuyaLocalMediaPlayer(TuyaLocalEntity, MediaPlayerEntity):
         self._shuffle_dp = dps_map.pop("shuffle", None)
         self._repeat_dp = dps_map.pop("repeat", None)
         self._sound_mode_dp = dps_map.pop("sound_mode", None)
+        self._metadata = {}
+        for n, dp in dps_map:
+            if n in PROP_TO_ATTR:
+                self._metadata[dp.id] = dps_map.pop(n)
+
+        for dp in self._metadata.values():
+            dps_map.pop(dp.name, None)
+
         self._init_end(dps_map)
 
         self._support_flags = MediaPlayerEntityFeature(0)
@@ -93,6 +103,7 @@ class TuyaLocalMediaPlayer(TuyaLocalEntity, MediaPlayerEntity):
         # MEDIA_ENQUEUE for adding media to the queue
         # SEARCH_MEDIA for searching media on the device
 
+    @property
     def state(self):
         """Return the state of the media player."""
         if self._state_dp:
@@ -107,54 +118,49 @@ class TuyaLocalMediaPlayer(TuyaLocalEntity, MediaPlayerEntity):
             return MediaPlayerState.ON
         return None
 
-    def volume_level(self):
-        """Return the volume level of the media player (0..1)."""
-        if self._volume_dp:
-            return self._volume_dp.get_value(self._device)
-        return None
-
+    @property
     def volume_step(self):
         """Return the volume step of the media player."""
         if self._volume_dp:
             return self._volume_dp.step(self._device)
         return None
 
-    def is_volume_muted(self):
-        """Return True if the volume is muted."""
-        if self._mute_dp:
-            return self._mute_dp.get_value(self._device)
-        return None
-
+    @property
     def source(self):
         """Return the current input source of the media player."""
         if self._source_dp:
             return self._source_dp.get_value(self._device)
         return None
 
+    @property
     def source_list(self):
         """Return the list of available input sources of the media player."""
         if self._source_dp:
             return self._source_dp.values(self._device)
         return None
 
+    @property
     def sound_mode(self):
         """Return the current sound mode of the media player."""
         if self._sound_mode_dp:
             return self._sound_mode_dp.get_value(self._device)
         return None
 
+    @property
     def sound_mode_list(self):
         """Return the list of available sound modes of the media player."""
         if self._sound_mode_dp:
             return self._sound_mode_dp.values(self._device)
         return None
 
+    @property
     def shuffle(self):
         """Return the current shuffle state of the media player."""
         if self._shuffle_dp:
             return self._shuffle_dp.get_value(self._device)
         return None
 
+    @property
     def repeat(self):
         """Return the current repeat state of the media player."""
         if self._repeat_dp:
@@ -265,3 +271,16 @@ class TuyaLocalMediaPlayer(TuyaLocalEntity, MediaPlayerEntity):
             await self._repeat_dp.async_set_value(self._device, repeat)
         else:
             raise NotImplementedError()
+
+    def on_receive(self, dps, full_poll):
+        """Handle an update from the device."""
+        # Set metadata attributes
+        for d, v in dps:
+            if d in self._metadata:
+                setattr(self, _attr(self._metadata[d]), v)
+                if self._metadata[d].name == "media_position":
+                    setattr(self, "_attr_media_position_updated_at", utcnow())
+
+    def _attr(dp):
+        """Return the attribute name for a given datapoint."""
+        return f"_attr_{dp.name}"

+ 7 - 0
custom_components/tuya_local/translations/bg.json

@@ -705,6 +705,13 @@
                     "turkish": "Türkçe",
                     "polish": "Polski",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Изключено"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/ca.json

@@ -704,6 +704,13 @@
                     "thai": "ไทย",
                     "turkish": "Türkçe",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Desactivat"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/cs.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Vypnuto"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/de.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Aus"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/el.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Απενεργοποίηση"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/en.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Off"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/es.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Apagado"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/fr.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Désactivé"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/hu.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Ki"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/id.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Mati"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/it.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Spento"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/ja.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "オフ"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/no-NB.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Av"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/pl.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Wyłączony"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/pt-BR.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Desligado"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/pt-PT.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Desligado"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/ro.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Oprit"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/ru.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Выкл"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/sv.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Av"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/uk.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "Вимк"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/ur.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "آف"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/zh-Hans.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "关闭"
                 }
             },

+ 7 - 0
custom_components/tuya_local/translations/zh-Hant.json

@@ -704,6 +704,13 @@
                     "slovak": "Slovenčina",
                     "thai": "ไทย",
                     "vietnamese": "Tiếng Việt",
+                    "english_au": "English (Australia)",
+                    "english_ca": "English (Canada)",
+                    "english_in": "English (India)",
+                    "english_us": "English (United States)",
+                    "french_ca": "Français (Canada)",
+                    "hindi": "हिंदी",
+                    "spanish_us": "Español (Estados Unidos)",
                     "off": "關"
                 }
             },

+ 28 - 0
tests/test_media_player.py

@@ -124,3 +124,31 @@ class TestMediaPlayerState:
 
         # Test that the state is correct when the device is playing
         assert media_player.state == MediaPlayerState.PLAYING
+
+    async def async_test_metadata(self, hass, mocker):
+        """Test media metadata properties."""
+        dps = {"6": "local", "8": "order", "9": "pop", "12": 0, "13": 100}
+        entry = MockConfigEntry(
+            domain=DOMAIN,
+            data={
+                CONF_TYPE: "novadigital_midpanel8_controller",
+                CONF_DEVICE_ID: "dummy",
+                CONF_PROTOCOL_VERSION: "auto",
+            },
+        )
+        m_add_entities = Mock()
+        m_device = mock_device(dps, mocker)
+
+        hass.data[DOMAIN] = {"dummy": {"device": m_device}}
+
+        await async_setup_entry(hass, entry, m_add_entities)
+        media_player = hass.data[DOMAIN]["dummy"]["media_player"]
+        now = hass.helpers.event.dt_util.utcnow()
+        media_player.on_receive({"12": 123, "13": 345})
+        assert media_player.media_position == 123
+        assert (
+            media_player.media_position_updated_at >= now
+            and media_player.media_position_updated_at
+            <= hass.helpers.event.dt_util.utcnow()
+        )
+        assert media_player.media_duration == 345