diff --git a/bricksrc/collections.py b/bricksrc/collections.py index 88e39f8a..3023b093 100644 --- a/bricksrc/collections.py +++ b/bricksrc/collections.py @@ -180,4 +180,9 @@ "aliases": [BRICK["PV_Array"]], "constraints": {BRICK.hasPart: [BRICK.PV_Panel]}, }, + "Electric_Vehicle_Charging_Hub": { + "tags": [TAG.Collection, TAG.Electric, TAG.Vehicle, TAG.Charging, TAG.Hub], + "aliases": [BRICK["EV_Charging_Hub"]], + "constraints": {BRICK.hasPart: [BRICK.Electric_Vehicle_Charging_Station]}, + }, } diff --git a/bricksrc/definitions.csv b/bricksrc/definitions.csv index 0ea76f7e..05bed105 100644 --- a/bricksrc/definitions.csv +++ b/bricksrc/definitions.csv @@ -322,6 +322,9 @@ https://brickschema.org/schema/Brick#Electric_Current,, https://brickschema.org/schema/Brick#Electric_Energy,, https://brickschema.org/schema/Brick#Electric_Power_Sensor,Measures the amount of instantaneous electric power consumed, https://brickschema.org/schema/Brick#Electric_Radiator,Electric heating device, +https://brickschema.org/schema/Brick#Electric_Vehicle_Charging_Hub,"A collection of charging stations for charging electric vehicles. A hub may be located in a parking lot, for example", +https://brickschema.org/schema/Brick#Electric_Vehicle_Charging_Port,An individual point of attachment for charing a single electric vehicle, +https://brickschema.org/schema/Brick#Electric_Vehicle_Charging_Station,An individual piece of equipment supplying electrical power for charging electric vehicles. Contains 1 or more electric vehicle charging ports, https://brickschema.org/schema/Brick#Electric_Voltage,, https://brickschema.org/schema/Brick#Electrical_Energy_Usage_Sensor,A sensor that records the quantity of electrical energy consumed in a given period, https://brickschema.org/schema/Brick#Electrical_Meter,A meter that measures the usage or consumption of electricity, diff --git a/bricksrc/entity_properties.py b/bricksrc/entity_properties.py index a968787e..e0378122 100644 --- a/bricksrc/entity_properties.py +++ b/bricksrc/entity_properties.py @@ -347,6 +347,33 @@ SH.node: BSH.VirtualMeterShape, RDFS.label: Literal("is virtual meter"), }, + BRICK.electricVehicleChargerType: { + SKOS.definition: Literal( + "Which type of EVSE charger this is, e.g. Level 1 (up to up to 2.5kW of AC power on 1 phase 120V input), Level 2 (direct AC power but can use higher voltage and up to 3 phases), or Level 3 (direct DC power)" + ), + "property_of": BRICK.Electric_Vehicle_Charging_Station, + RDFS.label: Literal("has electric vehicle charger type"), + SH.node: BSH.ElectricVehicleChargingTypeShape, + }, + BRICK.electricVehicleChargerDirectionality: { + SKOS.definition: Literal( + "Indicates if the EVSE charger supports bidirectional charging or just unidirectional charging of the EV battery" + ), + "property_of": [ + BRICK.Electric_Vehicle_Charging_Station, + BRICK.Electric_Vehicle_Charging_Port, + ], + RDFS.label: Literal("has electric vehicle charger directionality"), + SH.node: BSH.ElectricVehicleChargingDirectionalityShape, + }, + BRICK.electricVehicleConnectorType: { + SKOS.definition: Literal( + "Identifies which kind of connector the port has. This property helps identify the physical connection required between the vehicle and the charging equipment." + ), + "property_of": BRICK.Electric_Vehicle_Charging_Port, + RDFS.label: Literal("has electric vehicle connector type"), + SH.node: BSH.ElectricVehicleConnectorTypeShape, + }, } building_primary_function_values = [ @@ -451,7 +478,7 @@ BSH.ElectricalComplexPowerShape: {"values": ["real", "reactive", "apparent"]}, BSH.ElectricalFlowShape: {"values": ["import", "export", "net", "absolute"]}, BSH.PhasesShape: {"values": ["A", "B", "C", "AB", "BC", "AC", "ABC"]}, - BSH.PhaseCountShape: {"values": ["1", "2", "3", "Total"]}, + BSH.PhaseCountShape: {"values": [1, 2, 3, "Total"]}, BSH.CurrentFlowTypeShape: {"values": ["AC", "DC"]}, BSH.StageShape: {"values": [1, 2, 3, 4]}, BSH.BuildingPrimaryFunctionShape: {"values": building_primary_function_values}, @@ -533,6 +560,25 @@ }, }, }, + BRICK.ElectricVehicleChargingTypeShape: { + "values": ["Level 1", "Level 2", "Level 3"] + }, + BRICK.ElectricVehicleChargingDirectionalityShape: { + "values": ["unidirectional", "bidirectional"] + }, + BRICK.ElectricVehicleConnectorTypeShape: { + "values": [ + "Type 1 (CSS)", + "Type 2 (CSS)", + "GB/T", + "Type 1 (SAE J1772)", + "Type 2 (IEC 62196)", + "CHAdeMO", + "CCS (Combined Charging System)", + "Tesla Supercharger", + "Wireless", + ] + }, } diff --git a/bricksrc/equipment.py b/bricksrc/equipment.py index 69acdc4b..32bd2c65 100644 --- a/bricksrc/equipment.py +++ b/bricksrc/equipment.py @@ -45,6 +45,25 @@ "Electrical_Equipment": { "tags": [TAG.Electrical, TAG.Equipment], "subclasses": { + "Electric_Vehicle_Charging_Station": { + "tags": [ + TAG.Electric, + TAG.Vehicle, + TAG.Charging, + TAG.Station, + TAG.Equipment, + ], + "constraints": {BRICK.hasPart: [BRICK.Electric_Vehicle_Charging_Port]}, + }, + "Electric_Vehicle_Charging_Port": { + "tags": [ + TAG.Electric, + TAG.Vehicle, + TAG.Charging, + TAG.Port, + TAG.Equipment, + ], + }, "Energy_Storage": { "tags": [TAG.Energy, TAG.Storage, TAG.Equipment], "subclasses": { diff --git a/bricksrc/rules.ttl b/bricksrc/rules.ttl index eea36277..3ffda01e 100644 --- a/bricksrc/rules.ttl +++ b/bricksrc/rules.ttl @@ -39,6 +39,24 @@ WHERE { sh:targetClass brick:Entity ; . +bsh:InferInverseProperties2 + a sh:NodeShape ; + sh:rule [ + a sh:SPARQLRule ; + sh:construct """ + CONSTRUCT { +$this ?p ?o . +} +WHERE { +?o ?invP $this . +?invP owl:inverseOf ?p . +} + """ ; + sh:prefixes ; + ] ; + sh:targetClass brick:Entity ; +. + bsh:InferSymmetricProperties a sh:NodeShape ; sh:rule [ @@ -424,3 +442,52 @@ bsh:hasSubstance a sh:NodeShape ; sh:targetObjectsOf brick:hasSubstance ; sh:class brick:Substance ; . + + +# add unidirectional charging to all EVsE chargers as a default value +# UNLESS there is already a brick:electricVehicleChargerDirectionality attribute +# on ports associated with the charger +bsh:AddDefaultEVSEChargerDirection a sh:NodeShape ; + sh:targetClass brick:Electric_Vehicle_Charging_Station ; + sh:rule [ + a sh:SPARQLRule ; + sh:prefixes ; + sh:construct """ + CONSTRUCT { + $this brick:electricVehicleChargerDirectionality [ brick:value "unidirectional" ] + } WHERE { + $this rdf:type brick:Electric_Vehicle_Charging_Station . + FILTER NOT EXISTS { + $this brick:electricVehicleChargerDirectionality ?direction . + } + FILTER NOT EXISTS { + $this brick:hasPart ?port . + ?port a brick:Electric_Vehicle_Charging_Port . + ?port brick:electricVehicleChargerDirectionality ?other_direction . + } + } + """ ; + ] ; +. + +# inherit the directionality of the EVSE charger to the ports +bsh:InheritEVSEChargerDirection a sh:NodeShape ; + sh:targetClass brick:Electric_Vehicle_Charging_Port ; + sh:rule [ + a sh:SPARQLRule ; + sh:prefixes ; + sh:construct """ + CONSTRUCT { + $this brick:electricVehicleChargerDirectionality ?direction + } WHERE { + $this rdf:type brick:Electric_Vehicle_Charging_Port . + $this brick:isPartOf ?charger . + ?charger a brick:Electric_Vehicle_Charging_Station . + ?charger brick:electricVehicleChargerDirectionality ?direction . + FILTER NOT EXISTS { + $this brick:electricVehicleChargerDirectionality ?other_direction . + } + } + """ ; + ] ; +. diff --git a/examples/evse/evse.ttl b/examples/evse/evse.ttl new file mode 100644 index 00000000..ac613199 --- /dev/null +++ b/examples/evse/evse.ttl @@ -0,0 +1,55 @@ +@prefix brick: . +@prefix unit: . +@prefix xsd: . +@prefix : . + +:parking_lot a brick:Parking_Structure ; + brick:isLocationOf :hub . + +:hub a brick:Electric_Vehicle_Charging_Hub ; + brick:hasPart :station1, :station2 . + +:station1 a brick:Electric_Vehicle_Charging_Station ; + brick:electricVehicleChargerType [ brick:value "Level 1" ] ; + brick:hasPart :port1-1, :port1-2 ; +. + +:port1-1 a brick:Electric_Vehicle_Charging_Port ; + brick:electricVehicleChargerDirectionality [ brick:value "bidirectional" ] ; + brick:electricVehicleConnectorType [ brick:value "Level 1 (SAE J1772)" ] ; + brick:currentFlowType [ brick:value "AC" ] ; + brick:electricalPhaseCount [ brick:value 1 ] ; + brick:hasPoint :port1-1-voltage ; +. +:port1-1-voltage a brick:Battery_Voltage_Sensor ; + brick:hasUnit unit:PERCENT ; +. + +:port1-2 a brick:Electric_Vehicle_Charging_Port ; + brick:electricVehicleChargerDirectionality [ brick:value "bidirectional" ] ; + brick:electricVehicleConnectorType [ brick:value "Level 1 (SAE J1772)" ] ; + brick:currentFlowType [ brick:value "AC" ] ; + brick:electricalPhaseCount [ brick:value 1 ] ; + brick:hasPoint :port1-2-voltage ; +. +:port1-2-voltage a brick:Battery_Voltage_Sensor ; + brick:hasUnit unit:PERCENT ; +. + +:station2 a brick:Electric_Vehicle_Charging_Station ; + brick:electricVehicleChargerDirectionality [ brick:value "unidirectional" ] ; + brick:electricVehicleChargerType [ brick:value "Level 2" ] ; + brick:hasPart :port2-1 ; +. + +:port2-1 a brick:Electric_Vehicle_Charging_Port ; + # this should inherit 'unidirectional' from the station + brick:electricVehicleConnectorType [ brick:value "Level 2 (IEC 62196)" ] ; + brick:currentFlowType [ brick:value "AC" ] ; + brick:electricalPhaseCount [ brick:value 3 ] ; + brick:hasPoint :port2-1-voltage ; +. + +:port2-1-voltage a brick:Battery_Voltage_Sensor ; + brick:hasUnit unit:PERCENT ; +. diff --git a/examples/last_known_value/last_known_value.ttl b/examples/last_known_value/last_known_value.ttl index 20caa4c7..fab58808 100644 --- a/examples/last_known_value/last_known_value.ttl +++ b/examples/last_known_value/last_known_value.ttl @@ -4,6 +4,10 @@ @prefix unit: . @prefix xsd: . @prefix bacnet: . +@prefix owl: . + +bldg: a owl:Ontology ; + owl:imports . bldg:sample-device a bacnet:BACnetDevice ; bacnet:device-instance 123 ; diff --git a/generate_brick.py b/generate_brick.py index a40c30bd..3b567b1b 100755 --- a/generate_brick.py +++ b/generate_brick.py @@ -1,3 +1,4 @@ +import logging import os import brickschema import importlib @@ -6,7 +7,6 @@ import csv import glob import ontoenv -import logging import pyshacl from rdflib import Graph, Literal, BNode, URIRef from rdflib.namespace import XSD @@ -58,11 +58,15 @@ from bricksrc.entity_properties import entity_properties, get_shapes from bricksrc.deprecations import deprecations + logging.basicConfig( format="%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s", datefmt="%Y-%m-%d:%H:%M:%S", level=logging.INFO, ) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + G = brickschema.Graph() bind_prefixes(G) @@ -552,19 +556,19 @@ def define_shape_properties(definitions, graph=G): graph.add((brick_value_shape, SH["in"], enumeration)) graph.add((brick_value_shape, SH.minCount, Literal(1))) vals = defn.pop("values") - if isinstance(vals[0], str): + if all(map(lambda v: isinstance(v, str), vals)): Collection( graph, enumeration, map(lambda x: Literal(x, datatype=XSD.string), vals), ) - elif isinstance(vals[0], int): + if all(map(lambda v: isinstance(v, int), vals)): Collection( graph, enumeration, map(lambda x: Literal(x, datatype=XSD.integer), vals), ) - elif isinstance(vals[0], float): + if all(map(lambda v: isinstance(v, float), vals)): Collection( graph, enumeration, @@ -752,7 +756,7 @@ def add_definitions(graph=G): # Setpoint names are not explicit in class's names. Thus needs # to be explicily added for the definition text. setpoint = setpoint + "_Setpoint" - logging.info(f"Inferred setpoint: {setpoint}") + logger.info(f"Inferred setpoint: {setpoint}") limit_def = limit_def_template.format(direction=direction, setpoint=setpoint) if param != BRICK.Limit: # definition already exists for Limit graph.add((param, SKOS.definition, Literal(limit_def, lang="en"))) @@ -816,11 +820,11 @@ def handle_deprecations(): G.add((deprecated_term, BRICK.isReplacedBy, md["replace_with"])) -logging.info("Beginning BRICK Ontology compilation") +logger.info("Beginning BRICK Ontology compilation") # handle ontology definition define_ontology(G) -logging.info("Inheriting annotations down the subclass trees") +logger.info("Inheriting annotations down the subclass trees") inherit_has_quantity(setpoint_definitions) inherit_has_quantity(sensor_definitions) inherit_has_quantity(alarm_definitions) @@ -848,7 +852,7 @@ def handle_deprecations(): define_classes(roots, BRICK.Class) # <= Brick v1.3.0 define_classes(roots, BRICK.Entity) # >= Brick v1.3.0 -logging.info("Defining properties") +logger.info("Defining properties") # define BRICK properties G.add((BRICK.Relationship, A, OWL.ObjectProperty)) G.add((BRICK.Relationship, RDFS.label, Literal("Relationship"))) @@ -867,7 +871,7 @@ def handle_deprecations(): G.add((VCARD.Address, A, OWL.Class)) -logging.info("Defining Point subclasses") +logger.info("Defining Point subclasses") # define Point subclasses define_classes(setpoint_definitions, BRICK.Point) define_classes(sensor_definitions, BRICK.Point) @@ -882,7 +886,7 @@ def handle_deprecations(): for o in filter(lambda x: x != pc, pointclasses): G.add((BRICK[pc], OWL.disjointWith, BRICK[o])) -logging.info("Defining Equipment, System and Location subclasses") +logger.info("Defining Equipment, System and Location subclasses") # define other root class structures define_classes(location_subclasses, BRICK.Location) define_classes(equipment_subclasses, BRICK.Equipment) @@ -893,7 +897,7 @@ def handle_deprecations(): define_classes(security_subclasses, BRICK.Security_Equipment) define_classes(safety_subclasses, BRICK.Safety_Equipment) -logging.info("Defining Measurable hierarchy") +logger.info("Defining Measurable hierarchy") # define measurable hierarchy G.add((BRICK.Measurable, RDFS.subClassOf, BRICK.Entity)) # set up Quantity definition @@ -958,6 +962,7 @@ def handle_deprecations(): }""" ) +logger.info("Adding applicable units") # this requires two passes to associate the applicable units with # each of the quantities. The first pass associates Brick quantities # with QUDT units via the "hasQUDTReference" property; the second pass @@ -979,6 +984,7 @@ def handle_deprecations(): # G.add((unit, RDFS.label, label)) +logger.info("Defining entity properties") # entity property definitions (must happen after units are defined) G.add((BRICK.value, SKOS.definition, Literal("The basic value of an entity property"))) G.add((BRICK.EntityProperty, RDFS.subClassOf, OWL.ObjectProperty)) @@ -990,14 +996,16 @@ def handle_deprecations(): G.remove((BRICK.value, A, OWL.ObjectProperty)) +logger.info("Adding deprecations") # handle class deprecations handle_deprecations() # handle non-class deprecations G.parse("bricksrc/deprecations.ttl") -logging.info("Adding class definitions") +logger.info("Adding class definitions") add_definitions() +logger.info("Adding other .ttl files") # add all TTL files in bricksrc for ttlfile in glob.glob("bricksrc/*.ttl"): G.parse(ttlfile, format="turtle") @@ -1008,6 +1016,7 @@ def handle_deprecations(): for triple in G.cbd(ref_schema_uri): G.remove(triple) +logger.info("Cleaning up ontology prefixes") # remove duplicate ontology definitions and # move prefixes onto Brick ontology definition for ontology, pfx in G.subject_objects(predicate=SH.declare): @@ -1052,7 +1061,7 @@ def handle_deprecations(): extension_graph.serialize(dest, format="ttl") -logging.info(f"Brick ontology compilation finished! Generated {len(G)} triples") +logger.info(f"Brick ontology compilation finished! Generated {len(G)} triples") extension_graphs = {"shacl_tag_inference": shaclGraph} @@ -1064,6 +1073,10 @@ def handle_deprecations(): fp.write(graph.serialize(format="turtle").rstrip()) fp.write("\n") +# add inferred information to Brick +# logger.info("Adding inferred information to Brick") +# G.expand('shacl', backend='topquadrant') + # serialize Brick to output with open("Brick.ttl", "w", encoding="utf-8") as fp: fp.write(G.serialize(format="turtle").rstrip()) @@ -1073,6 +1086,7 @@ def handle_deprecations(): if os.path.exists("Brick+extensions.ttl"): os.remove("Brick+extensions.ttl") + # create new directory for storing imports os.makedirs("imports", exist_ok=True) env = ontoenv.OntoEnv(initialize=True) diff --git a/requirements.txt b/requirements.txt index 9109be04..80cd83f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pytest>=7.3 tqdm>=4.0 pyshacl>=0.25 docker>=6.0 -brickschema[all]>=0.7.4a8 +brickschema[all]>=0.7.4a9 black==23.3.0 pre-commit>=3.2 flake8>=6.0 diff --git a/tests/conftest.py b/tests/conftest.py index 74fa1d6c..de592499 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,8 +32,8 @@ def pytest_configure(config): @pytest.fixture() def brick_with_imports(): - env = ontoenv.OntoEnv() - g = brickschema.graph.Graph() + env = ontoenv.OntoEnv(initialize=True) + g = brickschema.Graph() g.load_file("Brick.ttl") g.bind("qudt", QUDT) g.bind("rdf", RDF) diff --git a/tests/test_examples.py b/tests/test_examples.py index a0f378a8..c06ae0b3 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -10,8 +10,27 @@ def test_example_file_with_reasoning(brick_with_imports, filename): g = brick_with_imports g.load_file(filename) env.import_dependencies(g) - # g.expand("shacl", backend="topquadrant") - # g.serialize("/tmp/res.ttl") + g.expand("shacl", backend="topquadrant") valid, _, report = g.validate(engine="topquadrant") assert valid, report + + +# specific unit test for the 'evse.ttl' example file +def test_evse_example_file_with_reasoning(brick_with_imports): + g = brick_with_imports + g.load_file("examples/evse/evse.ttl") + env.import_dependencies(g) + g.expand("shacl", backend="topquadrant") + + # test that all Electric_Vehicle_Charging_Ports in the model + # have a brick:electricVehicleChargerDirectionality property + q = """ + SELECT ?evcp WHERE { + ?evcp a/rdfs:subClassOf* brick:Electric_Vehicle_Charging_Port . + FILTER NOT EXISTS { + ?evcp brick:electricVehicleChargerDirectionality ?dir . + } + }""" + res = list(g.query(q)) + assert len(res) == 0, "All EVCPs must have a directionality property"