Ejemplo n.º 1
0
def query_object(sock, parameter):
    # query information about an object ID:
    object_info = R.get_by_name(parameter)

    # construct a byte stream that will send a read command for the object ID we want, and send it
    send_frame = make_frame(command=Command.READ, id=object_info.object_id)
    sock.send(send_frame)

    # loop until we got the entire response frame
    frame = ReceiveFrame()
    while True:
        ready_read, _, _ = select.select([sock], [], [], 2.0)
        if sock in ready_read:
            # receive content of the input buffer
            buf = sock.recv(256)
            # if there is content, let the frame consume it
            if len(buf) > 0:
                frame.consume(buf)
                # if the frame is complete, we're done
                if frame.complete():
                    break
            else:
                # the socket was closed by the device, exit
                sys.exit(1)

    # decode the frames payload
    value = decode_value(object_info.response_data_type, frame.data)
    return value
Ejemplo n.º 2
0
    def __init__(self, oinfo: ObjectInfo, interval: int, last_transmit: datetime = datetime.min,
                 last_arrival: datetime = datetime.min, in_flight: bool = False, is_inventory: bool = False) -> None:
        self.oinfo = oinfo
        self.last_transmit = last_transmit
        self.last_arrival = last_arrival
        self.interval = interval
        self.in_flight = in_flight
        self.is_inventory = is_inventory

        self._payload = make_frame(Command.READ, self.oinfo.object_id)
