matthsc/gigaset-elements-api

View on GitHub
test-data/data-tools.ts

Summary

Maintainability
F
4 days
Test Coverage
import {
  IBaseStationRoot,
  IElementRoot,
  IEndnodesItem,
  IEventRoot,
  IEventsItem,
  IGp02Item,
  IRoom,
  ISensorsItem,
  ISettingsItem,
  ISubelementsItem,
} from "../src/model";
import { GigasetElementsApi } from "../src";

export async function retrieveAndPrepareTestData(
  api: GigasetElementsApi,
  from?: Date | number,
): Promise<[IBaseStationRoot, IElementRoot, IEventRoot]> {
  const baseStations = await api.getBaseStations();
  const elements = await api.getElements();

  if (!from) from = Date.now() - 31 * 24 * 60 * 60 * 1000;
  let to: number = Date.now();
  let allEvents: IEventsItem[] = [];
  let result: IEventRoot;
  const batchSize = 500;
  do {
    result = await api.getEvents(from, to, batchSize);
    const newEvents = result.events;
    if (newEvents.length) {
      allEvents = allEvents.concat(
        quicklyFilterDuplicateEventsByTypeAndElement(newEvents),
      );
      to = Number.parseInt(newEvents[newEvents.length - 1].ts, 10) - 1;
    }
  } while (result.events?.length === batchSize);
  allEvents = quicklyFilterDuplicateEventsByTypeAndElement(allEvents);

  const elementRoot: IEventRoot = { ...result, events: allEvents };
  reduceTestData(baseStations, elements, elementRoot);
  tryStripPersonalDataAndGiveUniqueIds(baseStations, elements, elementRoot);

  return [baseStations, elements, elementRoot];
}

function quicklyFilterDuplicateEventsByTypeAndElement(
  events: IEventsItem[],
): IEventsItem[] {
  const set = new Set<string>();
  return events.filter((e) => {
    const key = e.type + "-" + JSON.stringify(e.o);
    if (set.has(key)) return false;
    set.add(key);
    return true;
  });
}

interface IPersonalDataMaps {
  baseIds: Map<string, string>;
  baseNames: Map<string, string>;
  elementIds: Map<string, string>;
  elementNames: Map<string, string>;
  roomNames: Map<string, string>;
  gp02Ids: Map<string, string>;
  gp02Names: Map<string, string>;
  getOrCreateId: (
    map: Map<string, string>,
    prefix: string,
    id: string,
  ) => string;
}

export function tryStripPersonalDataAndGiveUniqueIds(
  baseStations: IBaseStationRoot,
  elementRoot: IElementRoot,
  eventRoot: IEventRoot,
): void {
  // init
  const maps: IPersonalDataMaps = {
    baseIds: new Map<string, string>(),
    baseNames: new Map<string, string>(),
    elementIds: new Map<string, string>(),
    elementNames: new Map<string, string>(),
    roomNames: new Map<string, string>(),
    gp02Ids: new Map<string, string>(),
    gp02Names: new Map<string, string>(),
    getOrCreateId: (
      map: Map<string, string>,
      prefix: string,
      id: string,
    ): string => {
      // return previously created id
      if (map.has(id)) return map.get(id) as string;

      // generate pseudo-random postfix
      const chars =
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEVFGHIKLMNOPQRSTUVWXYZ";
      let postfix = "";
      for (let i = 0; i < 5; i++)
        postfix += chars[Math.floor(Math.random() * chars.length)];

      // create id, add to map, and return it
      const value = `${prefix}${map.size
        .toString()
        .padStart(3, "0")}-${postfix}`;
      map.set(id, value);
      return value;
    },
  };

  tryStripPersonalDataFromBaseStation(baseStations, maps);
  tryStripPersonalDataFromElement(elementRoot, maps);
  tryStripPersonalDataFromEvents(eventRoot.events, maps);
}

function tryStripPersonalDataFromBaseStation(
  baseStations: IBaseStationRoot,
  maps: IPersonalDataMaps,
) {
  for (const base of baseStations) {
    // update base station
    const { id } = base;
    base.id = maps.getOrCreateId(maps.baseIds, "baseId", id);
    base.friendly_name = maps.getOrCreateId(maps.baseNames, "baseName", id);

    // update elements in endnodes and sensors
    tryStripPersonalDataFromBaseStationElement(base.endnodes, maps);
    tryStripPersonalDataFromBaseStationElement(base.sensors, maps);

    // update endnode_ids in .intrusion_settings
    for (const mode of base.intrusion_settings?.modes ?? []) {
      const keys = Object.keys(mode);
      for (const key of keys) {
        const settings = (
          mode[key as keyof typeof mode] as { settings?: ISettingsItem[] }
        ).settings;
        if (settings) {
          for (const setting of settings) {
            if (setting.endnode_id)
              setting.endnode_id = maps.elementIds.get(
                setting.endnode_id,
              ) as string;
          }
        }
      }
    }
  }
}

