/*
;    Project:       Open Vehicle Monitor System
;    Date:          14th March 2017
;
;    Changes:
;    1.0  Initial release
;
;    (C) 2011       Michael Stegen / Stegen Electronics
;    (C) 2011-2017  Mark Webb-Johnson
;    (C) 2011        Sonny Chen @ EPRO/DX
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
*/

#include "ovms_log.h"
static const char *TAG = "notify";

#include <stdlib.h>
#include <stdio.h>
#include <sstream>
#include "ovms.h"
#include "ovms_notify.h"
#include "ovms_command.h"
#include "ovms_config.h"
#include "ovms_events.h"
#include "ovms_script.h"
#include "vehicle.h"
#include "buffered_shell.h"
#include "string.h"
#include "ovms_mutex.h"

using namespace std;

OvmsNotify       MyNotify       __attribute__ ((init_priority (1820)));

// Tracing:
//  level 0 = no tracing (logging)
//  level 1 = trace standard (text & data) notifications
//  level 2 = also trace stream notifications
#define DO_TRACE(type) (MyNotify.m_trace == 2 || (MyNotify.m_trace == 1 && strcmp((type), "stream") != 0))


////////////////////////////////////////////////////////////////////////
// Console commands...

void notify_trace(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (strcmp(cmd->GetName(),"all")==0)
    MyNotify.m_trace = 2;
  else if (strcmp(cmd->GetName(),"on")==0)
    MyNotify.m_trace = 1;
  else
    MyNotify.m_trace = 0;

  writer->printf("Notification tracing is now %s\n",cmd->GetName());
  }

void notify_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  OvmsRecMutexLock lock(&MyNotify.m_mutex);
  writer->printf("Notification system has %d readers registered\n",
    MyNotify.CountReaders());
  for (OvmsNotifyCallbackMap_t::iterator itc=MyNotify.m_readers.begin(); itc!=MyNotify.m_readers.end(); itc++)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    writer->printf("  %s(%d): verbosity=%d\n", mc->m_caller, mc->m_reader, mc->m_verbosity);
    }

  if (MyNotify.m_types.size() > 0)
    {
    writer->puts("Notify types:");
    for (OvmsNotifyTypeMap_t::iterator itm=MyNotify.m_types.begin(); itm!=MyNotify.m_types.end(); ++itm)
      {
      OvmsNotifyType* mt = itm->second;
      OvmsRecMutexLock lock(&mt->m_mutex);
      writer->printf("  %s: %d entries\n",
        mt->m_name, mt->m_entries.size());
      for (NotifyEntryMap_t::iterator ite=mt->m_entries.begin(); ite!=mt->m_entries.end(); ++ite)
        {
        OvmsNotifyEntry* e = ite->second;
        writer->printf("    %d: [%d pending] %s\n",
          ite->first, e->CountPending(), e->GetValue().c_str());
        }
      }
    }
  }

void notify_raise(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  writer->printf("Raise %s notification for %s/%s as %s\n",
    cmd->GetName(), argv[0], argv[1], argv[2]);

  if (strcmp(cmd->GetName(),"text")==0)
    MyNotify.NotifyString(argv[0],argv[1],argv[2]);
  else if (strcmp(cmd->GetName(),"command")==0)
    MyNotify.NotifyCommand(argv[0],argv[1],argv[2]);
  else
    MyNotify.NotifyErrorCode(atol(argv[0]),atol(argv[1]),(strcmp(argv[2],"yes")==0));
  }

void notify_errorcode_list(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  for (OvmsNotifyErrorCodeMap_t::iterator it=MyNotify.m_errorcodes.begin(); it!=MyNotify.m_errorcodes.end(); ++it)
    {
    writer->printf("%u(0x%04.4x) %s raised %d, updated %d, sec(s) ago\n",
      it->first,
      it->second->lastdata,
      (it->second->active)?"active":"cleared",
      monotonictime-it->second->raised,
      monotonictime-it->second->updated);
    }
  }

void notify_errorcode_clear(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  for (OvmsNotifyErrorCodeMap_t::iterator it=MyNotify.m_errorcodes.begin(); it!=MyNotify.m_errorcodes.end(); ++it)
    {
    delete it->second;
    }
  MyNotify.m_errorcodes.clear();
  }

#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

