Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suggestion for update #388

Closed
Duhlin12 opened this issue Jun 24, 2024 · 3 comments
Closed

suggestion for update #388

Duhlin12 opened this issue Jun 24, 2024 · 3 comments

Comments

@Duhlin12
Copy link

Duhlin12 commented Jun 24, 2024

I have updated the code in netatmo.js to allow specifying the unit for displaying wind speed in the configuration file with the
configuration parameter:

windUnit: "MS", // Possible "KMH", "MPH", "MS", "BFT", "KT"

the updated code:

/* MagicMirror²
 * Module: MMM-Netatmo
 *
 * By Christopher Fenner https://github.com/CFenner
 * MIT Licensed.
 */
/* global Module */
Module.register('netatmo', {
  // default config
  defaults: {
    initialDelay: 0,
    updateInterval: 3, // every 3 minutes, refresh interval on netatmo is 10 minutes
    animationSpeed: 1000,
    design: 'classic', // or bubbles
    horizontal: true,
    lastMessageThreshold: 600, // in seconds (10 minutes)
    showLastMessage: true,
    showBattery: true,
    showRadio: true,
    showWiFi: true,
    showTrend: true,
    showMeasurementIcon: true,
    showMeasurementLabel: true,
    showStationName: true,
    showModuleNameOnTop: false,
    apiBase: 'api.netatmo.com',
    authEndpoint: '/oauth2/token',
    dataEndpoint: '/api/getstationsdata',
    fontClassModuleName: 'xsmall',
    fontClassPrimary: 'large',
    fontClassSecondary: 'xsmall',
    fontClassMeasurement: 'xsmall',
    thresholdCO2Average: 800,
    thresholdCO2Bad: 1800,
    mockData: false,
    windUnit: 'KMH' // Default wind unit
  },
  notifications: {
    AUTH: 'NETATMO_AUTH',
    AUTH_RESPONSE: 'NETATMO_AUTH_RESPONSE',
    DATA: 'NETATMO_DATA',
    DATA_RESPONSE: 'NETATMO_DATA_RESPONSE',
  },
  moduleType: {
    MAIN: 'NAMain',
    INDOOR: 'NAModule4',
    OUTDOOR: 'NAModule1',
    RAIN: 'NAModule3',
    WIND: 'NAModule2',
  },
  measurement: {
    CO2: 'CO2',
    HUMIDITY: 'Humidity',
    TEMPERATURE: 'Temperature',
    TEMPERATURE_TREND: 'temp_trend',
    PRESSURE: 'Pressure',
    PRESSURE_TREND: 'pressure_trend',
    NOISE: 'Noise',
    WIND_STRENGTH: 'WindStrength',
    WIND_ANGLE: 'WindAngle',
    GUST_STRENGTH: 'GustStrength',
    GUST_ANGLE: 'GustAngle',
    RAIN: 'Rain',
    RAIN_PER_HOUR: 'sum_rain_1',
    RAIN_PER_DAY: 'sum_rain_24',
  },
  // init method
  start () {
    Log.info(`Starting module: ${this.name}`)
    this.loaded = false
    this.moduleList = []

    // get a new token at start-up.
    setTimeout(() => {
      // best way is using initialize at start and if auth OK --> fetch data
      this.sendSocketNotification('INIT', this.config)
    }, this.config.initialDelay * 1000)

    // set auto-update
    setInterval(() => {
      this.sendSocketNotification(this.notifications.DATA)
    }, this.config.updateInterval * 60 * 1000 + this.config.initialDelay * 1000)
  },
  updateModuleList (stationList) {
    let moduleList = []

    for (const station of stationList) {
      moduleList.push(this.getModule(station, station.home_name))

      station.modules.forEach(function (module) {
        moduleList.push(this.getModule(module, station.home_name))
      }.bind(this))

      if (station.reachable) { this.lastUpdate = station.dashboard_data.time_utc }
    }
    this.loaded = true
    if (JSON.stringify(this.moduleList) === JSON.stringify(moduleList)) {
      return
    }
    // reorder modules
    if (this.config.moduleOrder && this.config.moduleOrder.length > 0) {
      const reorderedModuleList = []
      for (const moduleName of this.config.moduleOrder) {
        for (const module of moduleList) {
          if (module.name === moduleName) {
            reorderedModuleList.push(module)
          }
        }
      }
      moduleList = reorderedModuleList
    }
    this.moduleList = moduleList
  },
  getModule (module, stationName) {
    const result = {}

    result.name = module.module_name
    if (this.config.showStationName) {
      result.name = `${stationName} - ${result.name}`
    }
    result.measurementList = []

    if (!module.reachable) {
      let measurement = ''
      if (module.type === this.moduleType.MAIN) {
        measurement = 'wifi'
      } else {
        measurement = 'radio'
      }

      result.measurementList.push({
        name: measurement,
        value: this.getValue(measurement, 0),
        unit: this.getUnit(measurement),
        icon: `${this.getIcon(measurement, 0)} flash red`,
        label: this.translate(measurement.toUpperCase()),
      })

      return result
    }

    // TODO check module.reachable
    let primaryType = ''
    let primaryValue = ''
    let secondaryType = ''
    let secondaryValue = ''

    // add module sensor measurements
    switch (module.type) {
      case this.moduleType.MAIN:
        result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE))
        if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.PRESSURE_TREND)) }
        result.measurementList.push(this.getMeasurement(module, this.measurement.NOISE))
        // break; fallthrough
      case this.moduleType.INDOOR:
        if (this.config.design === 'bubbles') {
          secondaryType = this.measurement.CO2
          secondaryValue = module.dashboard_data[secondaryType]
          result.secondary = {
            value: this.getValue(secondaryType, secondaryValue),
            unit: this.getUnit(secondaryType),
            class: this.kebabCase(secondaryType),
            visualClass: this.getCO2Status(secondaryValue),
          }
        } else {
          result.measurementList.push(this.getMeasurement(module, this.measurement.CO2))
        }
        // break; fallthrough
      case this.moduleType.OUTDOOR:
        if (this.config.design === 'bubbles') {
          primaryType = this.measurement.TEMPERATURE
          primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : ''
          result.primary = {
            value: this.getValue(primaryType, primaryValue),
            unit: this.getUnit(primaryType),
            class: this.kebabCase(primaryType),
          }
        } else {
          result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE))
        }
        if (this.config.showTrend) { result.measurementList.push(this.getMeasurement(module, this.measurement.TEMPERATURE_TREND)) }
        result.measurementList.push(this.getMeasurement(module, this.measurement.HUMIDITY))
        break
      case this.moduleType.WIND:
        if (this.config.design === 'bubbles') {
          primaryType = this.measurement.WIND_STRENGTH
          primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : ''
          result.primary = {
            value: this.getValue(primaryType, primaryValue),
            unit: this.getUnit(primaryType),
            class: this.kebabCase(primaryType),
          }
          secondaryType = this.measurement.WIND_ANGLE
          secondaryValue = module.dashboard_data[secondaryType]
          result.secondary = {
            value: this.getValue(secondaryType, secondaryValue),
            unit: this.getUnit(secondaryType),
            class: this.kebabCase(secondaryType),
            visualClass: 'xlarge wi wi-direction-up',
          }
        } else {
          result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_STRENGTH))
          result.measurementList.push(this.getMeasurement(module, this.measurement.WIND_ANGLE))
        }
        //         $('<div/>').addClass('visual xlarge wi wi-direction-up').css('transform', 'rotate(' + value + 'deg)')
        result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_STRENGTH))
        result.measurementList.push(this.getMeasurement(module, this.measurement.GUST_ANGLE))
        break
      case this.moduleType.RAIN:
        if (this.config.design === 'bubbles') {
          primaryType = this.measurement.RAIN
          primaryValue = module.dashboard_data ? module.dashboard_data[primaryType] : ''
          result.primary = {
            value: this.getValue(primaryType, primaryValue),
            unit: this.getUnit(primaryType),
            class: this.kebabCase(primaryType),
          }
        } else {
          result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN))
        }
        result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_HOUR))
        result.measurementList.push(this.getMeasurement(module, this.measurement.RAIN_PER_DAY))
        break
      default:
        break
    }
    // add module specific measurements
    if (module.type === this.moduleType.MAIN) {
      if (this.config.showWiFi) { result.measurementList.push(this.getMeasurement(module, 'wifi', module.wifi_status)) }
    } else {
      if (this.config.showRadio) { result.measurementList.push(this.getMeasurement(module, 'radio', module.rf_status)) }
      if (this.config.showBattery) { result.measurementList.push(this.getMeasurement(module, 'battery', module.battery_percent)) }
    }
    // reorder measurements
    if (this.config.dataOrder && this.config.dataOrder.length > 0) {
      const reorderedMeasurementList = []
      for (const measurementName of this.config.dataOrder) {
        for (const measurement of result.measurementList) {
          if (measurement.name === measurementName) {
            reorderedMeasurementList.push(measurement)
          }
        }
      }
      result.measurementList = reorderedMeasurementList
    }
    return result
  },
  getMeasurement (module, measurement, value) {
    value = value || module.dashboard_data[measurement]
    if (measurement === this.measurement.TEMPERATURE_TREND || measurement === this.measurement.PRESSURE_TREND) {
      value = value || 'undefined'
    }

    return {
      name: measurement,
      value: this.getValue(measurement, value),
      unit: this.getUnit(measurement),
      icon: this.getIcon(measurement, value),
      label: this.translate(measurement.toUpperCase()),
    }
  },
  kebabCase (name) {
    return name.replace(/([a-z])([A-Z])/g, '$1-$2')
      .replace(/[\s_]+/g, '-')
      .toLowerCase()
  },
  getValue (measurement, value) {
    if (!value) { return value }
    switch (measurement) {
      case this.measurement.CO2:
        return value.toFixed(0)// + '&nbsp;ppm'
      case this.measurement.NOISE:
        return value.toFixed(0)// + '&nbsp;dB'
      case this.measurement.HUMIDITY:
      case 'battery':
      case 'wifi':
      case 'radio':
        return value.toFixed(0)// + '%'
      case this.measurement.PRESSURE:
        return value.toFixed(0)// + '&nbsp;mbar'
      case this.measurement.TEMPERATURE:
        return value.toFixed(1)// + '°C'
      case this.measurement.RAIN:
      case this.measurement.RAIN_PER_HOUR:
      case this.measurement.RAIN_PER_DAY:
        return value.toFixed(1)// + '&nbsp;mm/h'
      case this.measurement.WIND_STRENGTH:
        return this.convertWindUnit(value, this.config.windUnit)
      case this.measurement.GUST_STRENGTH:
        return this.convertWindUnit(value, this.config.windUnit)
      case this.measurement.WIND_ANGLE:
      case this.measurement.GUST_ANGLE:
        return `${this.getDirection(value)}&nbsp;|&nbsp;${value}`// + '°'
      case this.measurement.TEMPERATURE_TREND:
      case this.measurement.PRESSURE_TREND:
        return this.translate(value.toUpperCase())
      default:
        return value
    }
  },
  convertWindUnit (value, unit) {
    switch (unit) {
      case 'KMH':
        return value.toFixed(1)
      case 'MS':
        return (value / 3.6).toFixed(1)
      case 'MPH':
        return (value / 1.609).toFixed(1)
      case 'BFT':
        return this.convertToBeaufort(value)
      case 'KT':
        return (value / 1.852).toFixed(1)
      default:
        return value
    }
  },
  convertToBeaufort (value) {
    if (value < 1) return 0
    if (value <= 5) return 1
    if (value <= 11) return 2
    if (value <= 19) return 3
    if (value <= 28) return 4
    if (value <= 38) return 5
    if (value <= 49) return 6
    if (value <= 61) return 7
    if (value <= 74) return 8
    if (value <= 88) return 9
    if (value <= 102) return 10
    if (value <= 117) return 11
    return 12
  },
  getUnit (measurement) {
    switch (measurement) {
      case this.measurement.CO2:
        return 'ppm'
      case this.measurement.NOISE:
        return 'dB'
      case this.measurement.HUMIDITY:
      case 'battery':
      case 'wifi':
      case 'radio':
        return '%'
      case this.measurement.PRESSURE:
        return 'mbar'
      case this.measurement.TEMPERATURE:
        return '°C'
      case this.measurement.RAIN:
      case this.measurement.RAIN_PER_HOUR:
      case this.measurement.RAIN_PER_DAY:
        return 'mm/h'
      case this.measurement.WIND_STRENGTH:
      case this.measurement.GUST_STRENGTH:
        return this.getWindUnitLabel(this.config.windUnit)
      case this.measurement.WIND_ANGLE:
      case this.measurement.GUST_ANGLE:
        return '°'
      default:
        return ''
    }
  },
  getWindUnitLabel (unit) {
    switch (unit) {
      case 'KMH':
        return 'km/h'
      case 'MS':
        return 'm/s'
      case 'MPH':
        return 'mph'
      case 'BFT':
        return 'Bft'
      case 'KT':
        return 'kt'
      default:
        return ''
    }
  },
  getDirection (value) {
    if (value < 11.25) return 'N'
    if (value < 33.75) return 'NNE'
    if (value < 56.25) return 'NE'
    if (value < 78.75) return 'ENE'
    if (value < 101.25) return 'E'
    if (value < 123.75) return 'ESE'
    if (value < 146.25) return 'SE'
    if (value < 168.75) return 'SSE'
    if (value < 191.25) return 'S'
    if (value < 213.75) return 'SSW'
    if (value < 236.25) return 'SW'
    if (value < 258.75) return 'WSW'
    if (value < 281.25) return 'W'
    if (value < 303.75) return 'WNW'
    if (value < 326.25) return 'NW'
    if (value < 348.75) return 'NNW'
    return 'N'
  },
  getCO2Status (value) {
    if (!value || value === 'undefined' || value < 0) return 'undefined'
    if (value >= this.config.thresholdCO2Bad) return 'bad'
    if (value >= this.config.thresholdCO2Average) return 'average'
    return 'good'
  },
  getIcon (dataType, value) {
    switch (dataType) {
      // case this.measurement.CO2:
      //   return 'fa-lungs'
      case this.measurement.NOISE:
        return 'fa-volume-up'
      case this.measurement.HUMIDITY:
        return 'fa-tint'
      case this.measurement.PRESSURE:
        return 'fa-tachometer-alt'
      case this.measurement.GUST_STRENGTH:
      case this.measurement.WIND_STRENGTH:
        return 'fa-wind'
      // case this.measurement.GUST_ANGLE:
      // case this.measurement.WIND_ANGLE:
      case this.measurement.PRESSURE_TREND:
      case this.measurement.TEMPERATURE_TREND:
        return this.getTrendIcon(value)
      case 'wifi':
        return 'fa-wifi'
      case 'radio':
        return 'fa-broadcast-tower'
      case 'battery':
        return this.getBatteryIcon(value)
      default:
        return ''
    }
  },
  getTrendIcon (value) {
    if (value === 'stable') return 'fa-chevron-circle-right'
    if (value === 'down') return 'fa-chevron-circle-down'
    if (value === 'up') return 'fa-chevron-circle-up'
    if (value === 'undefined') return 'fa-times-circle'
  },
  getBatteryIcon (value) {
    if (value > 80) return 'fa-battery-full'
    if (value > 60) return 'fa-battery-three-quarters'
    if (value > 40) return 'fa-battery-half'
    if (value > 20) return 'fa-battery-quarter'
    return 'fa-battery-empty flash red'
  },
  getStyles () {
    return [`${this.name}.${this.config.design}.css`]
  },
  getTemplate () {
    return `${this.name}.${this.config.design}.njk`
  },
  getTemplateData () {
    return {
      loaded: this.loaded,
      showLastMessage: this.config.showLastMessage,
      showBattery: this.config.showBattery,
      showRadio: this.config.showRadio,
      showWiFi: this.config.showWiFi,
      showTrend: this.config.showTrend,
      showMeasurementIcon: this.config.showMeasurementIcon,
      showMeasurementLabel: this.config.showMeasurementLabel,
      showModuleNameOnTop: this.config.showModuleNameOnTop,
      horizontal: this.config.horizontal,
      moduleList: this.moduleList,
      fontClassModuleName: this.config.fontClassModuleName,
      fontClassPrimary: this.config.fontClassPrimary,
      fontClassSecondary: this.config.fontClassSecondary,
      fontClassMeasurement: this.config.fontClassMeasurement,
      labelLoading: this.translate('LOADING'),
    }
  },
  getTranslations () {
    return {
      en: 'l10n/en.json', // fallback language
      cs: 'l10n/cs.json',
      de: 'l10n/de.json',
      fr: 'l10n/fr.json',
      hu: 'l10n/hu.json',
      nb: 'l10n/nb.json',
      nn: 'l10n/nn.json',
      ru: 'l10n/ru.json',
      sv: 'l10n/sv.json',
    }
  },
  socketNotificationReceived (notification, payload) {
    Log.debug(`Netatmo: received ${notification}`)
    switch (notification) {
      case this.notifications.AUTH_RESPONSE:
        if (payload.status === 'OK') {
          console.log('Netatmo: AUTH OK')
          this.sendSocketNotification(this.notifications.DATA)
        } else {
          console.error(`Netatmo: AUTH FAILED ${payload.message}`)
        }
        break
      case this.notifications.DATA_RESPONSE:
        if (payload.status === 'OK') {
          console.log('Devices %o', payload.payloadReturn)
          const stationList = payload.payloadReturn
          this.updateModuleList(stationList)
          this.updateDom(this.config.animationSpeed)
        } else if (payload.status === 'INVALID_TOKEN') {
          // node_module has no valid token, reauthenticate
          console.error('DATA FAILED, refreshing token')
          // i'm not agree with this... can have error 403 loop
          // --> managed with node_helper
          // this.sendSocketNotification(this.notifications.AUTH)
        } else {
          console.error(`Netatmo: DATA FAILED ${payload.message}`)
        }
        break
    }
  },
})
@CFenner
Copy link
Owner

CFenner commented Jun 24, 2024

@Duhlin12 thanks for the suggestion, I added your code to #387, would you mind testing it?

You can specify a uom by setting unitOfMeasurementWind in the config to one of MS,KT,MPH,BFT,KPH. If you remove the setting, it should use the one that you configured in your Netatmo app.

@CFenner
Copy link
Owner

CFenner commented Jun 24, 2024

@all-contributors please add @Duhlin12 for code

Copy link
Contributor

@CFenner

I've put up a pull request to add @Duhlin12! 🎉

@CFenner CFenner closed this as completed Jun 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants