initial working env

This commit is contained in:
Arian Nasr
2025-12-11 03:30:35 -05:00
commit 25469a54b3
11 changed files with 799 additions and 0 deletions

0
greenbutton/__init__.py Normal file
View File

248
greenbutton/enums.py Normal file
View File

@@ -0,0 +1,248 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from enum import Enum
class AccumulationBehaviourType(Enum):
notApplicable = 0
bulkQuantity = 1
cumulative = 3
deltaData = 4
indicating = 6
summation = 9
instantaneous = 12
class CommodityType(Enum):
notApplicable = 0
electricity = 1
air = 4
naturalGas = 7
propane = 8
potableWater = 9
steam = 10
wastewater = 11
heatingFluid = 12
coolingFluid = 13
class ConsumptionTierType(Enum):
notApplicable = 0
blockTier1 = 1
blockTier2 = 2
blockTier3 = 3
blockTier4 = 4
blockTier5 = 5
blockTier6 = 6
blockTier7 = 7
blockTier8 = 8
blockTier9 = 9
blockTier10 = 10
blockTier11 = 11
blockTier12 = 12
blockTier13 = 13
blockTier14 = 14
blockTier15 = 15
blockTier16 = 16
class CurrencyCode(Enum):
na = 0
aus = 36
cad = 124
usd = 840
eur = 978
@property
def symbol(self):
if self in [CurrencyCode.aus, CurrencyCode.cad, CurrencyCode.usd]:
return '$'
elif self is CurrencyCode.eur:
return ''
else:
return '¤'
@property
def uom_id(self):
if self in CURRENCY_UOM_IDS:
return CURRENCY_UOM_IDS[self]
else:
return None
class DataQualifierType(Enum):
notApplicable = 0
average = 2
maximum = 8
minimum = 9
normal = 12
class FlowDirectionType(Enum):
notApplicable = 0
forward = 1
reverse = 19
class KindType(Enum):
notApplicable = 0
currency = 3
current = 4
currentAngle = 5
date = 7
demand = 8
energy = 12
frequency = 15
power = 37
powerFactor = 38
quantityPower = 40
voltage = 54
voltageAngle = 55
distortionPowerFactor = 64
volumetricFlow = 155
class PhaseCode(Enum):
notApplicable = 0
c = 32
ca = 40
b = 64
bc = 66
a = 128
an = 129
ab = 132
abc = 224
s2 = 256
s2n = 257
s1 = 512
s1n = 513
s1s2 = 768
s1s2n = 769
class QualityOfReading(Enum):
valid = 0
manuallyEdited = 7
estimatedUsingReferenceDay = 8
estimatedUsingLinearInterpolation = 9
questionable = 10
derived = 11
projected = 12
mixed = 13
raw = 14
normalizedForWeather = 15
other = 16
validated = 17
verified = 18
class ServiceKind(Enum):
electricity = 0
naturalGas = 1
water = 2
pressure = 4
heat = 5
cold = 6
communication = 7
time = 8
class TimeAttributeType(Enum):
notApplicable = 0
tenMinutes = 1
fifteenMinutes = 2
twentyFourHours = 4
thirtyMinutes = 5
sixtyMinutes = 7
daily = 11
monthly = 13
present = 15
previous = 16
weekly = 24
forTheSpecifiedPeriod = 32
daily30MinuteFixedBlock = 79
class UomType(Enum):
notApplicable = 0
amps = 5
volts = 29
joules = 31
hertz = 33
watts = 38
cubicMeters = 42
voltAmps = 61
voltAmpsReactive = 63
cosine = 65
voltsSquared = 67
ampsSquared = 69
voltAmpHours = 71
wattHours = 72
voltAmpReactiveHours = 73
ampHours = 106
cubicFeet = 119
cubicFeetPerHour = 122
cubicMetersPerHour = 125
usGallons = 128
usGallonsPerHour = 129
imperialGallons = 130
imperialGallonsPerHour = 131
britishThermalUnits = 132
britishThermalUnitsPerHour = 133
liters = 134
litersPerHour = 137
gaugePascals = 140
absolutePascals = 155
therms = 169
UOM_SYMBOLS = {
UomType.notApplicable: '',
UomType.amps: 'A',
UomType.volts: 'V',
UomType.joules: 'J',
UomType.hertz: 'Hz',
UomType.watts: 'W',
UomType.cubicMeters: '',
UomType.voltAmps: 'VA',
UomType.voltAmpsReactive: 'VAr',
UomType.cosine: 'cos',
UomType.voltsSquared: '',
UomType.ampsSquared: '',
UomType.voltAmpHours: 'VAh',
UomType.wattHours: 'Wh',
UomType.voltAmpReactiveHours: 'VArh',
UomType.ampHours: 'Ah',
UomType.cubicFeet: 'ft³',
UomType.cubicFeetPerHour: 'ft³/h',
UomType.cubicMetersPerHour: 'm³/h',
UomType.usGallons: 'US gal',
UomType.usGallonsPerHour: 'US gal/h',
UomType.imperialGallons: 'IMP gal',
UomType.imperialGallonsPerHour: 'IMP gal/h',
UomType.britishThermalUnits: 'BTU',
UomType.britishThermalUnitsPerHour: 'BTU/h',
UomType.liters: 'L',
UomType.litersPerHour: 'L/h',
UomType.gaugePascals: 'Pag',
UomType.absolutePascals: 'Pa',
UomType.therms: 'thm',
}
UOM_IDS = {
UomType.amps: 'electric current_A',
UomType.volts: 'electric potential_V',
UomType.joules: 'energy_J',
UomType.hertz: 'frequency_Hz',
UomType.watts: 'power_W',
UomType.cubicMeters: 'energy_m**3_gas',
UomType.voltAmps: 'apparent power_VA',
UomType.voltAmpsReactive: 'reactive power_VAR',
UomType.voltAmpHours: 'apparent energy_VAh',
UomType.wattHours: 'energy_Wh',
UomType.voltAmpReactiveHours: 'reactive energy_VARh',
UomType.ampHours: 'electric charge_Ah',
UomType.cubicFeet: 'energy_ft**3_gas',
UomType.usGallons: 'volume_gal',
UomType.britishThermalUnits: 'energy_BTU',
UomType.britishThermalUnitsPerHour: 'power_BTU-h',
UomType.liters: 'volume_L',
UomType.litersPerHour: 'volumetric flow_L-h',
UomType.absolutePascals: 'pressure_Pa',
UomType.therms: 'energy_therm',
}
CURRENCY_UOM_IDS = {
CurrencyCode.aus: 'currency_AUD',
CurrencyCode.cad: 'currency_CAD',
CurrencyCode.usd: 'currency_USD',
CurrencyCode.eur: 'currency_EUR'
}

112
greenbutton/objects.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/python
import datetime
import functools
import pytz
from .enums import *
from .utils import *
@functools.total_ordering
class DateTimeInterval:
def __init__(self, entity):
self.duration = getEntity(entity, 'espi:duration',
lambda e: datetime.timedelta(seconds=int(e.text)))
self.start = getEntity(entity, 'espi:start',
lambda e: datetime.datetime.fromtimestamp(int(e.text), pytz.timezone("UTC")))
def __repr__(self):
return '<DateTimeInterval (%s, %s)>' % (self.start, self.duration)
def __eq__(self, other):
if not isinstance(other, DateTimeInterval):
return False
return (self.start, self.duration) == (other.start, other.duration)
def __lt__(self, other):
if not isinstance(other, DateTimeInterval):
return False
return (self.start, self.duration) < (other.start, other.duration)
@functools.total_ordering
class IntervalReading:
def __init__(self, entity, parent):
self.intervalBlock = parent
self.cost = getEntity(entity, 'espi:cost', lambda e: int(e.text) / 100000.0)
self.timePeriod = getEntity(entity, 'espi:timePeriod',
lambda e: DateTimeInterval(e))
self._value = getEntity(entity, 'espi:value', lambda e: int(e.text))
self.tou = getEntity(entity, 'espi:tou', lambda e: int(e.text))
self.readingQualities = set([ReadingQuality(rq, self) for rq in entity.findall('espi:ReadingQuality', ns)])
def __repr__(self):
return '<IntervalReading (%s, %s: %s %s)>' % (self.timePeriod.start, self.timePeriod.duration, self.value, self.value_symbol)
def __eq__(self, other):
if not isinstance(other, IntervalReading):
return False
return (self.timePeriod, self.value) == (other.timePeriod, other.value)
def __lt__(self, other):
if not isinstance(other, IntervalReading):
return False
return (self.timePeriod, self.value) < (other.timePeriod, other.value)
@property
def value(self):
if self.intervalBlock is not None and \
self.intervalBlock.meterReading is not None and \
self.intervalBlock.meterReading.readingType is not None and \
self.intervalBlock.meterReading.readingType.powerOfTenMultiplier is not None:
multiplier = 10 ** self.intervalBlock.meterReading.readingType.powerOfTenMultiplier
else:
multiplier = 1
return self._value * multiplier
@property
def cost_units(self):
if self.intervalBlock is not None and \
self.intervalBlock.meterReading is not None and \
self.intervalBlock.meterReading.readingType is not None and \
self.intervalBlock.meterReading.readingType.currency is not None:
return self.intervalBlock.meterReading.readingType.currency
else:
return CurrencyCode.na
@property
def cost_symbol(self):
return self.cost_units.symbol
@property
def cost_uom_id(self):
return self.cost_units.uom_id
@property
def value_units(self):
if self.intervalBlock is not None and \
self.intervalBlock.meterReading is not None and \
self.intervalBlock.meterReading.readingType is not None and \
self.intervalBlock.meterReading.readingType.uom is not None:
return self.intervalBlock.meterReading.readingType.uom
else:
return UomType.notApplicable
@property
def value_symbol(self):
return UOM_SYMBOLS[self.value_units]
@property
def value_uom_id(self):
if self.value_units in UOM_IDS:
return UOM_IDS[self.value_units]
else:
return None
class ReadingQuality:
def __init__(self, entity, parent):
self.intervalReading = parent
self.quality = getEntity(entity, 'espi:quality', lambda e: QualityOfReading(int(e.text)))

48
greenbutton/parse.py Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/python
import sys
import xml.etree.ElementTree as ET
from .resources import *
def parse_feed(filename):
tree = ET.parse(filename)
usagePoints = []
for entry in tree.getroot().findall('atom:entry/atom:content/espi:UsagePoint/../..', ns):
up = UsagePoint(entry)
usagePoints.append(up)
meterReadings = []
for entry in tree.getroot().findall('atom:entry/atom:content/espi:MeterReading/../..', ns):
mr = MeterReading(entry, usagePoints=usagePoints)
meterReadings.append(mr)
for entry in tree.getroot().findall('atom:entry/atom:content/espi:LocalTimeParameters/../..', ns):
ltp = LocalTimeParameters(entry, usagePoints=usagePoints)
readingTypes = []
for entry in tree.getroot().findall('atom:entry/atom:content/espi:ReadingType/../..', ns):
rt = ReadingType(entry, meterReadings=meterReadings)
readingTypes.append(rt)
intervalBlocks = []
for entry in tree.getroot().findall('atom:entry/atom:content/espi:IntervalBlock/../..', ns):
ib = IntervalBlock(entry, meterReadings=meterReadings)
intervalBlocks.append(ib)
return usagePoints
if __name__ == '__main__':
ups = parse_feed(sys.argv[1])
for up in ups:
print('UsagePoint (%s) %s %s:' % (up.title, up.serviceCategory.name, up.status))
for mr in up.meterReadings:
print(' Meter Reading (%s) %s:' % (mr.title, mr.readingType.uom.name))
for ir in mr.intervalReadings:
print(' %s, %s: %s %s' % (ir.timePeriod.start, ir.timePeriod.duration, ir.value, ir.value_symbol), end = '')
if ir.cost is not None:
print(' (%s%s)' % (ir.cost_symbol, ir.cost))
if len(ir.readingQualities) > 0:
print('[%s]' % ', '.join([rq.quality.name for rq in ir.readingQualities]))
print

154
greenbutton/resources.py Normal file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/python
import bisect
import functools
from .utils import *
from .enums import *
from .objects import *
class Resource(object):
def __init__(self, entry):
self.link_self = getLink(entry, 'self')
self.link_up = getLink(entry, 'up')
self.link_related = getLink(entry, 'related', True)
self.title = getEntity(entry, 'atom:title', lambda e: e.text)
def __repr__(self):
return '<%s (%s)>' % (self.__class__.__name__, self.title or self.link_self)
def isParentOf(self, other):
return other.link_self in self.link_related or other.link_up in self.link_related
class UsagePoint(Resource):
def __init__(self, entry, meterReadings=[]):
super(UsagePoint, self).__init__(entry)
obj = entry.find('./atom:content/espi:UsagePoint', ns)
self.roleFlags = getEntity(obj, 'espi:roleFlags', lambda e: int(e.text, 16))
self.status = getEntity(obj, 'espi:status', lambda e: int(e.text))
self.serviceCategory = getEntity(obj, './espi:ServiceCategory/espi:kind',
lambda e: ServiceKind(int(e.text)))
self.meterReadings = set()
for mr in meterReadings:
if self.isParentOf(mr):
self.addMeterReading(mr)
self.localTimeParameters = None
def addMeterReading(self, meterReading):
assert self.isParentOf(meterReading)
self.meterReadings.add(meterReading)
meterReading.usagePoint = self
def addLocalTimeParameters(self, localTimeParameters):
assert self.isParentOf(localTimeParameters)
self.localTimeParameters = localTimeParameters
class MeterReading(Resource):
def __init__(self, entry, usagePoints=[], readingTypes=[], intervalBlocks=[]):
super(MeterReading, self).__init__(entry)
self.usagePoint = None
self.readingType = None
self.intervalBlocks = []
for up in usagePoints:
if up.isParentOf(self):
up.addMeterReading(self)
for rt in readingTypes:
if self.isParentOf(rt):
self.setReadingType(rt)
for ib in intervalBlocks:
if self.isParentOf(ib):
self.addIntervalBlock(r)
@property
def intervalReadings(self):
for ib in self.intervalBlocks:
for ir in ib.intervalReadings:
yield ir
def setReadingType(self, readingType):
assert self.isParentOf(readingType)
assert self.readingType is None or self.readingType.link_self == readingType.link_self
self.readingType = readingType
readingType.meterReading = self
def addIntervalBlock(self, intervalBlock):
assert self.isParentOf(intervalBlock)
bisect.insort(self.intervalBlocks, intervalBlock)
intervalBlock.meterReading = self
class ReadingType(Resource):
def __init__(self, entry, meterReadings=[]):
super(ReadingType, self).__init__(entry)
self.meterReading = None
obj = entry.find('./atom:content/espi:ReadingType', ns)
self.accumulationBehaviour = getEntity(obj, 'espi:accumulationBehaviour',
lambda e: AccumulationBehaviourType(int(e.text)))
self.commodity = getEntity(obj, 'espi:commodity',
lambda e: CommodityType(int(e.text)))
self.consumptionTier = getEntity(obj, 'espi:consumptionTier',
lambda e: ConsumptionTierType(int(e.text)))
self.currency = getEntity(obj, 'espi:currency',
lambda e: CurrencyCode(int(e.text)))
self.dataQualifier = getEntity(obj, 'espi:dataQualifier',
lambda e: DataQualifierType(int(e.text)))
self.defaultQuality = getEntity(obj, 'espi:defaultQuality',
lambda e: QualityOfReading(int(e.text)))
self.flowDirection = getEntity(obj, 'espi:flowDirection',
lambda e: FlowDirectionType(int(e.text)))
self.intervalLength = getEntity(obj, 'espi:intervalLength', lambda e: int(e.text))
self.kind = getEntity(obj, 'espi:kind', lambda e: KindType(int(e.text)))
self.phase = getEntity(obj, 'espi:phase', lambda e: PhaseCode(int(e.text)))
self.powerOfTenMultiplier = getEntity(obj, 'espi:powerOfTenMultiplier',
lambda e: int(e.text))
self.timeAttribute = getEntity(obj, 'espi:timeAttribute',
lambda e: TimeAttributeType(int(e.text)))
self.tou = getEntity(obj, 'espi:tou', lambda e: TOUType(int(e.text)))
self.uom = getEntity(obj, 'espi:uom', lambda e: UomType(int(e.text)))
for mr in meterReadings:
if mr.isParentOf(self):
mr.setReadingType(self)
class LocalTimeParameters(Resource):
def __init__(self, entry, usagePoints=[]):
super(LocalTimeParameters, self).__init__(entry)
self.localTimeParameters = None
obj = entry.find('./atom:content/espi:LocalTimeParameters', ns)
self.tzOffset = getEntity(obj, 'espi:tzOffset', lambda e: int(e.text))
for up in usagePoints:
if up.isParentOf(self):
up.addLocalTimeParameters(self)
@functools.total_ordering
class IntervalBlock(Resource):
def __init__(self, entry, meterReadings=[]):
super(IntervalBlock, self).__init__(entry)
self.meterReading = None
obj = entry.find('./atom:content/espi:IntervalBlock', ns)
self.interval = getEntity(obj, 'espi:interval', lambda e: DateTimeInterval(e))
self.intervalReadings = sorted([IntervalReading(ir, self) for ir in obj.findall('espi:IntervalReading', ns)])
for mr in meterReadings:
if mr.isParentOf(self):
mr.addIntervalBlock(self)
def __eq__(self, other):
if not isinstance(other, IntervalBlock):
return False
return self.link_self == other.link_self
def __lt__(self, other):
return self.interval < other.interval

28
greenbutton/utils.py Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/python
ns = {'atom': 'http://www.w3.org/2005/Atom',
'espi': 'http://naesb.org/espi'}
def getEntity(source, target, accessor=None, multiple=False):
"""Extracts the named entity from the source XML tree. `accessor` is a
function of one argyment; if provided and the target entity is found, the
target will be passed into `accessor` and its result will be returned. If
`multiple` is true, the result will be all entities that match (i.e. the
function will use `finall` instead of `find`)."""
if multiple:
es = source.findall(target, ns)
if accessor:
return [ accessor(e) for e in es ]
else:
return es
else:
e = source.find(target, ns)
if e is not None and accessor is not None:
return accessor(e)
else:
return e
def getLink(source, relation, multiple=False):
"""Shorthand for pulling a link with the given "rel" attribute from the source."""
return getEntity(source, './atom:link[@rel="%s"]' % relation, lambda e: e.attrib['href'], multiple)