static duk_ret_t DukOvmsNotifyRaise(duk_context *ctx)
  {
  const char *type = duk_to_string(ctx,0);
  const char *subtype = duk_to_string(ctx,1);
  const char *message = duk_to_string(ctx,2);

  if (type && subtype && message)
    {
    uint32_t id = MyNotify.NotifyString(type, subtype, message);
    duk_push_uint(ctx, id);
    }
  else
    {
    duk_push_uint(ctx, 0);
    }
  return 1;  /* one return value */
  }

#endif //#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

////////////////////////////////////////////////////////////////////////
// OvmsNotifyEntry is the virtual object for notification entries.
// These are added to a particular OvmsNotifyType (as a member of a list)
// and readers are notified. Once a reader has processed an entry, the
// MarkRead function is called. The framework can test IsAllRead() to
// see if all readers have processed the entry and housekeep cleanup
// appropriately.

OvmsNotifyEntry::OvmsNotifyEntry(const char* subtype)
  {
  m_pendingreaders = 0;
  m_id = 0;
  m_created = esp_log_timestamp();
  m_type = NULL;
  m_subtype = strdup(subtype);
  }

OvmsNotifyEntry::~OvmsNotifyEntry()
  {
  if (m_subtype) free(m_subtype);
  }

bool OvmsNotifyEntry::IsRead(size_t reader)
  {
  return !(m_pendingreaders & 1ul << reader);
  }

int OvmsNotifyEntry::CountPending()
  {
  int cnt = 0;
  unsigned long pend = m_pendingreaders;
  for (int i=0; i<NOTIFY_MAX_READERS; i++)
    {
    cnt += pend & 1;
    pend >>= 1;
    }
  return cnt;
  }

bool OvmsNotifyEntry::IsAllRead()
  {
  return (m_pendingreaders == 0);
  }

const extram::string OvmsNotifyEntry::GetValue()
  {
  return extram::string("");
  }

const char* OvmsNotifyEntry::GetSubType()
  {
  return m_subtype;
  }

////////////////////////////////////////////////////////////////////////
// OvmsNotifyEntryString is the notification entry for a constant
// string type.

OvmsNotifyEntryString::OvmsNotifyEntryString(const char* subtype, const char* value)
  : OvmsNotifyEntry(subtype)
  {
  m_value = extram::string(value);
  }

OvmsNotifyEntryString::~OvmsNotifyEntryString()
  {
  }

const extram::string OvmsNotifyEntryString::GetValue()
  {
  return m_value;
  }

////////////////////////////////////////////////////////////////////////
// OvmsNotifyEntryCommand is the notification entry for a command
// callback type.

OvmsNotifyEntryCommand::OvmsNotifyEntryCommand(const char* subtype, int verbosity, const char* cmd)
  : OvmsNotifyEntry(subtype)
  {
  m_cmd = new char[strlen(cmd)+1];
  strcpy(m_cmd,cmd);

  BufferedShell* bs = new BufferedShell(false, verbosity);
  // command notifications can only be raised by the system or "notify raise" in enabled mode,
  // so we can assume this is a secure shell:
  bs->SetSecure(true);
  bs->ProcessChars(m_cmd, strlen(m_cmd));
  bs->ProcessChar('\n');
  bs->Dump(m_value);
  delete bs;
  }

OvmsNotifyEntryCommand::~OvmsNotifyEntryCommand()
  {
  if (m_cmd)
    {
    delete [] m_cmd;
    m_cmd = NULL;
    }
  }

const extram::string OvmsNotifyEntryCommand::GetValue()
  {
  return m_value;
  }

////////////////////////////////////////////////////////////////////////
// OvmsNotifyType is the container for an ordered list of
// OvmsNotifyEntry objects (being the notification data queued)

OvmsNotifyType::OvmsNotifyType(const char* name)
  {
  m_name = name;
  m_nextid = 1;
  }

OvmsNotifyType::~OvmsNotifyType()
  {
  }

uint32_t OvmsNotifyType::QueueEntry(OvmsNotifyEntry* entry)
  {
  OvmsRecMutexLock lock(&m_mutex);
  uint32_t id = m_nextid++;

  entry->m_id = id;
  entry->m_type = this;
  m_entries[id] = entry;

  if (strcmp(m_name, "data") != 0 &&
      strcmp(m_name, "stream") != 0)
    {
    std::string event("notify.");
    event.append(m_name);
    event.append(".");
    event.append(entry->m_subtype);
    MyEvents.SignalEvent(event, (void*)id);
    }

  // Dispatch the callbacks...
  MyNotify.NotifyReaders(this, entry);

  // Check if we can cleanup...
  Cleanup(entry);

  return id;
  }

