Для обмена с системой СитиПоинт через API предлагаем следующую стратегию.

Варианты обмена могут быть и другие - в зависимости от специфики автопарка и бизнес-требований.

1. Повтор запросов

В обязательном порядке запрашивающая сторона должна реализовать паттерн retry в виде повторной отправки запроса через 2-3-10 секунд на случай если API СитиПоинт


2. Авторизация

2. Сверка справочников автомобилей

3. Получение текущего состояния


4. Получение событий 

5. Слияние событий

Предполагается что события после запроса сохраняются в некоторой локальной базе данных (БД)

  1. Запрашиваем события в период с DateFrom по DateTo с сервера и упорядочиваем их по дате начала.
  2. Для каждого типа события выполняем
    1. Из БД запрашиваем события данного типа у которых EndRecordDate больше DateFrom запроса.
    2. Для каждого события из БД (далее событие БД - BaseEvent)
      1. Если BaseEvent.BeginRecordDate события больше либо равен DateFrom запроса, то удаляем данное событие из БД.
      2. Иначе, если в запросе есть события данного типа
        1. Берём первое событие с таким же типом из запроса (далее событие из запроса - RequestEvent).
        2. В BaseEvent.EndRecordDate записываем значение RequestEvent.EndRecordDate.
        3. Объеденияем ExtraData событий
        4. Удаляем RequestEvent из списка событий полученных из запроса.
    3. Все оставшиеся события из запроса добавляем в БД.

Объединение ExtraData:

  1. Если в событиях есть DriverKeyNumber то он всегда берётся из более раннего события (BaseEvent)
  2. Если в событиях есть Sensors то .
    1. Записываем все значения переданные в  RequestEvent.ExtraData.Sensors в BaseEvent.ExtraData.Sensors, если в BaseEvent.ExtraData.Sensors есть поля которых нет в RequestEvent.ExtraData.Sensors то они остаются неизменными.
  3. Если в событиях есть SpeedLimit то в BaseEvent.ExtraData записывается минимальный SpeedLimit из обоих событий.
  4. Если в событиях есть MaxSpeed то в BaseEvent.ExtraData записывается максимальные MaxSpeed из обоих событий
  5. Остальные поля ExtraData всегда переносятся из RequestEvent


Пример алгоритма слияния событий (без слияния ExtraData)

class Event:
    """Класс описывающий событие"""
    __slots__ = 'type', 'date_from', 'date_to'

    def __init__(self, type, date_from, date_to):
        self.date_from = date_from # дата начала события
        self.date_to = date_to     # дата окончания события
        self.type = type           # тип события

    def intersects(self, obj):
        """ Метод проверяет что данное событие пересекается по времени с другим событием или периодом запроса"""
        return self.date_from <= obj.date_to and self.date_to >= obj.date_from

    def within(self, obj):
        """ Метод проверяет что данное событие полностью входит в другое событие или период запроса"""
        return self.date_from >= obj.date_from and self.date_to <= obj.date_to


#################################################

class Storage:
    """
      Класс для хранения событий полученных из БД или запроса
    """

    __slots__ = 'date_from', 'date_to', 'events'

    def __init__(self, events, date_from, date_to):
        self.events = events        # массив событий, всегда упорядочен по дате начала события
        self.date_from = date_from  # для запроса - дата с которой запрашивались события
        self.date_to = date_to      # для запроса - дата до которой запрашивались события

    def remove(self, event):
        self.events.remove(event)

    def add(self, event):
        self.events.append(event)
        self.events.sort(key = lambda ev: ev.date_from)

    def get_all_event_types(self):
        types = set()
        for ev in self.events:
            if ev.type not in types:
                types.add(ev.type)

        return types

    def get_all(self):
        return self.events

    def get_events_with_type(self, type):
        return [ ev for ev in self.events if ev.type == type ]

    def get_first_event_with_type(self, type):
        for ev in self.events:
            if ev.type == type:
                return ev

#################################################