function tryStripPersonalDataFromBaseStationElement(
  elements: IEndnodesItem[] | ISensorsItem[],
  maps: IPersonalDataMaps,
) {
  for (const element of elements) {
    const { id } = element;
    element.id = maps.getOrCreateId(maps.elementIds, "elementId", id);
    element.friendly_name = maps.getOrCreateId(
      maps.elementNames,
      "elementName",
      id,
    );
  }
}

function tryStripPersonalDataFromElement(
  elementRoot: IElementRoot,
  maps: IPersonalDataMaps,
) {
  // bs01
  for (const bs01 of elementRoot.bs01) {
    const { id: baseId } = bs01;
    bs01.id = maps.baseIds.get(baseId) as string;
    bs01.friendlyName = maps.baseNames.get(baseId) as string;
    tryStripPersonalDataFromRooms(bs01, maps);

    for (const subelement of bs01.subelements) {
      const [baseId, elementId] = subelement.id.split(".");
      subelement.id = `${maps.baseIds.get(baseId)}.${maps.elementIds.get(
        elementId,
      )}`;
      subelement.friendlyName =
        maps.elementNames.get(elementId) ?? subelement.friendlyName;

      tryStripPersonalDataFromRooms(subelement, maps);
    }
  }
  // gp02
  for (const gp02 of elementRoot.gp02) {
    gp02.id = maps.getOrCreateId(maps.gp02Ids, "gp02Id", gp02.id);
    gp02.friendlyName = maps.getOrCreateId(maps.gp02Names, "gp02Name", gp02.id);
    tryStripPersonalDataFromRooms(gp02, maps);
  }
}

function tryStripPersonalDataFromEvents(
  events: IEventsItem[],
  maps: IPersonalDataMaps,
) {
  for (const event of events) {
    const { source_id, source_type, o } = event;
    const [deviceIdsMap, deviceNamesMap] = getMapsBasedOnSourceType(
      source_type,
      maps,
    );
    event.source_id = deviceIdsMap.get(source_id) ?? event.source_id;
    event.source_name = deviceNamesMap.get(source_id) ?? event.source_name;

    if (!o) continue;
    if (o.id) {
      const elementId = o.id;
      o.id = maps.elementIds.get(elementId) ?? o.id;
      o.friendly_name = maps.elementNames.get(elementId) ?? o.friendly_name;
      tryStripPersonalDataFromRooms(o, maps);
    } else if (o.friendly_name) {
      o.friendly_name = event.source_name;
    }
    if (o.userId) {
      o.userId = o.userId.substring(0, o.userId.lastIndexOf("/")) + "/xxx";
    }
    if (o.configurationLoadedId) {
      o.configurationLoadedId = "0123";
    }
    if (o.basestationFriendlyName) {
      o.basestationFriendlyName = "basestation name";
    }
    if (o.clip) {
      o.clip = "00" + Math.random().toString().substring(2).replace(/^0+/g, "");
    }
    tryStripPersonalDataFromRooms(o, maps);
  }
}

function getMapsBasedOnSourceType(
  source_type: string,
  maps: IPersonalDataMaps,
) {
  switch (source_type) {
    case "gp02":
      return [maps.gp02Ids, maps.gp02Names];
    case "basestation":
    default:
      return [maps.baseIds, maps.baseNames];
  }
}

function tryStripPersonalDataFromRooms(
  objectWithRoomData: unknown,
  maps: IPersonalDataMaps,
) {
  const item = objectWithRoomData as ISubelementsItem;
  const rooms: IRoom[] = [];
  if (item.room) {
    rooms.push(item.room);
  }
  if (item.frontendTags?.room) {
    rooms.push(item.frontendTags.room);
  }
  for (const room of rooms) {
    const roomName = room.roomName || room.friendlyName;
    if (!roomName) continue;

    const getRoomName = () =>
      maps.getOrCreateId(maps.roomNames, "Room ", roomName);
    if (room.roomName) room.roomName = getRoomName();
    if (room.friendlyName) room.friendlyName = getRoomName();
  }
}