uint32_t OvmsNotifyType::AllocateNextID()
  {
  OvmsRecMutexLock lock(&m_mutex);
  return m_nextid++;
  }

void OvmsNotifyType::ClearReader(size_t reader)
  {
  OvmsRecMutexLock lock(&m_mutex);
  if (m_entries.size() > 0)
    {
    NotifyEntryMap_t::iterator next;
    for (NotifyEntryMap_t::iterator ite=m_entries.begin(); ite!=m_entries.end(); )
      {
      OvmsNotifyEntry* e = ite->second;
      ++ite;
      e->m_pendingreaders &= ~(1ul << reader);
      Cleanup(e, &ite);
      }
    }
  }

OvmsNotifyEntry* OvmsNotifyType::FirstUnreadEntry(size_t reader, uint32_t floor)
  {
  OvmsRecMutexLock lock(&m_mutex);
  for (NotifyEntryMap_t::iterator ite=m_entries.begin(); ite!=m_entries.end(); ++ite)
    {
    OvmsNotifyEntry* e = ite->second;
    if ((!e->IsRead(reader))&&(e->m_id > floor))
      return e;
    }
  return NULL;
  }

OvmsNotifyEntry* OvmsNotifyType::FindEntry(uint32_t id)
  {
  OvmsRecMutexLock lock(&m_mutex);
  auto k = m_entries.find(id);
  if (k == m_entries.end())
    return NULL;
  else
    return k->second;
  }

void OvmsNotifyType::MarkRead(size_t reader, OvmsNotifyEntry* entry)
  {
  OvmsRecMutexLock lock(&m_mutex);
  entry->m_pendingreaders &= ~(1ul << reader);
  Cleanup(entry);
  }

void OvmsNotifyType::Cleanup(OvmsNotifyEntry* entry, NotifyEntryMap_t::iterator* next /*=NULL*/)
  {
  if (entry->IsAllRead())
    {
    // We can cleanup...
    auto k = m_entries.find(entry->m_id);
    if (k != m_entries.end())
      {
      NotifyEntryMap_t::iterator it = m_entries.erase(k);
      if (next) *next = it;
      }
    if (DO_TRACE(m_name))
      ESP_LOGD(TAG,"Cleanup type %s id %d",m_name,entry->m_id);
    delete entry;
    }
  }

////////////////////////////////////////////////////////////////////////
// OvmsNotifyCallbackEntry contains the callback function for a
// particular reader

