# hass_devices.yaml
# Copyright (C) 2020-2025 Kedron Holdings LLC, All Rights Reserved
# This file is part of Reactor.
#
# *** IMPORTANT ***
# This is a system file. DO NOT place overrides or make modifications here.
# You can add or override implementation section items by specifying them
# in local.HassController.implementation in your site configuration.
# You can add or override mapping section items by specifying your data in
# local.HassController.mapping in your site configuration.
---
version: 25280
revision: 1
format: 1
copyright: Copyright (C) 2020-2025 Kedron Holdings LLC, All Rights Reserved
requires: 24269

# The implementation section describes how each capability is mapped from
# HomeAssistant into the Reactor model. The attributes subsection of each
# capability must contain a declaration for each attribute described in
# the system definition of the capability. The actions section defines how
# each capability's actions is mapped to a HomeAssistant service, and how
# its service data is mapped from the Reactor action's parameters and data.
# See the docs for more information.
implementation:
  power_switch:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
    actions:
      # Use the most generic service
      'on':
        hass_service: homeassistant.turn_on
      'off':
        hass_service: homeassistant.turn_off
      'set': true
  toggle:
    actions:
      toggle:
        hass_service: homeassistant.toggle
  dimming:
    attributes:
      level:
        expr: "round( (source.attributes?.brightness ?? 0) / 255, 2 )"
    actions:
      set:
        # If we're setting dimming level to 0, remap to direct off
        remap:
          - condition: (parameters?.level ?? 0) == 0
            action: power_switch.off
        hass_service: homeassistant.turn_on
        service_data:
          brightness:
            expr: "max( 0, min( 255, floor((parameters?.level ?? 0)*255 + 0.5) ) )"
      up: true
      down: true
  rgb_color:
    attributes:
      red:
        expr: "source.attributes.rgb_color?[0]"
      green:
        expr: "source.attributes.rgb_color?[1]"
      blue:
        expr: "source.attributes.rgb_color?[2]"
    actions:
      set_rgb:
        hass_service: light.turn_on
        service_data:
          rgb_color:
            expr: >
              [int(parameters.red), int(parameters.green), int(parameters.blue)]
      set:
        hass_service: light.turn_on
        service_data:
          rgb_color:
            expr: >
              [ (parameters.value>>16)&255,
                (parameters.value>>8)&255,
                parameters.value&255 ]
  hs_color:
    attributes:
      hue:
        expr: "source.attributes.hs_color?[0]"
      saturation:
        expr: "source.attributes.hs_color?[1] / 100"
    actions:
      set_hue:
        hass_service: light.turn_on
        service_data:
          hs_color:
            expr: >
              [ parameters.hue,
                floor( entity.attributes.hs_color.saturation * 100 ) ]
      set_saturation:
        hass_service: light.turn_on
        service_data:
          hs_color:
            expr: >
              [ entity.attributes.hs_color.hue,
                floor( parameters.level * 100 ) ]
      set_hs:
        hass_service: light.turn_on
        service_data:
          hs_color:
            expr: "[ parameters.hue, floor( parameters.saturation * 100 ) ]"
  xy_color:
    attributes:
      x:
        expr: "source.attributes.xy_color?[0]"
      y:
        expr: "source.attributes.xy_color?[1]"
    actions:
      set:
        hass_service: light.turn_on
        service_data:
          xy_color:
            expr: "[ parameters.x ?# 0, parameters.y ?# 0 ]"
  color_temperature:
    attributes:
      value:
        expr: >
          source.attributes?.kelvin ??
            ( 1e6 / source.attributes?.color_temp )
    actions:
      set:
        hass_service: light.turn_on
        service_data:
          color_temp:
            expr: floor( 1e6 / parameters?.value + 0.5 )
  light_effect:
    attributes:
      # Attribute 'flowing' could also be used?
      current:
        attribute: effect
      speed:
        attribute: speed
    actions:
      stop:
        hass_service: light.turn_on
        service_data:
          effect: Stop
      start:
        hass_service: light.turn_on
        service_data:
          effect:
            parameter: effect
          speed:
            parameter: speed
  binary_sensor:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  "person.binary_sensor":
    attributes:
      state:
        # source is "state"
        map:
          not_home: false
          home: true
        map_unmatched: null
  motion_sensor:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  tamper:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  co_detector:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  door_sensor:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  freeze_detector:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  gas_detector:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  heat_detector:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  presence_sensor:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  smoke_detector:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  window_sensor:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
  battery_maintenance:
    attributes:
      charging:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
      replace: null
      rechargeable: null
      state: null
  value_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  temperature_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  humidity_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  pressure_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  voltage_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  current_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  power_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  energy_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  velocity_sensor:
    attributes:
      value:
        expr: >
          source.state == 'unavailable' ? null : ( float( source.state ) ?# null )
      units:
        attribute: unit_of_measurement
  battery_power:
    attributes:
      level:
        expr: ( float( source.state ) / 100 ) ?# null
      since:
        expr: time()
  string_sensor:
    attributes:
      value:
        expr: str( source.state ?? "" )
  lock:
    attributes:
      state:
        # source is "state"
        map:
          locked: true
          unlocked: false
        map_unmatched: null
    actions:
      lock:
        hass_service: lock.lock
      unlock:
        hass_service: lock.unlock
      'set':
        remap:
          - condition: bool( parameters.state )
            action: lock.lock
          - condition: "false === bool( parameters.state )"
            action: lock.unlock
          - action: false  # explicit no-action if parameters don't match/map
  # Toggle action for lock domain is special. We could probably just use
  # homeassistant.toggle, but this serves as a demonstration/validation of how
  # remapping can be used to turn one action into another (and for a specific
  # domain in this case).
  'lock.toggle':
    actions:
      toggle:
        # Remap to another action based on lock state. The condition expression
        # is looking at the Reactor entity, not the Hass data.
        remap:
          - condition: entity.attributes.lock.state
            action: lock.unlock
          - action: lock.lock
  hvac_control:
    attributes:
      state:
        source: attributes.hvac_action
      mode:
        # source is "state"
        map:
          heat_cool: auto
    actions:
      set_mode:
        hass_service: climate.set_hvac_mode
        service_data:
          hvac_mode:
            parameter: mode
            map:
              auto: "heat_cool"
  climate.power_switch:
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
    actions:
      'on':
        hass_service: climate.turn_on
      'off':
        hass_service: climate.turn_off
  hvac_heating_unit:
    attributes:
      state:
        expr: "source.attributes.hvac_action == 'heating'"
      setpoint:
        expr: >
          source.attributes.target_temp_low ?? source.attributes.temperature
      units:
        source: attributes.temperature_unit
    actions:
      # Hass likes to have the mode with the setpoint. If the mode is auto
      # (heat_cool in Hass), then send both upper and lower as well; otherwise
      # suppress them.
      set_setpoint:
        hass_service: climate.set_temperature
        service_data:
          temperature:
            parameter: "setpoint"
          target_temp_low:
            expr: "entity.attributes.hvac_control.mode === 'auto' ? parameters.setpoint : null"
            skip_null: true
          target_temp_high:
            expr: "entity.attributes.hvac_control.mode === 'auto' ? entity.attributes.hvac_cooling_unit.setpoint : null"
            skip_null: true
  hvac_cooling_unit:
    attributes:
      state:
        expr: "source.attributes.hvac_action == 'cooling'"
      setpoint:
        expr: "source.attributes.target_temp_high ?? source.attributes.temperature"
      units:
        source: attributes.temperature_unit
    actions:
      # Hass likes to have the mode with the setpoint. If the mode is auto (heat_cool in Hass),
      # then send both upper and lower as well; otherwise suppress them.
      set_setpoint:
        hass_service: climate.set_temperature
        service_data:
          temperature:
            parameter: setpoint
          target_temp_low:
            expr: "entity.attributes.hvac_control.mode === 'auto' ? entity.attributes.hvac_heating_unit.setpoint : null"
            skip_null: true
          target_temp_high:
            expr: "entity.attributes.hvac_control.mode === 'auto' ? parameters.setpoint : null"
            skip_null: true
  hvac_blower_unit:
    attributes:
      state:
        expr: source.attributes?.fan_action === "running"
      mode:
        source: attributes.fan_mode
      filter_change: null
    actions:
      set_mode:
        hass_service: climate.set_fan_mode
        service_data:
          fan_mode:
            parameter: mode
  hvac_blower_swing:
    attributes:
      mode:
        source: attributes.swing_mode
    actions:
      set_mode:
        hass_service: climate.set_swing_mode
        service_data:
          swing_mode:
            parameter: mode
  climate.temperature_sensor:
    attributes:
      value:
        source: attributes.current_temperature
      units:
        value: null
  hvac_humidifier:
    attributes:
      state:
        # source is state
        map:
          "on": true
          "off": false
      mode:
        expr: "lower(source.attributes.mode)"
        map:
          "auto": "auto"
          "manual": "off"
          "continuous": "on"
      setpoint:
        expr: float(source.attributes.humidity) / 100.0
    actions:
      set_mode:
        hass_service: humidifier.set_mode
        service_data:
          mode:
            parameter: mode
            map:
              "auto": "Auto"
              "off": "Manual"
              "on": "Continuous"
      set_setpoint:
        hass_service: humidifier.set_humidity
        service_data:
          humidity:
            expr: parameters.setpoint * 100.0
  humdifier.power_switch:
    attributes:
      state:
        # source is state
        map:
          "on": true
          "off": false
        map_unmatched: null
    actions:
      'on':
        hass_service: humidifier.turn_on
      'off':
        hass_service: humidifier.turn_off
      'set':
        remap:
          - condition: bool( parameters.state )
            action: power_switch.on
          - action: power_switch.off
  humidifier.toggle:
    actions:
      toggle:
        hass_service: humidifier.toggle
  hvac_dehumidifier:
    attributes:
      state:
        # source is state
        map:
          "on": true
          "off": false
      mode:
        expr: "lower(source.attributes.mode)"
        map:
          "auto": "auto"
          "manual": "off"
          "continuous": "on"
      setpoint:
        expr: float(source.attributes.humidity) / 100.0
    actions:
      set_mode:
        hass_service: humidifier.set_mode
        service_data:
          mode:
            parameter: mode
            map:
              "auto": "Auto"
              "off": "Manual"
              "on": "Continuous"
      set_setpoint:
        hass_service: humidifier.set_humidity
        service_data:
          humidity:
            expr: parameters.setpoint * 100.0

  zwave_device:
    deprecated: true
    attributes:
      node_id:
        source: attributes.node_id
      failed:
        source: attributes.is_failed
      capabilities:
        expr: "join( source?.attributes?.capabilities ?? [], ',' )"
      manufacturer_info: null
      version_info: null
    actions:
      poll:
        hass_service: zwave_js.refresh_value
        service_data:
          refresh_all_values: true
      refresh:
        hass_service: zwave_js.refresh_value
        service_data:
          refresh_all_values: true
      reset_meters:
        hass_service: zwave_js.reset_meter
        service_data: {}
      set_config:
        hass_service: zwave_js.set_config_parameter
        service_data:
          parameter:
            parameter: parameter
          value:
            expr: "int( parameters.value ) ?? null"
      set_value:
        hass_service: zwave_js.set_value
        service_data:
          endpoint:
            parameter: endpoint
          command_class:
            parameter: commandClass
          property:
            parameter: property
          property_key:
            parameter: propertyKey
          value:
            parameter: value
  'input_select.selector':
    attributes:
      value: {}  # source is "state"
      values:
        attribute: options
    actions:
      select:
        hass_service: input_select.select_option
        service_data:
          option:
            parameter: value
  select.selector:
    attributes:
      value: {}  # source is "state"
      values:
        attribute: options  # already an array
    actions:
      select:
        hass_service: select.select_option
        service_data:
          option:
            parameter: value
  cover:
    attributes:
      state:
        # source is "state"
        map:
          'closed': false
          'open': true
          'opening': true
          'closing': true
        map_unmatched: null
    actions:
      open:
        hass_service: cover.open_cover
      close:
        hass_service: cover.close_cover
      stop:
        hass_service: cover.stop_cover
  'cover.toggle':
    actions:
      toggle:
        remap:
          - condition: entity.attributes.cover.state
            action: cover.close
          - action: cover.open
  'cover.position':
    attributes:
      value:
        expr: int( source.attributes.current_position ) / 100
    actions:
      set:
        hass_service: cover.set_cover_position
        service_data:
          position:
            expr: floor(parameters.value * 100)
      relative:
        hass_service: cover.set_cover_position
        service_data:
          position:
            expr: "100 * max( 0, min( 1, entity.attributes.position.value + parameters.amount ) )"
      increase:
        hass_service: cover.set_cover_position
        service_data:
          position:
            expr: "100 * min( 1, parameters.amount + entity.attributes.position.value )"
      decrease:
        hass_service: cover.set_cover_position
        service_data:
          position:
            expr: "100 * max( 0, parameters.amount - entity.attributes.position.value )"
  vacuum:
    attributes:
      status:
        # 2025.1 introduces "activity" attribute ref: https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property/
        expr: "source.attributes.activity ?? source.state"
        # As of 2025.1, HA activity maps directly to Reactor's known values
      position:
        source: attributes.position
      bin_loaded:
        source: attributes.bin_present
      bin_full:
        source: attributes.bin_full
      online: null
      filter_change: null
    actions:
      start:
        hass_service: vacuum.start
      stop:
        hass_service: vacuum.stop
      pause:
        hass_service: vacuum.pause
      home:
        hass_service: vacuum.return_to_base
      locate:
        hass_service: vacuum.locate
  lawn_mower:
    attributes:
      status: {}  # source is `state` by default, and its values map to ours as of 2025.1.0
    actions:
      start:
        hass_service: lawn_mower.start_mowing
      pause:
        hass_service: lawn_mower.pause
      home:
        hass_service: lawn_mower.dock
  'media_player.power_switch':
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
        map_unmatched: null
    actions:
      'on':
        hass_service: media_player.turn_on
      'off':
        hass_service: media_player.turn_off
  'media_player.toggle':
    actions:
      toggle:
        hass_service: media_player.toggle
  'media_player.av_transport':
    attributes:
      state:
        # source is "state"
        map:
          'playing': 'PLAYING'
          'stopped': 'STOPPED'
          'paused': 'PAUSED'
          # ???: 'TRANSITIONING'
      current_track: null
      current_title:
        attribute: media_title
      current_artist:
        attribute: media_artist
      current_album:
        # order is significant here, as a playlist running may set both playlist and album name
        expr: >
          source.attributes.media_playlist ?? source.attributes.media_album_name ?? source.attributes.media_channel
      album_art:
        value: null
    actions:
      stop:
        hass_service: media_player.media_stop
      pause:
        hass_service: media_player.media_pause
      play:
        hass_service: media_player.media_play
      fast_forward:
        hass_service: media_player.media_next_track
      rewind:
        hass_service: media_player.media_previous_track
  'media_player.media_navigation':
    attributes: {}
    actions:
      next_track:
        hass_service: media_player.media_next_track
      previous_track:
        hass_service: media_player.media_previous_track
      next_chapter: false
      previous_chapter: false
      next_media: false
      previous_media: false
  'media_player.av_repeat':
    attributes:
      shuffle:
        source: attributes.shuffle
      repeat_mode:
        expr: "lower(source.attributes.repeat ?? 'unknown' )"
    actions:
      set_shuffle:
        hass_service: media_player.shuffle_set
        service_data:
          shuffle:
            parameter: state
      shuffle_on:
        hass_service: media_player.shuffle_set
        service_data:
          shuffle: true
      shuffle_off:
        hass_service: media_player.shuffle_set
        service_data:
          shuffle: false
      set_repeat:
        hass_service: media_player.repeat_set
        service_data:
          repeat:
            parameter: mode
      repeat_off:
        hass_service: media_player.repeat_set
        service_data:
          repeat: "off"
      repeat_one:
        hass_service: media_player.repeat_set
        service_data:
          repeat: "one"
      repeat_all:
        hass_service: media_player.repeat_set
        service_data:
          repeat: "all"
  'media_player.volume':
    attributes:
      level:
        source: attributes.volume_level
    actions:
      increase:
        hass_service: media_player.volume_up
      decrease:
        hass_service: media_player.volume_down
      relative:
        hass_service: media_player.volume_set
        service_data:
          volume_level:
            expr: entity.attributes.volume.level + ( parameters.amount ?? 0 )
      set:
        hass_service: media_player.volume_set
        service_data:
          volume_level:
            parameter: value
      set_db: false
  'media_player.muting':
    attributes:
      state:
        attribute: is_volume_muted
    actions:
      mute:
        hass_service: media_player.volume_mute
        service_data:
          is_volume_muted: true
      unmute:
        hass_service: media_player.volume_mute
        service_data:
          is_volume_muted: false
      toggle:
        hass_service: media_player.volume_mute
        service_data:
          is_volume_muted:
            expr: "! entity.attributes.muting.state"
      set:
        hass_service: media_player.volume_mute
        service_data:
          is_volume_muted:
            parameter: muting
  location:
    attributes:
      latitude:
        attribute: latitude
      longitude:
        attribute: longitude
      elevation: null
    actions: {}
  'x_hass_system':
    attributes: {}
    actions:
      call_service:
        hass_service: '@service'
        service_data: '@data'
        target: '@target'
        skip_entity_id: true
      fire_event:
        function: do_system_fire_event
      reload_integration:
        function: do_system_reload_integration
      reload_configuration:
        hass_service: homeassistant.reload_core_config
      restart: true  # HassController provides function
  'x_hass':
    attributes: {}
    actions:
      call_service:
        hass_service: '@service'
        service_data: '@data'
        use_target: true  # entity ID in target, not data (default after HassController 23293)
  'scene.script':
    attributes:
      runnable: true
    actions:
      run:
        hass_service: scene.turn_on
  'script.script':
    attributes:
      runnable: true
    actions:
      run:
        hass_service: script.turn_on
  'automation.script':
    attributes:
      runnable: true
    actions:
      run:
        hass_service: automation.trigger
  'automation.power_switch':
    attributes:
      state:
        # source is "state"
        map:
          'off': false
          'on': true
    actions:
      'on':
        hass_service: automation.turn_on
      'off':
        hass_service: automation.turn_off
      'set':
        remap:
          - condition: bool( parameters.state )
            action: power_switch.on
          - action: power_switch.off
  wx:
    attributes:
      temperature:
        expr: float( source.attributes?.temperature )
      temperature_unit:
        attribute: temperature_unit
      humidity:
        expr: float( source.attributes?.humidity )
      humidity_unit: '%'
      description:
        source: state
      precipitation_unit:
        attribute: precipitation_unit
      wind_speed:
        expr: float( source.attributes?.wind_speed )
      wind_speed_unit:
        attribute: wind_speed_unit
      wind_compass:
        expr: float( source.attributes?.wind_bearing )
      wind_direction:
        expr: >
          i=floor( ( source.attributes?.wind_bearing ?# 0 ) / 22.5 ),
          (['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'])[i]
      pressure:
        expr: float( source.attributes?.pressure )
      pressure_unit:
        attribute: pressure_unit
      visibility:
        expr: float( source.attributes?.visibility )
      visibility_unit:
        attribute: visibility_unit
      location: null
      condition_code: null
      asoftime: null
      icon: null
      feels_like: null
      precipitation_24hr: null
      precipitation_1hr: null
      precipitation_other: null
      precipitation_type: null
      wind_gust: null
      wind_conditions: null
      cloud_cover: null
      ceiling: null
      ceiling_unit: null

  button:
    attributes:
      state: single
      since:
        expr: "time(source.last_updated)"
    actions:
      press:
        remap:
          - action: button.single
      single:
        hass_service: button.press

  valve:
    attributes:
      state:
        # source is "state"
        map:
          'open': true
          'closed': false
        map_unmatched: null
    actions:
      'set':
        remap:
          - condition: "bool( parameters.state )"
            action: valve.open
          - condition: "false === parameters.state"
            action: valve.close
          - action: false  # explicit no-action if not matched; get it right or do nothing!
      open:
        hass_service: valve.open_valve
      close:
        hass_service: valve.close_valve
  'valve.toggle':
    actions:
      toggle:
        hass_service: valve.toggle
  'valve.position':
    attributes:
      value:
        expr: int( source.attributes.current_position ) / 100
    actions:
      set:
        hass_service: valve.set_valve_position
        service_data:
          position:
            expr: floor(parameters.value * 100)
      relative:
        hass_service: valve.set_valve_position
        service_data:
          position:
            expr: "100 * max( 0, min( 1, entity.attributes.position.value + parameters.amount ) )"
      increase:
        hass_service: valve.set_valve_position
        service_data:
          position:
            expr: "100 * min( 1, parameters.amount + entity.attributes.position.value )"
      decrease:
        hass_service: valve.set_valve_position
        service_data:
          position:
            expr: "100 * max( 0, parameters.amount - entity.attributes.position.value )"

# The mapping section describes how HassController maps HomeAssistant devices
# and entities into Reactor entities. The first matching rule controls.
# Again, do not make overrides/mods here. See the note at the top of this file
# for how to make site-specific mods.
mapping:
  - match_domain: "persistent_notification"
    ignore: true
  - match_domain: "zone"
    ignore: true

service_map:
  switch:  # device classes: https://www.home-assistant.io/integrations/switch/#device-class
    capability:
      - extend:
          - power_switch
          - toggle
    type: Switch
    primary_attribute: power_switch.state

  light:
    # Ref: https://github.com/home-assistant/core/blob/dev/homeassistant/components/light/__init__.py
    capability:
      - extend:
          - power_switch
          - toggle
      # Some of these are now deprecated, in favor of keywords in array supported_color_modes
      # 2021-06-24
      - condition: "bool((attributes?.supported_features ?? 0) & 1)"
        extend: dimming
      - condition: "bool((attributes?.supported_features ?? 0) & 2)"
        extend: color_temperature
      - condition: "bool((attributes?.supported_features ?? 0) & 4)"
        extend: light_effect
      - condition: "bool((attributes?.supported_features ?? 0) & 16)"
        extend:
          - rgb_color
          - hs_color
      # 2021-06-24: These are the "new" way
      # mode "onoff" is already handled by default capabilities above; "unknown", "white" is not handled yet.
      # 2022-11-08: If one color mode is supported, that implies all of them, and brightness/dimming.
      #             Ref: https://developers.home-assistant.io/docs/core/entity/light?_highlight=light
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'brightness' ) >= 0"
        extend: dimming
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'color_temp' ) >= 0"
        extend: color_temperature
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'rgb' ) >= 0"
        extend: [rgb_color, hs_color, xy_color, color_temperature, dimming]
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'rgbw' ) >= 0"
        extend: [rgb_color, hs_color, xy_color, color_temperature, dimming]
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'rgbww' ) >= 0"
        extend: [rgb_color, hs_color, xy_color, color_temperature, dimming]
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'hs' ) >= 0"
        extend: [rgb_color, hs_color, xy_color, color_temperature, dimming]
      - condition: "indexOf( attributes?.supported_color_modes ?? [], 'xy' ) >= 0"
        extend: [rgb_color, hs_color, xy_color, color_temperature, dimming]
    type: Light
    primary_attribute: power_switch.state

  sensor:
    #  See https://www.home-assistant.io/integrations/sensor/
    -
      match_attributes:
        device_class: "temperature"
      capability:
        - extend:
            - temperature_sensor
            - value_sensor  # for backward compat pre-21165
      type: ValueSensor
      primary_attribute: temperature_sensor.value
    -
      match_attributes:
        device_class: "humidity"
      capability:
        - extend:
            - humidity_sensor
            - value_sensor  # for backward compat pre-21165
      type: ValueSensor
      primary_attribute: humidity_sensor.value
    -
      match_attributes:
        device_class: "weight"
      capability:
        - extend:
            - weight_sensor
      type: ValueSensor
      primary_attribute: weight_sensor.value
    -
      match_attributes:
        device_class: "pressure"
      capability:
        - extend:
            - pressure_sensor
      type: ValueSensor
      primary_attribute: pressure_sensor.value
    -
      match_attributes:
        device_class: "voltage"
      capability:
        - extend:
            - voltage_sensor
            - value_sensor  # for backward compat pre-22277
      type: ValueSensor
      primary_attribute: voltage_sensor.value
    -
      match_attributes:
        device_class: "current"
      capability:
        - extend:
            - current_sensor
            - value_sensor  # for backward compat pre-22277
      type: ValueSensor
      primary_attribute: current_sensor.value
    -
      match_attributes:
        device_class: "energy"
      capability:
        - extend:
            - energy_sensor
            - value_sensor  # for backward compat pre-22277
      type: ValueSensor
      primary_attribute: energy_sensor.value
    -
      match_attributes:
        device_class: "power"
      capability:
        - extend:
            - power_sensor
            - value_sensor  # for backward compat pre-22277
      type: ValueSensor
      primary_attribute: power_sensor.value
    -
      match_attributes:
        device_class: "speed"
      capability:
        - extend:
            - velocity_sensor
      type: ValueSensor
      primary_attribute: velocity_sensor.value
    -
      match_attributes:
        device_class: "battery"
      capability:
        - extend:
            - value_sensor
            - battery_power  # for backward compat pre-21165
      type: ValueSensor
      primary_attribute: battery_power.level
    -
      # Catch-all (value) sensor type
      capability:
        - extend:
            - value_sensor
            - string_sensor
      type: ValueSensor
      primary_attribute: string_sensor.value

  binary_sensor:
    -
      match_attributes:
        device_class: "motion"
      capability:
        - extend:
            - motion_sensor
            - binary_sensor
      type: BinarySensor
      primary_attribute: motion_sensor.state
    -
      match_attributes:
        device_class: "tamper"
      capability:
        - extend:
            - tamper
            - binary_sensor
      primary_attribute: tamper.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "battery_charging"
      capability:
        - extend:
            - battery_maintenance
            - binary_sensor
      primary_attribute: battery_maintenance.charging
      type: BinarySensor
    -
      match_attributes:
        device_class: "carbon_monoxide"
      capability:
        - extend:
            - co_detector
            - binary_sensor
      primary_attribute: co_detector.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "cold"
      capability:
        - extend:
            - freeze_detector
            - binary_sensor
      primary_attribute: freeze_detector.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "door"
      capability:
        - extend:
            - door_sensor
            - binary_sensor
      primary_attribute: door_sensor.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "garage_door"
      capability:
        - extend:
            - door_sensor
            - binary_sensor
      primary_attribute: door_sensor.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "gas"
      capability:
        - extend:
            - gas_detector
            - binary_sensor
      primary_attribute: gas_detector.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "heat"
      capability:
        - extend:
            - heat_detector
            - binary_sensor
      primary_attribute: heat_detector.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "occupancy"
      capability:
        - extend:
            - presence_sensor
            - binary_sensor
      primary_attribute: presence_sensor.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "presence"
      capability:
        - extend:
            - presence_sensor
            - binary_sensor
      primary_attribute: presence_sensor.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "smoke"
      capability:
        - extend:
            - smoke_detector
            - binary_sensor
      primary_attribute: smoke_detector.state
      type: BinarySensor
    -
      match_attributes:
        device_class: "window"
      capability:
        - extend:
            - window_sensor
            - binary_sensor
      primary_attribute: window_sensor.state
      type: BinarySensor
    -
      # Catch-all binary sensor
      capability: binary_sensor
      primary_attribute: binary_sensor.state
      type: BinarySensor

  lock:
    capability: lock
    primary_attribute: lock.state
    type: Lock

  climate:
    # ref: https://developers.home-assistant.io/docs/core/entity/climate/
    capability:
      - extend: hvac_control
      - condition: "indexOf( attributes?.hvac_modes ?? [], 'off' ) >= 0"
        extend: power_switch
      - condition: "indexOf( attributes?.hvac_modes ?? [], 'heat' ) >= 0"
        extend: hvac_heating_unit
      - condition: "indexOf( attributes?.hvac_modes ?? [], 'cool' ) >= 0"
        extend: hvac_cooling_unit
      - condition: "!isnull( attributes?.current_temperature )"
        extend: temperature_sensor
      - condition: "bool((attributes?.supported_features ?? 0) & 8) || !isnull(attributes?.fan_action)"
        extend: hvac_blower_unit
      - condition: "bool((attributes?.supported_features ?? 0) & 32) || !isnull(attributes?.swing_mode)"
        extend: hvac_blower_swing
    type: Thermostat
    primary_attribute: hvac_control.state

  humidifier:
    -
      match_attributes:
        device_class: "dehumidifier"
      capability:
        - extend:
            - hvac_dehumidifier
            - power_switch
      type: BinarySensor
      primary_attribute: hvac_dehumidifier.state
    -
      # Catch-all other classes
      capability:
        - extend:
            - hvac_humidifier
            - power_switch
      type: BinarySensor
      primary_attribute: hvac_humidifier.state

  input_boolean:
    capability:
      - extend:
          - power_switch
          - toggle

  input_text:
    capability:
      - extend:
          - string_sensor

  input_number:
    capability:
      - extend:
          - value_sensor

  input_select:
    capability:
      - extend:
          - selector
          - string_sensor

  cover:  # there are now several device classes, see https://www.home-assistant.io/integrations/cover/#device-class
    capability:
      - extend:
          - cover
          - position
          - toggle
    type: Cover
    primary_attribute: cover.state

  media_player:  # device classes: https://www.home-assistant.io/integrations/media_player/#device-class
    -
      match_attributes:
        device_class: tv
      capability:
        - extend:
            - power_switch
            - toggle
            - volume
            - muting
      type: MediaPlayer
      primary_attribute: power_switch.state
    -  # Catch-all Media Player
      # ??? TO-DO: existence of sonos_group attribute is clue to availability of sonos service/actions
      capability:
        - extend:
            - power_switch
            - toggle
            - av_transport
            - media_navigation
            - av_repeat
            - volume
            - muting
      type: MediaPlayer
      primary_attribute: power_switch.state

  vacuum:
    capability: vacuum
    primary_attribute: vacuum.status

  zwave:
    capability: zwave_device

  zwave_js:
    capability: zwave_device

  scene:
    capability:
      - extend:
          - script
    type: Script
    primary_attribute: null

  automation:
    capability:
      - extend:
          - script
          - power_switch
    type: Script
    primary_attribute: power_switch.state

  script:
    capability:
      - extend:
          - script
    type: Script
    # primary_attribute: null

  device_tracker:
    capability:
      - extend: string_sensor
      - condition: "!isnull(attributes?.latitude)"
        extend: location
  person:
    capability:
      - extend: binary_sensor
    type: BinarySensor
    primary_attribute: binary_sensor.state

  proximity:
    capability:
      - extend: value_sensor
    type: ValueSensor
    primary_attribute: value_sensor.value
    # The generic map for "value_sensor" is sufficient here

  weather:
    capability:
      - extend: wx
    type: Weather
    primary_attribute: wx.description

  # TO-DO: Number should be equivalenced to sensor (our value_sensor), with mapping for device class
  number:  # https://www.home-assistant.io/integrations/number/#device-class
    capability:
      - extend: value_sensor
    type: ValueSensor
    primary_attribute: value_sensor.value

  select:
    capability:
      - extend: selector
    type: ValueSensor
    primary_attribute: selector.value

  button:
    capability:
      - extend: button
    type: Button
    primary_attribute: button.state

  valve:
    capability:
      # Ref: https://github.com/home-assistant/core/blob/master/homeassistant/components/valve/__init__.py
      # Ref:
      - extend: valve
      - extend: toggle
      - condition: "( ( attributes?.supported_features ?# 0 ) & 4 ) === 4"
        extend: position
    type: Valve
    primary_attribute: valve.state

  zone:
  persistent_notification:
  sun:
  # fan
  # tts
  # sonos
  # notify
  # stream
  # roku
  # yeelight
  # remote
# Ref: https://developers.home-assistant.io/docs/core/entity/media-player/

events:
  tag_scanned:
    -
      entity: "tag%data.tag_id%"
      type: Entity
      capabilities:
        tag:
          tag_id:
            from: data.tag_id
            force: true
          device_id:
            from: data.device_id
            force: true
          last_scanned:
            from: time_fired_epoch
          scan_count:
            expr: "( entity.attributes.tag.scan_count ?# 0 ) + 1"
      primary_attribute: "tag.scan_count"
    -
      entity: "rdr%data.device_id%"
      type: Entity
      capabilities:
        tag:
          tag_id:
            from: data.tag_id
            force: true
          device_id:
            from: data.device_id
            force: true
          last_scanned:
            from: time_fired_epoch
          scan_count:
            expr: "( entity.attributes.tag.scan_count ?# 0 ) + 1"
      primary_attribute: "tag.tag_id"