Ejemplo n.º 3
0
def timeseries2csv(host: str, port: int, output: Optional[str],
                   no_headers: bool, time_zone: str, quiet: bool,
                   resolution: str, count: int, day_before_today: int) -> None:
    '''
    Extract time series data from an RCT device. The tool works similar to the official App, but can be run
    independantly, it is designed to be run from a cronjob or as part of a script.

    The output format is CSV.  If --output is not given, then a name is constructed from the resolution and the current
    date.  Specify "-" to have the tool print the table to standard output, for use with other tools.  Unless
    --no-headers is set, the first line contains the column headers.

    Data is queried into the past, by specifying the latest point in time for which data should be queried.  Thus,
    DAYS_BEFORE_TODAY selects the last second of the day that is the given amount in the past.  0 therefor is the
    incomplete current day, 1 is the end of yesterday etc.

    The device has multiple sampling memories at varying sampling intervals.  The resolution can be selected using
    --resolution, which supports "minutes" (which is at 5 minute intervals), day, month and year.  The amount of time
    to cover (back from the end of DAY_BEFORE_TODAY) can be selected using --count:

    * For --resolution=minute, if DAY_BEFORE_TODAY is 0 it selects the last --count hours up to the current time.

    * For --resolution=minute, if DAY_BEFORE_TODAY is greater than 0, it selects --count days back.

    * For all the other resolutions, --count selects the amount of days, months and years to go back, respectively.

    Note that the tool does not remove extra information: If the device sends more data than was requested, that extra
    data is included.

    Examples:

    * The previous 3 hours at finest resolution: --resolution=minutes --count=3 0

    * A whole day, 3 days ago, at finest resolution: --resolution=minutes --count=24 3

    * 4 Months back, at 1 month resolution: --resolution=month --count=4 0
    '''
    global be_quiet
    be_quiet = quiet

    if count < 1:
        cprint('Error: --count must be a positive integer')
        sys.exit(1)

    timezone = pytz.timezone(time_zone)
    now = datetime.now()

    if resolution == 'minutes':
        oid_names = [
            'logger.minutes_ubat_log_ts', 'logger.minutes_ul3_log_ts',
            'logger.minutes_ub_log_ts', 'logger.minutes_temp2_log_ts',
            'logger.minutes_eb_log_ts', 'logger.minutes_eac1_log_ts',
            'logger.minutes_eext_log_ts', 'logger.minutes_ul2_log_ts',
            'logger.minutes_ea_log_ts', 'logger.minutes_soc_log_ts',
            'logger.minutes_ul1_log_ts', 'logger.minutes_eac2_log_ts',
            'logger.minutes_eac_log_ts', 'logger.minutes_ua_log_ts',
            'logger.minutes_soc_targ_log_ts',
            'logger.minutes_egrid_load_log_ts',
            'logger.minutes_egrid_feed_log_ts', 'logger.minutes_eload_log_ts',
            'logger.minutes_ebat_log_ts', 'logger.minutes_temp_bat_log_ts',
            'logger.minutes_eac3_log_ts', 'logger.minutes_temp_log_ts'
        ]
        # the prefix is cut from the front of individual oid_names to produce the name (the end is cut off, too)
        name_prefix = 'logger.minutes_'
        # one sample every 5 minutes
        timediff = relativedelta(minutes=5)
        # select whole days when not querying the current day
        if day_before_today > 0:
            # lowest timestamp that's of interest
            ts_start = (now - timedelta(days=day_before_today)).replace(
                hour=0, minute=0, second=0, microsecond=0)
            # highest timestamp, we stop when this is reached
            ts_end = ts_start.replace(hour=23,
                                      minute=59,
                                      second=59,
                                      microsecond=0)
        else:
            ts_start = ((now - (now - datetime.min) % timedelta(minutes=30)) - timedelta(hours=count)) \
                .replace(second=0, microsecond=0)
            ts_end = now.replace(second=59, microsecond=0)

    elif resolution == 'day':
        oid_names = [
            'logger.day_ea_log_ts', 'logger.day_eac_log_ts',
            'logger.day_eb_log_ts', 'logger.day_eext_log_ts',
            'logger.day_egrid_feed_log_ts', 'logger.day_egrid_load_log_ts',
            'logger.day_eload_log_ts'
        ]
        name_prefix = 'logger.day_'
        # one sample every day
        timediff = relativedelta(days=1)
        # <count> days
        ts_start = (now - timedelta(days=day_before_today + count)) \
            .replace(hour=0, minute=59, second=59, microsecond=0)
        ts_end = (now - timedelta(days=day_before_today)).replace(
            hour=23, minute=59, second=59, microsecond=0)
    elif resolution == 'month':
        oid_names = [
            'logger.month_ea_log_ts', 'logger.month_eac_log_ts',
            'logger.month_eb_log_ts', 'logger.month_eext_log_ts',
            'logger.month_egrid_feed_log_ts', 'logger.month_egrid_load_log_ts',
            'logger.month_eload_log_ts'
        ]
        name_prefix = 'logger.month_'
        # one sample per month
        timediff = relativedelta(months=1)
        # <count> months
        ts_start = (now - timedelta(days=day_before_today) - relativedelta(months=count)) \
            .replace(day=2, hour=0, minute=59, second=59, microsecond=0)
        if ts_start.year < 2000:
            ts_start = ts_start.replace(year=2000)
        ts_end = (now - timedelta(days=day_before_today)).replace(
            day=2, hour=23, minute=59, second=59, microsecond=0)
    elif resolution == 'year':
        oid_names = [
            'logger.year_ea_log_ts', 'logger.year_eac_log_ts',
            'logger.year_eb_log_ts', 'logger.year_eext_log_ts',
            'logger.year_egrid_feed_log_ts', 'logger.year_egrid_load_log_ts',
            'logger.year_eload_log_ts'
        ]  # , 'logger.year_log_ts']
        name_prefix = 'logger.year_'
        # one sample per year
        timediff = relativedelta(years=1)
        # <count> years
        ts_start = (now - timedelta(days=day_before_today) - relativedelta(years=count)) \
            .replace(month=1, day=2, hour=0, minute=59, second=59, microsecond=0)
        ts_end = (now - timedelta(days=day_before_today)) \
            .replace(month=1, day=2, hour=23, minute=59, second=59, microsecond=0)
    else:
        cprint('Unsupported resolution')
        sys.exit(1)

    if day_before_today < 0:
        cprint('DAYS_BEFORE_TODAY must be a positive number')
        sys.exit(1)
    if day_before_today > 365:
        cprint('DAYS_BEFORE_TODAY must be less than a year ago')
        sys.exit(1)

    oids = [x for x in R.all() if x.name in oid_names]

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.connect((host, port))
    except ConnectionRefusedError:
        cprint('Device refused connection')
        sys.exit(2)

    datetable: Dict[datetime, Dict[str, int]] = {
        dt: dict()
        for dt in datetime_range(ts_start, ts_end, timediff)
    }

    for oid in oids:
        name = oid.name.replace(name_prefix, '').replace('_log_ts', '')
        cprint(f'Requesting {name}')

        # set to true if the current time series reached its end, e.g. year 2000 for "year" resolution
        iter_end = False
        highest_ts = ts_end

        while highest_ts > ts_start and not iter_end:
            cprint(f'\ttimestamp: {highest_ts}')
            sock.send(
                make_frame(command=Command.WRITE,
                           id=oid.object_id,
                           payload=encode_value(DataType.INT32,
                                                int(highest_ts.timestamp()))))

            rframe = ReceiveFrame()
            while True:
                try:
                    rread, _, _ = select.select([sock], [], [], 2)
                except select.error as exc:
                    cprint(f'Select error: {str(exc)}')
                    raise

                if rread:
                    buf = sock.recv(1024)
                    if len(buf) > 0:
                        try:
                            rframe.consume(buf)
                        except FrameCRCMismatch:
                            cprint('\tCRC error')
                            break
                        if rframe.complete():
                            break
                    else:
                        cprint('Device closed connection')
                        sys.exit(2)
                else:
                    cprint('\tTimeout, retrying')
                    break

            if not rframe.complete():
                cprint('\tIncomplete frame, retrying')
                continue

            # in case something (such as a "net.package") slips in, make sure to ignore all irelevant responses
            if rframe.id != oid.object_id:
                cprint(f'\tGot unexpected frame oid 0x{rframe.id:08X}')
                continue

            try:
                _, table = decode_value(DataType.TIMESERIES, rframe.data)
            except (AssertionError, struct.error):
                # the device sent invalid data with the correct CRC
                cprint('\tInvalid data received, retrying')
                continue

            # work with the data
            for t_ts, t_val in table.items():

                # set the "highest" point in time to know what to request next when the day is not complete
                if t_ts < highest_ts:
                    highest_ts = t_ts

                # break if we reached the end of the day
                if t_ts < ts_start:
                    cprint('\tReached limit')
                    break

                # Check if the timestamp fits the raster, adjust depending on the resolution
                if t_ts not in datetable:
                    if resolution == 'minutes':
                        # correct up to one full minute
                        nt_ts = t_ts.replace(second=0)
                        if nt_ts not in datetable:
                            nt_ts = t_ts.replace(second=0,
                                                 minute=t_ts.minute + 1)
                            if nt_ts not in datetable:
                                cprint(
                                    f'\t{t_ts} does not fit raster, skipped')
                                continue
                        t_ts = nt_ts
                    elif resolution in ['day', 'month']:
                        # correct up to one hour
                        nt_ts = t_ts.replace(hour=0)
                        if nt_ts not in datetable:
                            nt_ts = t_ts.replace(hour=t_ts.hour + 1)
                            if nt_ts not in datetable:
                                cprint(
                                    f'\t{t_ts} does not fit raster, skipped')
                                continue
                        t_ts = nt_ts
                datetable[t_ts][name] = t_val

                # year statistics stop at 2000-01-02 00:59:59, so if the year hits 2000 we know we're done
                if resolution == 'year' and t_ts.year == 2000:
                    iter_end = True

    if output is None:
        output = f'data_{resolution}_{ts_start.isoformat("T")}.csv'

    if output == '-':
        fd = sys.stdout
    else:
        filedes, filepath = mkstemp(dir=os.path.dirname(output), text=True)
        fd = open(filedes, 'wt')

    writer = csv.writer(fd)

    names = [
        oid.name.replace(name_prefix, '').replace('_log_ts', '')
        for oid in oids
    ]

    if not no_headers:
        writer.writerow(['timestamp'] + names)

    for bts, btval in datetable.items():
        if btval:  # there may be holes in the data
            writer.writerow([timezone.localize(bts).isoformat('T')] +
                            [str(btval[name]) for name in names])

    if output != '-':
        fd.flush()
        os.fsync(fd.fileno())
        try:
            os.rename(filepath, output)
        except OSError as exc:
            cprint(f'Could not move destination file: {str(exc)}')
            try:
                os.unlink(filepath)
            except Exception:
                cprint(f'Could not remove temporary file {filepath}')
            sys.exit(1)
 def test_invalid_id_too_big(self):
     '''
     Passing in an ID that is too big (more than 4 bytes) should result in an error.
     '''
     with pytest.raises(struct.error):
         make_frame(Command.READ, 0xfffffffff)
 def test_response_standard_nopayload(self, id_in: int, data_out: str) -> None:
     '''
     Create a SendFrame and encode various response commands without payload.
     '''
     assert make_frame(Command.RESPONSE, id_in) == bytearray.fromhex(data_out)
 def test_write_standard_nopayload(self, id_in: int, data_out: str) -> None:
     '''
     Tests the encoding of various write commands without payload.
     '''
     assert make_frame(Command.WRITE, id_in) == bytearray.fromhex(data_out)