OvmsNotifyCallbackEntry::OvmsNotifyCallbackEntry(const char* caller, size_t reader, int verbosity, OvmsNotifyCallback_t callback,
                                                 bool configfiltered/*=true*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/)
  {
  m_caller = caller;
  m_reader = reader;
  m_verbosity = verbosity;
  m_callback = callback;
  m_configfiltered = configfiltered;
  m_filtercallback = filtercallback;
  }

OvmsNotifyCallbackEntry::~OvmsNotifyCallbackEntry()
  {
  }

bool OvmsNotifyCallbackEntry::Accepts(OvmsNotifyType* type, const char* subtype, size_t size)
  {
  // Check size
  if (size > m_verbosity)
    return false;
  // Check filter by config:
  if (m_configfiltered)
    {
    // Config syntax options:
    // a) explicit inclusion: e.g. 'ovmsv2,ovmsv3' (only enable these)
    // b) explicit exclusion: e.g. '*,-ovmsv2,-ovmsv3' (only disable these)
    // '-' to disable all, empty/'*' to enable all
    std::string filter = MyConfig.GetParamValue("notify", subtype);
    if (!filter.empty())
      {
      if (filter[0] == '*')
        {
        if (filter.find(std::string("-")+m_caller) != string::npos)
          return false;
        }
      else if (filter.find(m_caller) == string::npos)
        {
        return false;
        }
      }
    }
  // Check filter by callback:
  if (m_filtercallback)
    {
    if (m_filtercallback(type, subtype) == false)
      return false;
    }
  return true;
  }


////////////////////////////////////////////////////////////////////////
// OvmsNotifyCallbackEntry contains the callback function for a
//

OvmsNotify::OvmsNotify()
  {
  ESP_LOGI(TAG, "Initialising NOTIFICATIONS (1820)");

  m_nextreader = 1;

#ifdef CONFIG_OVMS_DEV_DEBUGNOTIFICATIONS
  m_trace = 1;
#else
  m_trace = 0;
#endif // #ifdef CONFIG_OVMS_DEV_DEBUGNOTIFICATIONS

  MyConfig.RegisterParam("notify", "Notification filters", true, true);

  // Register our commands
  OvmsCommand* cmd_notify = MyCommandApp.RegisterCommand("notify","NOTIFICATION framework", notify_status, "", 0, 0, false);
  cmd_notify->RegisterCommand("status","Show notification status",notify_status);
  OvmsCommand* cmd_notifyraise = cmd_notify->RegisterCommand("raise","NOTIFICATION raise framework");
  cmd_notifyraise->RegisterCommand("text","Raise a textual notification",notify_raise,"<type><subtype><message>", 3, 3);
  cmd_notifyraise->RegisterCommand("command","Raise a command callback notification",notify_raise,"<type><subtype><command>", 3, 3);
  cmd_notifyraise->RegisterCommand("errorcode","Raise an error code notification",notify_raise,"<code><data><raised>", 3, 3);
  OvmsCommand* cmd_notifyerrorcode = cmd_notify->RegisterCommand("errorcode","NOTIFICATION error code framework");
  cmd_notifyerrorcode->RegisterCommand("list","List error codes raised",notify_errorcode_list);
  cmd_notifyerrorcode->RegisterCommand("clear","Clear error code list",notify_errorcode_clear);
  OvmsCommand* cmd_notifytrace = cmd_notify->RegisterCommand("trace","NOTIFICATION trace framework");
  cmd_notifytrace->RegisterCommand("on","Standard notification tracing (text, error & data)",notify_trace);
  cmd_notifytrace->RegisterCommand("all","Full notification tracing (including streams)",notify_trace);
  cmd_notifytrace->RegisterCommand("off","Turn notification tracing OFF",notify_trace);

  RegisterType("info");     // payload: human readable text message
  RegisterType("error");    // payload: "<vehicletype>,<errorcode>,<errordata>"
  RegisterType("alert");    // payload: human readable text message
  RegisterType("data");     // payload: MP historical data record (tagged CSV, see MP documentation)
  RegisterType("stream");   // payload: subtype specific, use for high volume / short latency data streams

#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  ESP_LOGI(TAG, "Expanding DUKTAPE javascript engine");
  DuktapeObjectRegistration* dto = new DuktapeObjectRegistration("OvmsNotify");
  dto->RegisterDuktapeFunction(DukOvmsNotifyRaise, 3, "Raise");
  MyDuktape.RegisterDuktapeObject(dto);
#endif // CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  }

OvmsNotify::~OvmsNotify()
  {
  }

size_t OvmsNotify::RegisterReader(const char* caller, int verbosity, OvmsNotifyCallback_t callback,
                                  bool configfiltered/*=false*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/)
  {
  OvmsRecMutexLock lock(&m_mutex);
  size_t reader = m_nextreader++;

  m_readers[reader] = new OvmsNotifyCallbackEntry(caller, reader, verbosity, callback, configfiltered, filtercallback);

  return reader;
  }

void OvmsNotify::RegisterReader(size_t reader, const char* caller, int verbosity, OvmsNotifyCallback_t callback,
                                bool configfiltered/*=false*/, OvmsNotifyFilterCallback_t filtercallback/*=NULL*/)
  {
  OvmsRecMutexLock lock(&m_mutex);
  m_readers[reader] = new OvmsNotifyCallbackEntry(caller, reader, verbosity, callback, configfiltered, filtercallback);
  }

void OvmsNotify::ClearReader(size_t reader)
  {
  OvmsRecMutexLock lock(&m_mutex);
  auto k = m_readers.find(reader);
  if (k != m_readers.end())
    {
    for (OvmsNotifyTypeMap_t::iterator itt=m_types.begin(); itt!=m_types.end(); ++itt)
      {
      OvmsNotifyType* t = itt->second;
      t->ClearReader(k->second->m_reader);
      }
    OvmsNotifyCallbackEntry* ec = k->second;
    m_readers.erase(k);
    delete ec;
    }
  }

size_t OvmsNotify::CountReaders()
  {
  OvmsRecMutexLock lock(&m_mutex);
  return m_readers.size();
  }

