Script to merge yaml files recoursively

To simplify my configurations, I ever wonder to write all configuration keys into one file per device type, to reach all parameters immediately instead checking into splitted configuration.
So here is my node script. It merges all yaml files placed into ‘configuration’ subdirectory without duplicate keys and then place the result into configuration.yaml file.

Please, check before use it, for my configuration it works. Run with sudo npm compile.js after npm install recursive-readdir

here it is:

var recursive = require("recursive-readdir")
var fs = require('fs')
var obj = {}
var current_key = ''
var obj_string = ''
 
recursive("configuration", function (err, files) {
  // `files` is an array of absolute file paths 
  files.forEach((file) => {
    console.log('Reading ' + file)
    fs
      .readFileSync(file).toString().split('\n')
      .forEach(
        (line) => {
          line = line.replace("\r", "").replace("\t", "  ")

          let test_0 = line.match(/^\s*#.*/)
          if  (!test_0 && line.trim() !== '') {
            // check no spaces
            let test_1 = line.match(/^(\S.+)/)
            if (test_1) {
              current_key = test_1[0].replace(':','').trim()
              console.log('Feeding "' +  current_key + '"')
              if (typeof obj[current_key] === 'undefined') {
                obj[current_key] = []
              }
            }

            if ((line.trim() === 'customize_glob:' || line.trim() === 'customize:')) {
              current_key = line.trim().replace(':','').trim()
              console.log('Feeding "' +  current_key + '"')
              if (typeof obj['homeassistant'][current_key] === 'undefined') {
                obj['homeassistant'][current_key] = []
              }
            }

            if (!test_1 && line.trim() !== 'customize_glob:' && line.trim() !== 'customize:') {
              if (current_key === 'customize_glob' || current_key === 'customize') {
                obj['homeassistant'][current_key].push(line)
              } else {
                obj[current_key].push(line)
              }
            }
          } else {
            console.log('Skipped line', line)
          }
        }
      )
  })

  //console.log(obj)
  console.log('Writing to configuration.yaml')
  for (let key in obj)
  {
    console.log('"' + key + ':"')
    obj_string+= key + ':\n'
    for (let subkey in obj[key]) {
      if (typeof obj[key][subkey] !== 'string') {
        console.log('"' + '  ' + subkey + ':' + '"')
        obj_string+= '  ' + subkey + ':\n'
        for (let subkey2 in obj[key][subkey]) {
          console.log('"' + obj[key][subkey][subkey2] + '"')
          obj_string+= obj[key][subkey][subkey2] + '\n'
        }
      } else {
        console.log('"' + obj[key][subkey])
        obj_string+= obj[key][subkey] + '\n'
      }
    }
  }
  fs.truncate("./configuration.yaml", 0, function() {
    fs.writeFile("./configuration.yaml", obj_string, function (err) {
      if (err) {
        return console.log("Error writing file: " + err);
      } else {
        return console.log('DONE!')
      }
    })
  })
})

My .homeassistant directory structure is like this:

.homeassistant
  - compile.js
  /configuration
    - automation.yaml
    - trackers.yaml
    - homeassistant.yaml
    - windows.yaml
    - etc...

configuration/homeassistant.yaml contains all non-device related keys

group:
  #default_view:
  main:
    name: main
    view: yes
    entities:
      - updater.updater
      - input_select.presenza
      - group.covers
      - group.switches
      - group.livingcolors
      - group.system
      - group.camera_soggiorno
      - group.camera_garage
      - sensor.yr_symbol
      - sensor.temperatura_esterna
      - sensor.umidita_esterna
      - device_tracker.motoxdavide
      - group.zwave

  security_automations:
    name: Automazione Sicurezza
    icon: mdi:robot
    control: hidden
    entities:
      - input_select.presenza
      - automation.attiva_sorveglianza_non_a_casa
      - automation.disattiva_sorveglianza_non_a_casa
      - automation.attiva_sorveglianza_notturna_automatica
      - automation.disattiva_sorveglianza_notturna_automatica

homeassistant:
  # Name of the location where Home Assistant is running
  name: Home
  # Location required to calculate the time the sun rises and sets
  latitude: xx
  longitude: xx
  elevation: 9
  # C for Celsius, F for Fahrenheit
  #temperature_unit: C
  # Pick yours from here: http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
  time_zone: Europe/Rome
  unit_system: metric  
  whitelist_external_dirs:
    - /tmp
    - /home/pi/FTP

  customize_glob:
    "*media_player*":
      hidden: true

input_select:
  presenza:
    name: Presenza
    options:
      - A Casa
      - Non a Casa
      - Guardia
    initial: A Casa
    icon: mdi:incognito

zwave:
  usb_path: /dev/ttyUSB0
#  config_path: /srv/homeassistant/homeassistant_venv/lib/python3.4/site-packages/libopenzwave-0.3.1-py3.4-linux-armv7l.egg/config
  device_config: 
    cover.fibaro_system_fgrm222_roller_shutter_controller_2_level:
      invert_openclose_buttons: true
    cover.fibaro_system_fgrm222_roller_shutter_controller_2_level_2:
      invert_openclose_buttons: true
    #  ignored: false
    #  polling_intensity: 60

telegram_bot:
  platform: webhooks
  api_key: xxxx
  allowed_chat_ids:
  - xxxxx

notify:
  name: telegram
  platform: telegram
  chat_id: xxxx

mqtt:
  broker: 127.0.0.1
  port: 1883
  client_id: home-assistant-1
  username: pi
  password: xxxxx    

# View all events in a logbook
logbook:

# Allows you to issue voice commands from the frontend
conversation:

# Discover some devices automatically
discovery:

# Enables the frontend
frontend:

# Enables support for tracking state changes over time.
history:
  include:
    domains: 
      - sensor
#      - switch

# Checks for available updates
updater:

# Track the sun
sun:
  elevation: 9

# Show links to resources in log and frontend
#introduction:

# Config panel
config:

http:
  base_url: xxxx
  api_password: xxxxx
  ssl_certificate: /etc/letsencrypt/live/xxxx/fullchain.pem
  ssl_key:         /etc/letsencrypt/live/xxxx/privkey.pem
#  host_name: xxxx

logger:
  default: error

device_tracker:
  - platform: fritz
    host: 192.168.1.1
    username: xxxx
    password: xxx
    track_new_devices: yes
    interval_seconds: 300
    consider_home: 0:10:00
  - platform: nmap_tracker
    hosts: 192.168.1.1/24
    home_interval: 10
    interval_seconds: 60
#    exclude:
#     - 192.168.1.1
#     - 192.168.1.2
#     - 192.168.1.4

alarm_control_panel:
  platform: manual
  code: xxxx
  disarm_after_trigger: false

and, for example, my smart_socket.yaml

group:
  asciugatrice:
    name: Asciugatrice
    icon: mdi:washing-machine
    control: hidden
    entities:
      - switch.neo_coolcam_power_plug_12a_switch
      - zwave.neo_coolcam_power_plug_12a
      - sensor.neo_coolcam_power_plug_12a_voltage
      - sensor.neo_coolcam_power_plug_12a_current
      - sensor.neo_coolcam_power_plug_12a_power
      - sensor.neo_coolcam_power_plug_12a_energy
      - automation.avviso_asciugatrice_finita

  smart_socket:
    name: Smart Socket
    icon: mdi:power-plug
    control: hidden
    entities:
      - switch.neo_coolcam_power_plug_12a_switch_2
      - zwave.neo_coolcam_power_plug_12a_2
      - sensor.neo_coolcam_power_plug_12a_voltage_2
      - sensor.neo_coolcam_power_plug_12a_current_2
      - sensor.neo_coolcam_power_plug_12a_energy_2

homeassistant:
  customize_glob:
    "sensor.neo_coolcam_power_plug_12a_interval*":
      hidden: true
    "sensor.neo_coolcam_power_plug_12a_exporting*":
      hidden: true
    "sensor.neo_coolcam_power_plug_12a_alarm_type*":
      hidden: true
    "sensor.neo_coolcam_power_plug_12a_previous_reading*":
      hidden: true
    "sensor.neo_coolcam_power_plug_12a_sourcenodeid*":
      hidden: true
    "sensor.neo_coolcam_power_plug_12a_alarm_level*":
      hidden: true
    "switch.neo_coolcam_power_plug_12a_switch*":
      icon: mdi:power-plug
      friendly_name: Presa
      hidden: false
    "sensor.neo_coolcam_power_plug_12a_voltage*":
      icon: mdi:speedometer
      friendly_name: Tensione
      hidden: false
    "sensor.neo_coolcam_power_plug_12a_current*":
      icon: mdi:trending-up
      friendly_name: Corrente
      hidden: false
    "sensor.neo_coolcam_power_plug_12a_energy*":
      icon: mdi:flash
      friendly_name: Consumo
      hidden: false 
    "sensor.neo_coolcam_power_plug_12a_power*":
      icon: mdi:flash
      friendly_name: Potenza
      hidden: false

binary_sensor:
  - platform: template
    sensors:
      asciugatrice_accesa:
        friendly_name: Asciugatrice Accesa
        entity_id: sensor.neo_coolcam_power_plug_12a_current
        value_template: '{{ states.sensor.neo_coolcam_power_plug_12a_current.state | float > 0 }}'

As you can see the script permit to redeclare config keys many times… it merges values into one key at output. Hope this can helps someone.

The script also replaces tabs with double spaces and ignores commented rows.

I started going down that path until someone pointed out that package are doing this natively…

Not sure they do everything your script does but they are a great way to keep things together.