odse 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ods_e/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ ODS-E: Open Data Schema for Energy
3
+
4
+ A Python library for validating and transforming energy asset data
5
+ using the ODS-E specification.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .validator import validate, validate_file
11
+ from .transformer import transform, transform_stream
12
+
13
+ __all__ = ["validate", "validate_file", "transform", "transform_stream"]
ods_e/transformer.py ADDED
@@ -0,0 +1,139 @@
1
+ """
2
+ ODS-E Transformer
3
+
4
+ Transforms OEM-specific data formats to ODS-E schema.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Iterator, List, Optional, Union
9
+
10
+
11
+ def transform(
12
+ data: Union[str, Path],
13
+ source: str,
14
+ asset_id: Optional[str] = None,
15
+ timezone: Optional[str] = None,
16
+ ) -> List[Dict[str, Any]]:
17
+ """
18
+ Transform OEM data to ODS-E format.
19
+
20
+ Args:
21
+ data: Path to data file or data string
22
+ source: OEM source identifier (e.g., "huawei", "enphase", "solarman")
23
+ asset_id: Optional asset identifier to include in output
24
+ timezone: Optional timezone for timestamp conversion
25
+
26
+ Returns:
27
+ List of ODS-E formatted records
28
+ """
29
+ transformer = _get_transformer(source)
30
+ return transformer.transform(data, asset_id=asset_id, timezone=timezone)
31
+
32
+
33
+ def transform_stream(
34
+ data: Union[str, Path],
35
+ source: str,
36
+ **kwargs,
37
+ ) -> Iterator[Dict[str, Any]]:
38
+ """
39
+ Stream transform OEM data to ODS-E format.
40
+
41
+ Useful for large files where loading all records into memory
42
+ is not practical.
43
+
44
+ Args:
45
+ data: Path to data file
46
+ source: OEM source identifier
47
+ **kwargs: Additional arguments passed to transformer
48
+
49
+ Yields:
50
+ ODS-E formatted records one at a time
51
+ """
52
+ transformer = _get_transformer(source)
53
+ yield from transformer.transform_stream(data, **kwargs)
54
+
55
+
56
+ def _get_transformer(source: str):
57
+ """Get the appropriate transformer for the source."""
58
+ transformers = {
59
+ "huawei": HuaweiTransformer(),
60
+ "enphase": EnphaseTransformer(),
61
+ "solarman": SolarmanTransformer(),
62
+ }
63
+
64
+ source_lower = source.lower()
65
+ if source_lower not in transformers:
66
+ raise ValueError(
67
+ f"Unknown source '{source}'. "
68
+ f"Supported sources: {list(transformers.keys())}"
69
+ )
70
+
71
+ return transformers[source_lower]
72
+
73
+
74
+ class BaseTransformer:
75
+ """Base class for OEM transformers."""
76
+
77
+ def transform(
78
+ self,
79
+ data: Union[str, Path],
80
+ **kwargs,
81
+ ) -> List[Dict[str, Any]]:
82
+ """Transform data to ODS-E format."""
83
+ raise NotImplementedError
84
+
85
+ def transform_stream(
86
+ self,
87
+ data: Union[str, Path],
88
+ **kwargs,
89
+ ) -> Iterator[Dict[str, Any]]:
90
+ """Stream transform data to ODS-E format."""
91
+ # Default implementation: yield from transform()
92
+ yield from self.transform(data, **kwargs)
93
+
94
+
95
+ class HuaweiTransformer(BaseTransformer):
96
+ """Transform Huawei FusionSolar data to ODS-E."""
97
+
98
+ ERROR_CODES = {
99
+ "normal": [0, 1, 2, 3, 256, 512, 1025, 1026, 1280, 1281, 1536, 1792, 2048, 2304, 40960, 49152],
100
+ "warning": [513, 514, 772, 773, 774],
101
+ "critical": [768, 770, 771, 45056],
102
+ "fault": [769, 1024],
103
+ }
104
+
105
+ def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
106
+ # Placeholder implementation
107
+ return []
108
+
109
+ def _map_error_code(self, code: int) -> str:
110
+ for error_type, codes in self.ERROR_CODES.items():
111
+ if code in codes:
112
+ return error_type
113
+ return "unknown"
114
+
115
+
116
+ class EnphaseTransformer(BaseTransformer):
117
+ """Transform Enphase Envoy data to ODS-E."""
118
+
119
+ def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
120
+ # Placeholder implementation
121
+ return []
122
+
123
+
124
+ class SolarmanTransformer(BaseTransformer):
125
+ """Transform Solarman Logger data to ODS-E."""
126
+
127
+ STATE_MAPPING = {
128
+ "Normal": "normal",
129
+ "Operating": "normal",
130
+ "Warning": "warning",
131
+ "Fault": "fault",
132
+ "Error": "fault",
133
+ "Offline": "offline",
134
+ "Standby": "standby",
135
+ }
136
+
137
+ def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
138
+ # Placeholder implementation
139
+ return []
ods_e/validator.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ ODS-E Validator
3
+
4
+ Validates energy data against ODS-E JSON schemas.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import List, Optional, Union
11
+
12
+
13
+ @dataclass
14
+ class ValidationError:
15
+ """Represents a validation error."""
16
+ path: str
17
+ message: str
18
+ code: str
19
+
20
+
21
+ @dataclass
22
+ class ValidationResult:
23
+ """Result of a validation operation."""
24
+ is_valid: bool
25
+ errors: List[ValidationError]
26
+ warnings: List[ValidationError]
27
+ level: str = "schema"
28
+
29
+
30
+ def validate(
31
+ data: Union[dict, str, Path],
32
+ level: str = "schema",
33
+ capacity_kw: Optional[float] = None,
34
+ latitude: Optional[float] = None,
35
+ longitude: Optional[float] = None,
36
+ ) -> ValidationResult:
37
+ """
38
+ Validate data against ODS-E production-timeseries schema.
39
+
40
+ Args:
41
+ data: Dictionary, JSON string, or path to JSON file
42
+ level: Validation level - "schema" or "semantic"
43
+ capacity_kw: Asset capacity in kW (required for semantic validation)
44
+ latitude: Asset latitude (optional, for nighttime checks)
45
+ longitude: Asset longitude (optional, for nighttime checks)
46
+
47
+ Returns:
48
+ ValidationResult with is_valid status and any errors/warnings
49
+ """
50
+ # Parse input
51
+ if isinstance(data, (str, Path)):
52
+ if Path(data).exists():
53
+ with open(data) as f:
54
+ data = json.load(f)
55
+ else:
56
+ data = json.loads(data)
57
+
58
+ errors = []
59
+ warnings = []
60
+
61
+ # Schema validation
62
+ errors.extend(_validate_schema(data))
63
+
64
+ # Semantic validation
65
+ if level == "semantic" and not errors:
66
+ sem_errors, sem_warnings = _validate_semantic(
67
+ data, capacity_kw, latitude, longitude
68
+ )
69
+ errors.extend(sem_errors)
70
+ warnings.extend(sem_warnings)
71
+
72
+ return ValidationResult(
73
+ is_valid=len(errors) == 0,
74
+ errors=errors,
75
+ warnings=warnings,
76
+ level=level,
77
+ )
78
+
79
+
80
+ def validate_file(
81
+ file_path: Union[str, Path],
82
+ level: str = "schema",
83
+ **kwargs,
84
+ ) -> ValidationResult:
85
+ """
86
+ Validate a JSON file containing ODS-E data.
87
+
88
+ Args:
89
+ file_path: Path to JSON file
90
+ level: Validation level
91
+ **kwargs: Additional arguments passed to validate()
92
+
93
+ Returns:
94
+ ValidationResult
95
+ """
96
+ return validate(Path(file_path), level=level, **kwargs)
97
+
98
+
99
+ def _validate_schema(data: dict) -> List[ValidationError]:
100
+ """Validate against JSON schema."""
101
+ errors = []
102
+
103
+ # Required fields
104
+ required = ["timestamp", "kWh", "error_type"]
105
+ for field in required:
106
+ if field not in data:
107
+ errors.append(ValidationError(
108
+ path=f"$.{field}",
109
+ message=f"Required field '{field}' is missing",
110
+ code="REQUIRED_FIELD_MISSING",
111
+ ))
112
+
113
+ if errors:
114
+ return errors
115
+
116
+ # Type validation
117
+ if not isinstance(data.get("kWh"), (int, float)):
118
+ errors.append(ValidationError(
119
+ path="$.kWh",
120
+ message=f"Expected number, got {type(data.get('kWh')).__name__}",
121
+ code="TYPE_MISMATCH",
122
+ ))
123
+
124
+ # Enum validation
125
+ valid_error_types = [
126
+ "normal", "warning", "critical", "fault",
127
+ "offline", "standby", "unknown"
128
+ ]
129
+ if data.get("error_type") not in valid_error_types:
130
+ errors.append(ValidationError(
131
+ path="$.error_type",
132
+ message=f"Value '{data.get('error_type')}' not in enum {valid_error_types}",
133
+ code="ENUM_MISMATCH",
134
+ ))
135
+
136
+ # Bounds validation
137
+ if isinstance(data.get("kWh"), (int, float)) and data["kWh"] < 0:
138
+ errors.append(ValidationError(
139
+ path="$.kWh",
140
+ message="kWh must be >= 0",
141
+ code="OUT_OF_BOUNDS",
142
+ ))
143
+
144
+ if "PF" in data:
145
+ pf = data["PF"]
146
+ if isinstance(pf, (int, float)) and (pf < 0 or pf > 1):
147
+ errors.append(ValidationError(
148
+ path="$.PF",
149
+ message="Power factor must be between 0 and 1",
150
+ code="OUT_OF_BOUNDS",
151
+ ))
152
+
153
+ return errors
154
+
155
+
156
+ def _validate_semantic(
157
+ data: dict,
158
+ capacity_kw: Optional[float],
159
+ latitude: Optional[float],
160
+ longitude: Optional[float],
161
+ ) -> tuple:
162
+ """Validate semantic constraints."""
163
+ errors = []
164
+ warnings = []
165
+
166
+ # Physical bounds check
167
+ if capacity_kw and isinstance(data.get("kWh"), (int, float)):
168
+ # Assume 1-hour interval for simplicity
169
+ max_kwh = capacity_kw * 1.1
170
+ if data["kWh"] > max_kwh:
171
+ warnings.append(ValidationError(
172
+ path="$.kWh",
173
+ message=f"kWh ({data['kWh']}) exceeds maximum possible ({max_kwh}) for {capacity_kw}kW capacity",
174
+ code="EXCEEDS_PHYSICAL_MAXIMUM",
175
+ ))
176
+
177
+ # State/production consistency
178
+ if data.get("error_type") == "offline" and data.get("kWh", 0) > 10:
179
+ warnings.append(ValidationError(
180
+ path="$",
181
+ message=f"Significant production ({data['kWh']} kWh) reported with error_type 'offline'",
182
+ code="STATE_PRODUCTION_MISMATCH",
183
+ ))
184
+
185
+ return errors, warnings
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: odse
3
+ Version: 0.1.0
4
+ Summary: Open Data Schema for Energy - validation and transformation library
5
+ Author-email: Asoba Corporation <support@asoba.co>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/AsobaCloud/odse
8
+ Project-URL: Documentation, https://docs.asoba.co/ona-protocol/overview
9
+ Project-URL: Repository, https://github.com/AsobaCloud/odse
10
+ Keywords: energy,solar,iot,data,schema,validation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: jsonschema>=4.0.0
24
+ Requires-Dist: pyyaml>=6.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
28
+ Requires-Dist: black>=23.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+
31
+ # ODS-E: Open Data Schema for Energy
32
+
33
+ [![License: CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/)
34
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
35
+
36
+ ODS-E is an open specification for standardizing energy asset data from IoT devices, enabling interoperability across the renewable energy ecosystem.
37
+
38
+ ## Why ODS-E?
39
+
40
+ - **No Vendor Lock-in**: Your data works with any ODS-E compatible system
41
+ - **Faster Integrations**: Pre-built transforms for common OEMs (Huawei, Enphase, Solarman)
42
+ - **Analytics-Ready**: Standardized error taxonomy and semantic validation
43
+ - **Future-Proof**: CC-BY-SA licensed specification ensures extensions stay open
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ pip install odse
49
+ ```
50
+
51
+ ```python
52
+ from ods_e import validate, transform
53
+
54
+ # Validate ODS-E data
55
+ result = validate("production_data.json")
56
+
57
+ # Transform from OEM format
58
+ ods_data = transform("huawei_export.csv", source="huawei")
59
+ ```
60
+
61
+ ## Repository Structure
62
+
63
+ ```
64
+ ona-protocol/
65
+ ├── LICENSE-SPEC.md # CC-BY-SA 4.0 (specification, schemas, transforms)
66
+ ├── LICENSE-CODE.md # Apache 2.0 (reference implementation, tools)
67
+ ├── spec/ # Specification documents
68
+ ├── schemas/ # JSON Schema definitions
69
+ ├── transforms/ # OEM transform specifications
70
+ ├── src/ # Reference implementation
71
+ └── tools/ # CLI tools
72
+ ```
73
+
74
+ ## Core Schema
75
+
76
+ ```json
77
+ {
78
+ "timestamp": "2026-02-05T14:00:00Z",
79
+ "kWh": 847.5,
80
+ "error_type": "normal",
81
+ "PF": 0.98
82
+ }
83
+ ```
84
+
85
+ **Required fields:**
86
+ - `timestamp` - ISO 8601 with timezone
87
+ - `kWh` - Active energy (≥ 0)
88
+ - `error_type` - One of: `normal`, `warning`, `critical`, `fault`, `offline`, `standby`, `unknown`
89
+
90
+ ## Supported OEMs
91
+
92
+ | OEM | Format | Status |
93
+ |-----|--------|--------|
94
+ | Huawei FusionSolar | CSV, API | ✅ Included |
95
+ | Enphase Envoy | JSON, API | ✅ Included |
96
+ | Solarman Logger | CSV | ✅ Included |
97
+ | SolarEdge | JSON API | ✅ Included |
98
+ | Fronius | JSON API | ✅ Included |
99
+ | Switch Energy | CSV | ✅ Included |
100
+
101
+ ## License
102
+
103
+ - **Specification, Schemas, Transforms**: [CC-BY-SA 4.0](LICENSE-SPEC.md)
104
+ - **Reference Implementation, Tools**: [Apache 2.0](LICENSE-CODE.md)
105
+
106
+ ## Documentation
107
+
108
+ - [Full Documentation](https://docs.asoba.co/ona-protocol)
109
+ - [Schema Reference](https://docs.asoba.co/ona-protocol/schemas)
110
+ - [Transform Guide](https://docs.asoba.co/ona-protocol/transforms)
111
+
112
+ ## Contributing
113
+
114
+ Contributions are welcome. Schema and transform contributions must be licensed under CC-BY-SA 4.0.
115
+
116
+ ---
117
+
118
+ Maintained by [Asoba Corporation](https://asoba.co)
@@ -0,0 +1,8 @@
1
+ ods_e/__init__.py,sha256=1cgVXnHduy-sFJL6IeexDekVE9J1MZccMkUen0FoaNQ,340
2
+ ods_e/transformer.py,sha256=H1PLEFKORTq99HGpDnzKInoqYb5u276iELq6L1bRUC0,3826
3
+ ods_e/validator.py,sha256=oXLCH1Jqpa9HPwjz9DqW0Gzp4Z-qh-D-wp6ehG0m1VQ,5131
4
+ odse-0.1.0.dist-info/METADATA,sha256=yna_LYJo5krC2uNKaxQyOxx-guDif-EfLArtSpuxwcA,3989
5
+ odse-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ odse-0.1.0.dist-info/entry_points.txt,sha256=Q57qBjBH3FOsovuCw3cXGcxveUPKsp2CTLnm0CEwEpk,40
7
+ odse-0.1.0.dist-info/top_level.txt,sha256=HrO3os1RN2ICfcAKoeXAxwvQ0C9nz8q7_kF8aYGCTbM,6
8
+ odse-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ odse = ods_e.cli:main
@@ -0,0 +1 @@
1
+ ods_e