Ejemplo n.º 1
0
def get_co2eq_per_kwh():

    zone = request.args.get("zone", "")

    try:
        # Production seems to be more universally available.
        # If you want a more accurate and reliable results, please use the paid API from https://api.electricitymap.org
        parser = PARSER_KEY_TO_DICT["production"][zone]
    except KeyError as e:
        return error("zone not found", 400)

    try:
        # TODO: cache and/or save to database, right now we'll end up bombarding well meaning public APIs.
        # The current assumption is that the users will only call the endpoint once every 10-20 minutes, so this is fine.
        res = parser(zone, logger=logging.getLogger(__name__))
        if isinstance(res, (list, tuple)):
            # TODO: fix this to use all available data
            # At the time of writing this, I am only trying to get a working API with "close enough data"
            res = res[0]
    except Exception as e:
        return error("error fetching carbon intensity", 500)

    try:
        validate_production(res, zone)
    except ValidationError as e:
        # Log and respond with error
        return error("validation error: {}".format(str(e)), 500)

    co2eq = 0.0
    total_mw = 0.0
    default_co2e_per_mwh = kgco2e_per_mwh["unknown"]
    for (source_type, mw) in res["production"].items():
        # Sum of (emission per megawatt by source * megawatts generated)
        co2eq += kgco2e_per_mwh.get(source_type, default_co2e_per_mwh) * mw
        total_mw += mw

    # Dividing co2eq by total_mw will give us kgco2eq/MW.
    # For the sake of simplicity, we just assume it'll remain constant for 1 hour, so with that magic, it's kgco2eq/mwh
    # Since there's 1000 grams in 1 kg and 1000 kilowatts in 1 Megawatt, the number is same for gco2eq/kwh

    response = {
        "zone": zone,
        "carbonIntensity": math.ceil(co2eq / total_mw),
    }

    if "datetime" in res:
        # While python technically follows only ISO 8601 and seems rfc3339-compatible
        # api.electricitymap.com seems to use the Z notation instead of the +00:00 and we don't want to trip anything up
        timestring = res["datetime"].astimezone(timezone("UTC")).isoformat()[:-6] + "Z"
        response["datetime"] = timestring
        response["updatedAt"] = timestring

    return jsonify(response)
Ejemplo n.º 2
0
 def test_no_zoneKey(self):
     with self.assertRaises(Exception, msg="zoneKey is required!"):
         validate_production(p2, "FR")
Ejemplo n.º 3
0
 def test_no_datetime(self):
     with self.assertRaises(Exception, msg="Datetime key must be present!"):
         validate_production(p1, "FR")
Ejemplo n.º 4
0
 def test_no_datetime(self):
     with self.assertRaises(Exception, msg = 'Datetime key must be present!'):
         validate_production(p1, 'FR')
Ejemplo n.º 5
0
def test_parser(zone, data_type, target_datetime):
    """\b
    Parameters
    ----------
    zone: a two letter zone from the map
    data_type: in ['production', 'exchangeForecast', 'production', 'exchange',
      'price', 'consumption', 'generationForecast', 'consumptionForecast']
    target_datetime: string parseable by arrow, such as 2018-05-30 15:00
    \b
    Examples
    -------
    >>> poetry run test_parser FR
    >>> poetry run test_parser FR production
    >>> poetry run test_parser NO-NO3-\>SE exchange
    >>> poetry run test_parser GE production --target_datetime="2022-04-10 15:00"

    """
    if target_datetime:
        target_datetime = arrow.get(target_datetime).datetime
    start = time.time()

    parser = PARSER_KEY_TO_DICT[data_type][zone]
    if data_type in ["exchange", "exchangeForecast"]:
        args = zone.split("->")
    else:
        args = [zone]
    res = parser(*args,
                 target_datetime=target_datetime,
                 logger=logging.getLogger(__name__))

    if not res:
        raise ValueError("Error: parser returned nothing ({})".format(res))

    elapsed_time = time.time() - start
    if isinstance(res, (list, tuple)):
        res_list = list(res)
    else:
        res_list = [res]

    try:
        dts = [e["datetime"] for e in res_list]
    except:
        raise ValueError(
            "Parser output lacks `datetime` key for at least some of the "
            "ouput. Full ouput: \n\n{}\n".format(res))

    assert all([
        type(e["datetime"]) is datetime.datetime for e in res_list
    ]), "Datetimes must be returned as native datetime.datetime objects"

    last_dt = arrow.get(max(dts)).to("UTC")
    first_dt = arrow.get(min(dts)).to("UTC")
    max_dt_warning = ""
    if not target_datetime:
        max_dt_warning = (" :( >2h from now !!!" if
                          (arrow.utcnow() - last_dt).total_seconds() > 2 * 3600
                          else " -- OK, <2h from now :) (now={} UTC)".format(
                              arrow.utcnow()))

    print("Parser result:")
    pp = pprint.PrettyPrinter(width=120)
    pp.pprint(res)
    print("\n".join([
        "---------------------",
        "took {:.2f}s".format(elapsed_time),
        "min returned datetime: {} UTC".format(first_dt),
        "max returned datetime: {} UTC {}".format(last_dt, max_dt_warning),
    ]))

    if type(res) == dict:
        res = [res]
    for event in res:
        try:
            if data_type == "production":
                validate_production(event, zone)
            elif data_type == "consumption":
                validate_consumption(event, zone)
            elif data_type == "exchange":
                validate_exchange(event, zone)
        except ValidationError as e:
            logger.warning("Validation failed @ {}: {}".format(
                event["datetime"], e))
Ejemplo n.º 6
0
 def test_good_datapoint(self):
     self.assertFalse(validate_production(p9, "FR"),
                      msg="This datapoint is good!")
Ejemplo n.º 7
0
 def test_missing_types_allowed(self):
     self.assertFalse(
         validate_production(p7, "CH"),
         msg="CH, NO, AUS-TAS, US-NEISO don't require Coal/Oil/Unknown!",
     )
Ejemplo n.º 8
0
 def test_future_not_allowed(self):
     with self.assertRaises(
             Exception, msg="Datapoints from the future are not valid!"):
         validate_production(p5, "FR")
Ejemplo n.º 9
0
 def test_missing_types_allowed(self):
     self.assertFalse(validate_production(p7, 'CH'), msg = "CH, NO, AUS-TAS, US-NEISO don't require Coal/Oil/Unknown!")
Ejemplo n.º 10
0
 def test_missing_types(self):
     with self.assertRaises(Exception, msg = 'Coal/Oil/Unknown are required!'):
         validate_production(p6, 'FR')
Ejemplo n.º 11
0
 def test_future_not_allowed(self):
     with self.assertRaises(Exception, msg = 'Datapoints from the future are not valid!'):
         validate_production(p5, 'FR')
Ejemplo n.º 12
0
 def test_zoneKey_mismatch(self):
     with self.assertRaises(Exception, msg = 'zoneKey mismatch must be caught!'):
         validate_production(p4, 'FR')
Ejemplo n.º 13
0
 def test_bad_datetime(self):
     with self.assertRaises(Exception, msg = 'datetime object is required!'):
         validate_production(p3, 'FR')
Ejemplo n.º 14
0
 def test_no_zoneKey(self):
     with self.assertRaises(Exception, msg = 'zoneKey is required!'):
         validate_production(p2, 'FR')
Ejemplo n.º 15
0
 def test_bad_datetime(self):
     with self.assertRaises(Exception, msg="datetime object is required!"):
         validate_production(p3, "FR")
Ejemplo n.º 16
0
 def test_zoneKey_mismatch(self):
     with self.assertRaises(Exception,
                            msg="zoneKey mismatch must be caught!"):
         validate_production(p4, "FR")
Ejemplo n.º 17
0
 def test_negative_production(self):
     with self.assertRaises(Exception, msg = 'Negative generation should be rejected!'):
         validate_production(p8, 'FR')
Ejemplo n.º 18
0
 def test_missing_types(self):
     with self.assertRaises(Exception,
                            msg="Coal/Oil/Unknown are required!"):
         validate_production(p6, "FR")
Ejemplo n.º 19
0
 def test_good_datapoint(self):
     self.assertFalse(validate_production(p9, 'FR'), msg = 'This datapoint is good!')
Ejemplo n.º 20
0
 def test_negative_production(self):
     with self.assertRaises(Exception,
                            msg="Negative generation should be rejected!"):
         validate_production(p8, "FR")
Ejemplo n.º 21
0
 def test_no_countryCode(self):
     with self.assertRaises(Exception, msg = 'countryCode is required!'):
         validate_production(p2, 'FR')
Ejemplo n.º 22
0
def test_parser(zone, data_type, target_datetime):
    """

    Parameters
    ----------
    zone: a two letter zone from the map
    data_type: in ['production', 'exchangeForecast', 'production', 'exchange',
      'price', 'consumption', 'generationForecast', 'consumptionForecast']
    target_datetime: string parseable by arrow, such as 2018-05-30 15:00

    Examples
    -------
    >>> python test_parser.py NO-NO3-\>SE exchange
    parser result:
    {'netFlow': -51.6563, 'datetime': datetime.datetime(2018, 7, 3, 14, 38, tzinfo=tzutc()), 'source': 'driftsdata.stattnet.no', 'sortedZoneKeys': 'NO-NO3->SE'}
    ---------------------
    took 0.09s
    min returned datetime: 2018-07-03 14:38:00+00:00
    max returned datetime: 2018-07-03T14:38:00+00:00 UTC  -- OK, <2h from now :) (now=2018-07-03T14:39:16.274194+00:00 UTC)
    >>> python test_parser.py FR production
    parser result:
    [... long stuff ...]
    ---------------------
    took 5.38s
    min returned datetime: 2018-07-02 00:00:00+02:00
    max returned datetime: 2018-07-03T14:30:00+00:00 UTC  -- OK, <2h from now :) (now=2018-07-03T14:43:35.501375+00:00 UTC)
    """
    if target_datetime:
        target_datetime = arrow.get(target_datetime).datetime
    start = time.time()

    parser = PARSER_KEY_TO_DICT[data_type][zone]
    if data_type in ['exchange', 'exchangeForecast']:
        args = zone.split('->')
    else:
        args = [zone]
    res = parser(*args, target_datetime=target_datetime)

    if not res:
        print('Error: parser returned nothing ({})'.format(res))
        return

    elapsed_time = time.time() - start
    if isinstance(res, (list, tuple)):
        res_list = list(res)
    else:
        res_list = [res]

    try:
        dts = [e['datetime'] for e in res_list]
    except:
        print('Parser output lacks `datetime` key for at least some of the '
              'ouput. Full ouput: \n\n{}\n'.format(res))
        return

    assert all([type(e['datetime']) is datetime.datetime for e in res_list]), \
        'Datetimes must be returned as native datetime.datetime objects'

    last_dt = arrow.get(max(dts)).to('UTC')
    first_dt = arrow.get(min(dts)).to('UTC')
    max_dt_warning = ''
    if not target_datetime:
        max_dt_warning = (' :( >2h from now !!!' if
                          (arrow.utcnow() - last_dt).total_seconds() > 2 * 3600
                          else ' -- OK, <2h from now :) (now={} UTC)'.format(
                              arrow.utcnow()))

    print('\n'.join([
        'parser result:',
        res.__str__(),
        '---------------------',
        'took {:.2f}s'.format(elapsed_time),
        'min returned datetime: {} UTC'.format(first_dt),
        'max returned datetime: {} UTC {}'.format(last_dt, max_dt_warning),
    ]))

    try:
        if data_type == 'production':
            validate_production(res, zone)
        elif data_type == 'consumption':
            validate_consumption(res, zone)
        elif data_type == 'exchange':
            validate_exchange(res, zone)
    except ValidationError as e:
        print('\nValidation failed: {}'.format(e))
Ejemplo n.º 23
0
 def test_countryCode_mismatch(self):
     with self.assertRaises(Exception, msg = 'countryCode mismatch must be caught!'):
         validate_production(p4, 'FR')
Ejemplo n.º 24
0
def test_parser(zone, data_type, target_datetime):
    """

    Parameters
    ----------
    zone: a two letter zone from the map
    data_type: in ['production', 'exchangeForecast', 'production', 'exchange',
      'price', 'consumption', 'generationForecast', 'consumptionForecast']
    target_datetime: string parseable by arrow, such as 2018-05-30 15:00

    Examples
    -------
    >>> python test_parser.py NO-NO3-\>SE exchange
    parser result:
    {'netFlow': -51.6563, 'datetime': datetime.datetime(2018, 7, 3, 14, 38, tzinfo=tzutc()), 'source': 'driftsdata.stattnet.no', 'sortedZoneKeys': 'NO-NO3->SE'}
    ---------------------
    took 0.09s
    min returned datetime: 2018-07-03 14:38:00+00:00
    max returned datetime: 2018-07-03T14:38:00+00:00 UTC  -- OK, <2h from now :) (now=2018-07-03T14:39:16.274194+00:00 UTC)
    >>> python test_parser.py FR production
    parser result:
    [... long stuff ...]
    ---------------------
    took 5.38s
    min returned datetime: 2018-07-02 00:00:00+02:00
    max returned datetime: 2018-07-03T14:30:00+00:00 UTC  -- OK, <2h from now :) (now=2018-07-03T14:43:35.501375+00:00 UTC)
    """
    if target_datetime:
        target_datetime = arrow.get(target_datetime).datetime
    start = time.time()

    parser = PARSER_KEY_TO_DICT[data_type][zone]
    if data_type in ["exchange", "exchangeForecast"]:
        args = zone.split("->")
    else:
        args = [zone]
    res = parser(
        *args, target_datetime=target_datetime, logger=logging.getLogger(__name__)
    )

    if not res:
        print("Error: parser returned nothing ({})".format(res))
        sys.exit(1)

    elapsed_time = time.time() - start
    if isinstance(res, (list, tuple)):
        res_list = list(res)
    else:
        res_list = [res]

    try:
        dts = [e["datetime"] for e in res_list]
    except:
        print(
            "Parser output lacks `datetime` key for at least some of the "
            "output. Full output: \n\n{}\n".format(res)
        )
        sys.exit(2)

    assert all(
        [type(e["datetime"]) is datetime.datetime for e in res_list]
    ), "Datetimes must be returned as native datetime.datetime objects"

    last_dt = arrow.get(max(dts)).to("UTC")
    first_dt = arrow.get(min(dts)).to("UTC")
    max_dt_warning = ""
    if not target_datetime:
        max_dt_warning = (
            " :( >2h from now !!!"
            if (arrow.utcnow() - last_dt).total_seconds() > 2 * 3600
            else " -- OK, <2h from now :) (now={} UTC)".format(arrow.utcnow())
        )

    print("Parser result:")
    pp = pprint.PrettyPrinter(width=120)
    pp.pprint(res)
    print(
        "\n".join(
            [
                "---------------------",
                "took {:.2f}s".format(elapsed_time),
                "min returned datetime: {} UTC".format(first_dt),
                "max returned datetime: {} UTC {}".format(last_dt, max_dt_warning),
            ]
        )
    )

    if type(res) == dict:
        res = [res]
    for event in res:
        try:
            if data_type == "production":
                validate_production(event, zone)
            elif data_type == "consumption":
                validate_consumption(event, zone)
            elif data_type == "exchange":
                validate_exchange(event, zone)
        except ValidationError as e:
            print("Validation failed: {}".format(e))
            sys.exit(3)
Ejemplo n.º 25
0
 def test_good_datapoint(self):
     self.assertFalse(validate_production(p9, 'FR'),
                      msg='This datapoint is good!')