OvmsNotifyType* OvmsNotify::GetType(const char* type)
  {
  auto k = m_types.find(type);
  if (k == m_types.end())
    return NULL;
  else
    return k->second;
  }

void OvmsNotify::NotifyReaders(OvmsNotifyType* type, OvmsNotifyEntry* entry)
  {
  OvmsRecMutexLock lock(&m_mutex);
  for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    if (mc->Accepts(type, entry->GetSubType(), entry->GetValueSize()))
      {
      // deliver notification:
      if (mc->m_callback(type,entry) == true)
        entry->m_pendingreaders &= ~(1ul << mc->m_reader);
      }
    else
      {
      // in case the acceptance filter changed since queueing:
      entry->m_pendingreaders &= ~(1ul << mc->m_reader);
      }
    }
  }

bool OvmsNotify::HasReader(const char* type, const char* subtype, size_t size)
  {
  OvmsRecMutexLock lock(&m_mutex);
  OvmsNotifyType* mt = GetType(type);
  for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    if (mc->Accepts(mt, subtype, size))
      return true;
    }
  return false;
  }

void OvmsNotify::RegisterType(const char* type)
  {
  OvmsNotifyType* mt = GetType(type);
  if (mt == NULL)
    {
    mt = new OvmsNotifyType(type);
    m_types[type] = mt;
    ESP_LOGI(TAG,"Registered notification type %s",type);
    }
  }

uint32_t OvmsNotify::NotifyString(const char* type, const char* subtype, const char* value)
  {
  OvmsRecMutexLock lock(&m_mutex);
  OvmsNotifyType* mt = GetType(type);
  if (mt == NULL)
    {
    ESP_LOGW(TAG, "Notification raised for non-existent type %s: %s", type, value);
    return 0;
    }

  if (DO_TRACE(type))
    ESP_LOGI(TAG, "Raise text %s/%s: %s", type, subtype, value);

  // determine all currently active readers accepting the message:
  std::bitset<NOTIFY_MAX_READERS> readers;
  size_t size = strlen(value);
  for (OvmsNotifyCallbackMap_t::iterator itc=m_readers.begin(); itc!=m_readers.end(); ++itc)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    if (mc->Accepts(mt, subtype, size))
      readers.set(mc->m_reader);
    }
  if (readers.count() == 0)
    {
    ESP_LOGD(TAG, "Abort: no readers for type '%s' subtype '%s' size %d", type, subtype, size);
    return 0;
    }

  // create message:
  OvmsNotifyEntry* msg = (OvmsNotifyEntry*) new OvmsNotifyEntryString(subtype, value);
  msg->m_pendingreaders = readers.to_ulong();

  ESP_LOGD(TAG, "Created entry type '%s' subtype '%s' size %d has %d readers pending", type, subtype, size, readers.count());

  return mt->QueueEntry(msg);
  }