def merge_type(base, request, ev_type):
    """
    Метод для слияния событий типа ev_type.
	   base - все события полученные из локальной БД
       request - все события полученные из запроса
       ev_type - события какого типа сливаются.
    Результат слияния будет в base.
    """
    events = base.get_events_with_type(ev_type)
    for ev in events:
        if ev.within(request):
            """
            Если событие находится внутри запрошенного интервала то его можно безопасно удалить.
            Позже оно будет вновь добавлено из запрошенных данных.
            """
            base.remove(ev)

        elif ev.date_from < request.date_from:
            """
            Случай когда событие началось до запрошенного интервала и продолжается в нём
            """

            new_ev = request.get_first_event_with_type(ev_type)
            """
            Данные внутри base и request упорядоченны по date_from, имеет смысл просматривать только первые события одного типа
            """
            if new_ev is None:
                """
                Если не удалось найти это событие в новых данных, то это не означает что его небыло совсем
                   Некотрым событиям нужно чтобы было несколько пакетов для их фиксации (прим overspeed регистрируется только при наличии хотя бы 3х пакетов)
                   Возможна ситуация когда превышение не было зарегестрированно в запросе из-за того что один из пакетов находился до нового периода.
                   Такие события будут обработаны (удалены / слиты) в запросах с увеличенным периодом (запрос за 24 часа).
                """
                continue

            elif ev.intersects(new_ev):
                """
                В случае если старое событие пересекается с событием из нового периода, то в старом сдвигаем дату окончания события.
                     API считает события на основе данных в истории событий за запрошенный период. 
                     Для событий которые начались до указанного периода будет дата первого пакета из истории а не фактическая дата начала события, поэтому меняется только дата окончания.
                """
                ev.date_to = new_ev.date_to
                request.remove(new_ev)

    for ev in request.get_events_with_type(ev_type):
        """Переносим в бд все оставшиеся события"""
        base.add( ev )

def merge( base, request ):
    """
    Метод для слияния событий. 
    	base - все события полученные из локальной БД у которых дата окончания события больше даты DateFrom из запроса к api.
        request - все события полученные из запроса    
    Результат слияния будет в base.
    """
    all_types = base.get_all_event_types() | request.get_all_event_types()
	for type in all_types:
        merge_type(base, request, type)


6. Получение исторических данных