export function reduceTestData(
  baseStations: IBaseStationRoot,
  elementRoot: IElementRoot,
  eventRoot: IEventRoot,
) {
  // init
  const elementIdRemapping = new Map<string, ISubelementsItem>();
  const gp02IdRemapping = new Map<string, IGp02Item>();
  const getElementKey = (item: ISubelementsItem): string =>
    `${item.type}-${item.states?.factoryType}-${item.batteryStatus}-${item.calibrationStatus}-${item.connectionStatus}-${item.positionStatus}-${item.firmwareStatus}-${item.smokeChamberFail}-${item.permanentBatteryChangeRequest}-${item.permanentBatteryLow}-${item.smokeChamberFail}-${item.smokeDetected}-${item.smokeDetectorOff}-${item.testRequired}`;
  const getGp02Key = (item: IGp02Item): string => `${item.connectionStatus}`;
  const getEventKey = (item: IEventsItem): string =>
    `${item.type}-${item.o?.type}-${item.o?.factoryType}-${item.o?.call_type}-${item.o?.clip_type}-${item.o?.dialable}-${item.o?.line_type}`;
  const getElementIdFromEvent = (item: IEventsItem): string =>
    `${item.source_id}.${item.o?.id}`;

  // determine unique elements per bs01 / base station
  for (const bs01 of elementRoot.bs01) {
    const uniqueElementTypeMap = new Map<string, ISubelementsItem>();
    for (const subelement of bs01.subelements) {
      const key = getElementKey(subelement);
      if (uniqueElementTypeMap.has(key)) {
        elementIdRemapping.set(
          subelement.id,
          uniqueElementTypeMap.get(key) as ISubelementsItem,
        );
      } else {
        uniqueElementTypeMap.set(key, subelement);
      }
    }
  }
  // determine unique elements per gp02
  const uniqueGp02TypeMap = new Map<string, IGp02Item>();
  for (const gp02 of elementRoot.gp02) {
    const key = getGp02Key(gp02);
    if (uniqueGp02TypeMap.has(key)) {
      gp02IdRemapping.set(gp02.id, uniqueGp02TypeMap.get(key) as IGp02Item);
    } else {
      uniqueGp02TypeMap.set(key, gp02);
    }
  }

  // filter elements
  for (const base of baseStations) {
    base.endnodes = base.endnodes.filter(
      (e) => !elementIdRemapping.has(`${base.id}.${e.id}`),
    );
    base.sensors = base.sensors.filter(
      (s) => !elementIdRemapping.has(`${base.id}.${s.id}`),
    );
  }
  for (const bs01 of elementRoot.bs01) {
    bs01.subelements = bs01.subelements.filter(
      (s) => !elementIdRemapping.has(s.id),
    );
  }
  // filter gp02
  elementRoot.gp02 = elementRoot.gp02.filter((g) => !gp02IdRemapping.has(g.id));

  // remap and filter events
  const uniqueEventTypes = new Set<string>();
  eventRoot.events = eventRoot.events
    .reverse() // oldest first
    .filter((e) => {
      const key = getEventKey(e);
      if (uniqueEventTypes.has(key)) return false;
      uniqueEventTypes.add(key);
      return true;
    })
    .map((e) => {
      const mapped = { ...e };
      if (e.o) {
        const o = (mapped.o = { ...e.o });

        if (o.id) {
          const elementId = getElementIdFromEvent(e);
          if (elementIdRemapping.has(elementId)) {
            const mappedElement = elementIdRemapping.get(
              elementId,
            ) as ISubelementsItem;
            o.id = mappedElement.id.split(".")[1];
            o.friendly_name = mappedElement.friendlyName;
          }
        }

        if (
          mapped.source_type === "gp02" &&
          gp02IdRemapping.has(mapped.source_id)
        ) {
          mapped.source_id = gp02IdRemapping.get(mapped.source_id)
            ?.id as string;
        }
      }
      return mapped;
    })
    .reverse(); // newest first again
}