uint32_t OvmsNotify::NotifyCommand(const char* type, const char* subtype, const char* cmd)
  {
  OvmsRecMutexLock lock(&m_mutex);
  OvmsNotifyType* mt = GetType(type);
  if (mt == NULL)
    {
    ESP_LOGW(TAG, "Notification raised for non-existent type %s: %s", type, cmd);
    return 0;
    }

  if (DO_TRACE(type))
    ESP_LOGI(TAG, "Raise command %s/%s: %s", type, subtype, cmd);

  // Strategy:
  //  to minimize RAM usage and command calls we try to reuse higher verbosity messages
  //  if their result length fits for lower verbosity readers as well.

  // get verbosity levels needed by readers accepting the message:
  std::map<int, OvmsNotifyEntryCommand*> verbosity_msgs;
  std::bitset<NOTIFY_MAX_READERS> readers;
  for (auto itc=m_readers.begin(); itc!=m_readers.end(); itc++)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    if (mc->Accepts(mt, subtype))
      {
      verbosity_msgs[mc->m_verbosity] = NULL;
      readers.set(mc->m_reader); // cache acceptance
      }
    }
  if (verbosity_msgs.size() == 0)
    {
    if (DO_TRACE(type))
      {
      // no readers, but tracing enabled, so log command result:
      const int verbosity = COMMAND_RESULT_NORMAL;
      OvmsNotifyEntryCommand *msg = new OvmsNotifyEntryCommand(subtype, verbosity, cmd);
      ESP_LOGI(TAG, "Raise cmdres[%d] %s/%s: %s", verbosity, type, subtype, msg->GetValue().c_str());
      delete msg;
      }
    ESP_LOGD(TAG, "Abort: no readers for type '%s' subtype '%s'", type, subtype);
    return 0;
    }

  // fetch verbosity levels beginning at highest verbosity:
  OvmsNotifyEntryCommand *msg = NULL;
  size_t msglen = 0;
  for (auto ritm=verbosity_msgs.rbegin(); ritm!=verbosity_msgs.rend(); ritm++)
    {
    int verbosity = ritm->first;
    if (msg && msglen <= verbosity)
      {
      // reuse last verbosity level message:
      verbosity_msgs[verbosity] = msg;
      }
    else
      {
      msg = verbosity_msgs[verbosity];
      if (!msg)
        {
        // create verbosity level message:
        msg = new OvmsNotifyEntryCommand(subtype, verbosity, cmd);
        msglen = msg->GetValueSize();
        verbosity_msgs[verbosity] = msg;
        if (DO_TRACE(type))
          ESP_LOGI(TAG, "Raise cmdres[%d] %s/%s: %s", verbosity, type, subtype, msg->GetValue().c_str());
        }
      }
    }

  // add readers:
  for (auto itc=m_readers.begin(); itc!=m_readers.end(); itc++)
    {
    OvmsNotifyCallbackEntry* mc = itc->second;
    if (readers.test(mc->m_reader))
      {
      msg = verbosity_msgs[mc->m_verbosity];
      msg->m_pendingreaders |= (1ul << mc->m_reader);
      }
    }

  // queue all verbosity level messages beginning at lowest verbosity (fastest delivery):
  msg = NULL;
  uint32_t queue_id = 0;
  for (auto itm=verbosity_msgs.begin(); itm!=verbosity_msgs.end(); itm++)
    {
    if (itm->second == msg)
      continue; // already queued
    msg = itm->second;
    ESP_LOGD(TAG, "Created entry type '%s' subtype '%s' verbosity %d has %d readers pending",
             type, subtype, itm->first, msg->CountPending());
    queue_id = mt->QueueEntry(msg);
    }

  return queue_id;
  }


/**
 * NotifyStringf: printf style API
 */
uint32_t OvmsNotify::NotifyStringf(const char* type, const char* subtype, const char* fmt, ...)
  {
  char *buffer = NULL;
  uint32_t res = 0;
  va_list args;
  va_start(args, fmt);
  int len = vasprintf(&buffer, fmt, args);
  va_end(args);
  if (len >= 0)
    {
    res = NotifyString(type, subtype, buffer);
    free(buffer);
    }
  return res;
  }


/**
 * NotifyCommandf: printf style API
 */
uint32_t OvmsNotify::NotifyCommandf(const char* type, const char* subtype, const char* fmt, ...)
  {
  char *buffer = NULL;
  uint32_t res = 0;
  va_list args;
  va_start(args, fmt);
  int len = vasprintf(&buffer, fmt, args);
  va_end(args);
  if (len >= 0)
    {
    res = NotifyCommand(type, subtype, buffer);
    free(buffer);
    }
  return res;
  }

void OvmsNotify::NotifyErrorCode(uint32_t code, uint32_t data, bool raised, bool force)
  {
  // Raise a notification of an error code
  //   <code> error code (key field)
  //   <data> data associated with the error
  //   <raised> true if error is being raised, else false
  //   <force> true if notification should be forced, otherwise auto-suppress dupes

  // Firstly, let's see if we have a notification record for it already
  auto k = m_errorcodes.find(code);
  OvmsNotifyErrorCodeEntry_t* entry;
  if (k == m_errorcodes.end())
    {
    entry = new OvmsNotifyErrorCodeEntry_t;
    entry->raised = monotonictime;
    entry->updated = 0;
    entry->lastdata = 0;
    m_errorcodes[code] = entry;
    }
  else
    {
    entry = k->second;
    }
  entry->active = raised;
  if (raised) entry->lastdata = data;

  // Handle auto-suppression, if necessary
  if (!force && (entry->updated>0) && (monotonictime-entry->updated < NOTIFY_ERROR_AUTOSUPPRESS))
    {
    // We need to auto-suppress
    entry->updated = monotonictime;
    return;
    }

  // OK to raise...
  entry->updated = monotonictime;
  if (raised)
    {
    NotifyStringf("error", "code", "%s,%u,%u",
      MyVehicleFactory.ActiveVehicleType(),
      code,
      data);
    }
  }