7. Получение расхода топлива за период.

  1. Получаем списки датчиков топлива и расходов топлива


    GET https://api.url/v2.1/sensors?filter[sensor]=in(Destination,100, 110)


  2. Получаем датчики которые есть на ТС, для этого можно использовать метод получения текущего состояния

    http://api.url/v2.1/user/{user_id}/cars/states/{car_id}


  3. При наличии датчиков расход топлива (Destination=110) можно считать как разницу между показаниями датчика на начало периода и на конец.

    Прим: Пусть на ТС (id=1) установлен датчик "Расход топлива CAN" (id=67), тогда для расчета расхода топлива с 2021-09-01(Мск) по 2021-10-01 (Мск) нужно1) Переводим часовой пояс в UTC получаем     дата начала 2021-09-01 (мск) = 2021-08-31T21:00:00Z
         дата окончания 2021-10-01 (Мск) = 2021-09-30T21:00:00Z
    2) Получить значение расхода на начало периода
    Запрос: GET https://api.url/v2.1/user/1/cars/1/history/full?sort=-RecordDate&filter[histState]=and(exists(Sensors,67),lte(RecordDate,2021-08-31T21:00:00Z))&fields[histStat]=Sensors.67,RecordDate&limit[page]=1&with_total_count=false
    Ответ:
    {
        "id": "1634785201000000000",
        "type": "histState",
        "attributes": {
            "RecordDate": "2021-08-31T20:49:51Z",
            "Sensors": [
                {
                    "id": 67,
                    "value": 25580
                }
            ]
        }
    }
    3) Получить значение расхода на конец периода
    Запрос: GET https://api.url/v2.1/user/1/cars/1/history/full?sort=-RecordDate&filter[histState]=and(exists(Sensors,67),lt(RecordDate,2021-09-30T21:00:00Z))&fields[histStat]=Sensors.67,RecordDate&limit[page]=1&with_total_count=false
    Ответ:
    {
        "id": "1634785201000000000",
        "type": "histState",
        "attributes": {
            "RecordDate": "2021-08-31T20:49:51Z",
            "Sensors": [
                {
                    "id": 67,
                    "value": 26168
                }
            ]
        }
    }
    
    Расход за месяц будет равен 26168 - 25580 = 588 л.

    4. Если на ТС нет датчиков расхода топлива, но есть датчики уровня топлива (Destination=100). Тогда расход топлива = Сумма по всем датчикам(показание на начало периода - показание на конец периода) + Сумма всех заправок - Сумма всех сливов. Если нужно не чистый раход топлива на движение / работу а всё топлива которое было потрачено, то сливы вычетать не нужно


  4. Пусть на ТС (id=1) установлено 3 датчика 
    1. Топливный датчик 1 (id=12)
    2. Топливный датчик 2 (id=68)
    3. Топливный датчик CAN (id=55)
    тогда для расчета расхода топлива с 2021-09-01(Мск) по 2021-10-01 (Мск) нужно
    1) Переводим часовой пояс в UTC получаем
         дата начала 2021-09-01 (мск) = 2021-08-31T21:00:00Z
         дата окончания 2021-10-01 (Мск) = 2021-09-30T21:00:00Z
    2) Запрашиваем последние известные данные на начало периода по любому из датчиков
    GET https://api.url/v2.1/user/1/cars/1/history/full?sort=-RecordDate&filter[histState]=and(exists(Sensors,12,68,55),lte(RecordDate,2021-08-31T21:00:00Z))&fields[histStat]=Sensors.12,Sensors.68,Sensors.55,RecordDate&limit[page]=1&with_total_count=false
    Ответ:
    {
        "id": "1634785201000000000",
        "type": "histState",
        "attributes": {
            "RecordDate": "2021-08-31T20:49:51Z",
            "Sensors": [
                {
                    "id": 12,
                    "value": 24
                },
                {
                    "id": 68,
                    "value": 67
                }
            ]
        }
    }
    3) Если одного или нескольких из датчиков (в примере 55) нет в ответе то его нужно запросить отдельно.
    GET https://api.url/v2.1/user/1/cars/1/history/full?sort=-RecordDate&filter[histState]=and(exists(Sensors,12,68,55),lte(RecordDate,2021-08-31T21:00:00Z))&fields[histStat]=Sensors.12,Sensors.68,Sensors.55,RecordDate&limit[page]=1&with_total_count=false
    Ответ:
    {
        "id": "1634785201000000000",
        "type": "histState",
        "attributes": {
            "RecordDate": "2021-08-31T20:47:51Z",
            "Sensors": [
                {
                    "id": 55,
                    "value": 110
                }
            ]
        }
    }
    4) Получаем данные на конец периода
    Запрос: GET https://api.url/v2.1/user/1/cars/1/history/full?sort=-RecordDate&filter[histState]=and(exists(Sensors,12,68,55),lt(RecordDate,2021-09-30T21:00:00Z))&fields[histStat]=Sensors.12,Sensors.68,Sensors.55,RecordDate&limit[page]=1&with_total_count=false
    Ответ:
    {
        "id": "1634785201000000000",
        "type": "histState",
        "attributes": {
            "RecordDate": "2021-08-31T20:49:51Z",
            "Sensors": [
                {
                    "id": 12,
                    "value": 22
                },
                {
                    "id": 68,
                    "value": 77
                },
                {
                    "id": 55,
                    "value": 120
                }
            ]
        }
    }
    5) Если одного или нескольких датчиков нет в ответе, то нужно запросить его отдельно.
    6) Получаем все события заправок (fueling) и сливов (drain) за указанный период.
    Запрос: GET https://api.url/v2.1/user/1/cars/1/history/events/2021-08-31T21:00:00Z/2021-09-30T21:00:00Z?filter[history_event]=in(Type,'fueling','drain')
    Ответ:
       "data": [
            {
                "id": "1627897795018180000.fueling",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-09-02T09:49:55Z",
                    "EndRecordDate": "2021-09-02T09:53:35Z",
                    "BeginRecordId": "1627897795018180000",
                    "EndRecordId": "1627898015018200000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.9680443,
                        "Lat": 55.7403755
                    },
                    "EndCoords": {
                        "Lon": 37.9679985,
                        "Lat": 55.7403221
                    },
                    "ExtraData": {
                        "RefilledVolume": 49.33
                    }
                }
            },
            {
                "id": "1628010815039700000.fueling",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-09-13T17:13:35Z",
                    "EndRecordDate": "2021-09-13T17:20:55Z",
                    "BeginRecordId": "1628010815039700000",
                    "EndRecordId": "1628011255039750000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.6203613,
                        "Lat": 55.4579086
                    },
                    "EndCoords": {
                        "Lon": 37.6204224,
                        "Lat": 55.4579735
                    },
                    "ExtraData": {
                        "RefilledVolume": 48.58
                    }
                }
            },
            {
                "id": "1628010815139700000.fueling",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-09-13T17:13:35Z",
                    "EndRecordDate": "2021-09-13T17:20:55Z",
                    "BeginRecordId": "1628010815039700000",
                    "EndRecordId": "1628011255039750000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.6203613,
                        "Lat": 55.4579086
                    },
                    "EndCoords": {
                        "Lon": 37.6204224,
                        "Lat": 55.4579735
                    },
                    "ExtraData": {
                        "RefilledVolume": 68.58
                    }
                }
            },
            {
                "id": "1628010815239700000.fueling",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-09-21T17:13:35Z",
                    "EndRecordDate": "2021-09-21T17:20:55Z",
                    "BeginRecordId": "1628010815039700000",
                    "EndRecordId": "1628011255039750000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.6203613,
                        "Lat": 55.4579086
                    },
                    "EndCoords": {
                        "Lon": 37.6204224,
                        "Lat": 55.4579735
                    },
                    "ExtraData": {
                        "RefilledVolume": 45.73
                    }
                }
            },
            {
                "id": "1628166847003460000.drain",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-08-15T12:34:07Z",
                    "EndRecordDate": "2021-08-15T12:37:58Z",
                    "BeginRecordId": "1628166847003460000",
                    "EndRecordId": "1628167078003490000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.9680595,
                        "Lat": 55.7403564
                    },
                    "EndCoords": {
                        "Lon": 37.9680176,
                        "Lat": 55.7402725
                    },
                    "ExtraData": {
                        "DrainVolume": 10.61
                    }
                }
            },
            {
                "id": "1628166845003460000.drain",
                "type": "history_event",
                "attributes": {
                    "BeginRecordDate": "2021-08-17T12:34:07Z",
                    "EndRecordDate": "2021-08-17T12:37:58Z",
                    "BeginRecordId": "1628166847003460000",
                    "EndRecordId": "1628167078003490000",
                    "Type": "fueling",
                    "BeginCoords": {
                        "Lon": 37.9680595,
                        "Lat": 55.7403564
                    },
                    "EndCoords": {
                        "Lon": 37.9680176,
                        "Lat": 55.7402725
                    },
                    "ExtraData": {
                        "DrainVolume": 3.6
                    }
                }
            },
        ]
    7) Рассчитываем расход топлива: Сумма разниц показаний: (24 + 67 + 110 - 22-77-120) = -18Сумма заправок: 49.33 + 48.58 + 68.58 + 45.73 = 212.22
    Сумма сливов: 10.61 + 3.6 = 14.21
    
    Расход топлива: -18 + 212.22 - 14.21 = 180.01