export function mergeTestData(
  baseStationRoots: IBaseStationRoot[],
  elementRoots: IElementRoot[],
  eventRoots: IEventRoot[],
): [IBaseStationRoot, IElementRoot, IEventRoot] {
  // sort base stations, assuming tryStripPersonalDataAndGiveUniqueIds has run before
  baseStationRoots.forEach((r) => {
    r.sort((a, b) => a.friendly_name.localeCompare(b.friendly_name));
  });
  elementRoots.forEach((r) => {
    r.bs01.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
    r.bs02.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
    r.gp01.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
    r.gp02.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
    r.yc01.sort((a, b) => a.friendlyName.localeCompare(b.friendlyName));
  });

  // merge base stations, including elements
  const mergedBaseStationRoot = mergeBaseStationRoots(baseStationRoots);
  // merge elements, fix ids
  const mergedElementsRoot = mergeElementsRoots(elementRoots);
  // merge events, fix ids
  const mergedEventRoot = mergeEventRoots(
    eventRoots,
    baseStationRoots,
    mergedBaseStationRoot,
    elementRoots,
    mergedElementsRoot,
  );

  // reduce data, and run tryStripPersonalDataAndGiveUniqueIds again
  reduceTestData(mergedBaseStationRoot, mergedElementsRoot, mergedEventRoot);
  tryStripPersonalDataAndGiveUniqueIds(
    mergedBaseStationRoot,
    mergedElementsRoot,
    mergedEventRoot,
  );
  return [mergedBaseStationRoot, mergedElementsRoot, mergedEventRoot];
}

function mergeEventRoots(
  eventRoots: IEventRoot[],
  baseStationRoots: IBaseStationRoot[],
  mergedBaseStationRoot: IBaseStationRoot,
  elementRoots: IElementRoot[],
  mergedElementRoots: IElementRoot,
) {
  const mergedEventRoot = eventRoots[0];
  for (let i = 1; i < eventRoots.length; i++) {
    mergedEventRoot.events.push(
      ...eventRoots[i].events.map((e) => {
        switch (e.source_type) {
          case "gp02": {
            const gp02Index = elementRoots[i].gp02.findIndex(
              (r) => r.id === e.source_id,
            );
            if (gp02Index < 0) return e;

            const element = mergedElementRoots.gp02[gp02Index];
            const newEvent: IEventsItem = {
              ...e,
              source_id: element.id,
            };
            if (newEvent.o?.friendly_name && !newEvent.o?.id)
              newEvent.o.friendly_name = element.friendlyName;
            return newEvent;
          }
          case "basestation":
          default: {
            const baseIndex = baseStationRoots[i].findIndex(
              (r) => r.id === e.source_id,
            );
            if (baseIndex < 0) return e;

            const base = mergedBaseStationRoot[baseIndex];
            const newEvent: IEventsItem = {
              ...e,
              source_id: base.id,
              source_name: base.friendly_name,
            };
            if (newEvent.o?.friendly_name && !newEvent.o?.id)
              newEvent.o.friendly_name = base.friendly_name;
            return newEvent;
          }
        }
      }),
    );
  }
  // sort events
  mergedEventRoot.events.sort((a, b) => b.ts.localeCompare(a.ts));
  return mergedEventRoot;
}

function mergeElementsRoots(elementRoots: IElementRoot[]) {
  const mergedElementsRoot = elementRoots[0];
  for (let i = 1; i < elementRoots.length; i++) {
    const currentElementRoot = elementRoots[i];
    // bs01
    for (let j = 0; j < currentElementRoot.bs01.length; j++) {
      const currentBs01 = currentElementRoot.bs01[j];
      if (j >= mergedElementsRoot.bs01.length) {
        mergedElementsRoot.bs01.push(currentBs01);
      } else {
        mergedElementsRoot.bs01[j].subelements.push(
          ...currentBs01.subelements.map((e) => ({
            ...e,
            id: mergedElementsRoot.bs01[j].id + "." + e.id.split(".")[1],
          })),
        );
      }
    }
    // gp02
    for (let j = 0; j < currentElementRoot.gp02.length; j++) {
      const currentGp02 = currentElementRoot.gp02[j];
      if (j >= mergedElementsRoot.gp02.length)
        mergedElementsRoot.gp02.push(currentGp02);
    }
  }
  return mergedElementsRoot;
}

function mergeBaseStationRoots(baseStationRoots: IBaseStationRoot[]) {
  const mergedBaseStationRoot = baseStationRoots[0];
  for (let i = 1; i < baseStationRoots.length; i++) {
    const currentBaseStationRoot = baseStationRoots[i];
    for (let j = 0; j < baseStationRoots[i].length; j++) {
      const currentBaseStation = currentBaseStationRoot[j];
      if (j >= mergedBaseStationRoot.length) {
        mergedBaseStationRoot.push(currentBaseStation);
      } else {
        mergedBaseStationRoot[j].endnodes.push(...currentBaseStation.endnodes);
        mergedBaseStationRoot[j].sensors.push(...currentBaseStation.sensors);
      }
    }
  }
  return mergedBaseStationRoot;
}