python-mytnb 0.1.0__tar.gz

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.
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [dev, main]
6
+ pull_request:
7
+ branches: [dev, main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ with:
16
+ enable-cache: true
17
+ - run: uv sync --extra dev
18
+ - run: uv run pylint src/mytnb/ tests/
19
+ - run: uv run ruff check src/mytnb/ tests/
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: astral-sh/setup-uv@v5
29
+ with:
30
+ enable-cache: true
31
+ - run: uv sync --extra dev
32
+ - run: uv run pytest tests/ -v
@@ -0,0 +1,23 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ name: Build and publish to PyPI
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ steps:
16
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
18
+ with:
19
+ enable-cache: true
20
+ - name: Build package
21
+ run: uv build
22
+ - name: Publish to PyPI
23
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+ *.whl
10
+ .pytest_cache/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
20
+ *.swp
21
+ *.swo
22
+
23
+ # Reverse-engineering artifacts
24
+ resources/
25
+
26
+ # Config with credentials
27
+ mytnb.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Latiff Danieyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-mytnb
3
+ Version: 0.1.0
4
+ Summary: Python library to interface with the myTNB API (Tenaga Nasional Berhad)
5
+ Project-URL: Repository, https://github.com/danieyal/python-mytnb
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: click>=8.3.3
20
+ Requires-Dist: cryptography>=42.0
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: rich>=15.0.0
24
+ Requires-Dist: tls-client>=1.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pylint>=3.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.6; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # python-mytnb
33
+
34
+ A Python library to interface with the myTNB (Tenaga Nasional Berhad) API for electricity account management and usage monitoring in Malaysia.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ uv add python-mytnb
40
+ ```
41
+
42
+ Or with pip:
43
+
44
+ ```bash
45
+ pip install python-mytnb
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ import asyncio
52
+ from mytnb import MyTNBClient
53
+
54
+ async def main():
55
+ client = await MyTNBClient.login("user@example.com", "your-password")
56
+
57
+ async with client:
58
+ # Get smart meter usage & billing history
59
+ usage = await client.get_account_usage_smart("220123456789")
60
+
61
+ # Monthly billing breakdown
62
+ for month in usage.by_month.months:
63
+ print(f"{month.month} {month.year}: {month.usage_total} kWh — RM {month.amount_total}")
64
+
65
+ # Bill history & due amount
66
+ history = await client.get_bill_history("220123456789")
67
+ due = await client.get_account_due_amount("220123456789")
68
+
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ## CLI
73
+
74
+ Pass credentials directly:
75
+
76
+ ```bash
77
+ mytnb --email user@example.com --password yourpass usage 220123456789
78
+ ```
79
+
80
+ Or via environment variables:
81
+
82
+ ```bash
83
+ export MYTNB_EMAIL=user@example.com
84
+ export MYTNB_PASSWORD=yourpass
85
+ mytnb usage 220123456789
86
+ ```
87
+
88
+ All commands:
89
+
90
+ ```
91
+ mytnb login # Test login, show user info
92
+ mytnb usage <account> # Monthly usage & billing summary
93
+ mytnb usage --daily <account> # Daily usage breakdown
94
+ mytnb usage --json <account> # Full usage data as JSON
95
+ mytnb current-usage <account> # Simplified current usage summary
96
+ mytnb due-amount <account> # Outstanding balance
97
+ mytnb bill-history <account> # Payment history
98
+ ```
99
+
100
+ Global options: `--debug` for full tracebacks, `--version`.
101
+
102
+ Use a config file instead of flags (`--config <path>`, `mytnb.json`, or `~/.config/mytnb/config.json`):
103
+
104
+ ```bash
105
+ mytnb init-config # Generate a starter config file
106
+ ```
107
+
108
+ ## API Architecture
109
+
110
+ myTNB uses two API backends, both handled transparently by this library:
111
+
112
+ | Backend | Domain | Auth | Used for |
113
+ | ----------- | --------------------- | ------------------------------------------- | ----------------------------------- |
114
+ | REST | `api.mytnb.com.my` | JWT + API key | Account listing, eligibility checks |
115
+ | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
116
+
117
+ Request encryption for the ASMX API is automatic — just pass plaintext parameters.
118
+
119
+ ## Data Models
120
+
121
+ | Model | Description |
122
+ | --------------- | ---------------------------------------------------- |
123
+ | `AccountUsage` | Full usage response: metrics, monthly and daily data |
124
+ | `UsageMetric` | Current/average usage (kWh) |
125
+ | `CostMetric` | Current/projected cost (RM) |
126
+ | `BillingMonth` | Monthly billing record with tariff blocks |
127
+ | `DailyUsage` | Daily consumption and cost |
128
+ | `TariffBlock` | Tariff pricing block details |
129
+ | `SMRAccount` | Smart Meter Reading eligibility status |
130
+ | `BREligibility` | Bill rendering opt-in status |
131
+
132
+ ## Geographic Restrictions
133
+
134
+ The myTNB API only accepts connections from **Malaysian IP addresses** and blocks most VPN services. If you get a `GeoBlockedError` (HTTP 403), make sure you are connecting from a Malaysian network without a VPN.
135
+
136
+ ## Error Handling
137
+
138
+ ```python
139
+ from mytnb.exceptions import MyTNBError, APIError, AuthenticationError, GeoBlockedError
140
+
141
+ try:
142
+ client = await MyTNBClient.login("user@example.com", "password")
143
+ async with client:
144
+ usage = await client.get_account_usage_smart("220123456789")
145
+ except GeoBlockedError:
146
+ print("Blocked — connect from a Malaysian IP without VPN")
147
+ except AuthenticationError:
148
+ print("Invalid email or password")
149
+ except APIError as e:
150
+ print(f"API error {e.error_code}: {e.display_message}")
151
+ except MyTNBError as e:
152
+ print(f"Error: {e}")
153
+ ```
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ uv sync --extra dev
159
+ uv run pytest
160
+ uv run pylint src/mytnb/
161
+ ```
162
+
163
+ ## License
164
+
165
+ MIT
@@ -0,0 +1,134 @@
1
+ # python-mytnb
2
+
3
+ A Python library to interface with the myTNB (Tenaga Nasional Berhad) API for electricity account management and usage monitoring in Malaysia.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv add python-mytnb
9
+ ```
10
+
11
+ Or with pip:
12
+
13
+ ```bash
14
+ pip install python-mytnb
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```python
20
+ import asyncio
21
+ from mytnb import MyTNBClient
22
+
23
+ async def main():
24
+ client = await MyTNBClient.login("user@example.com", "your-password")
25
+
26
+ async with client:
27
+ # Get smart meter usage & billing history
28
+ usage = await client.get_account_usage_smart("220123456789")
29
+
30
+ # Monthly billing breakdown
31
+ for month in usage.by_month.months:
32
+ print(f"{month.month} {month.year}: {month.usage_total} kWh — RM {month.amount_total}")
33
+
34
+ # Bill history & due amount
35
+ history = await client.get_bill_history("220123456789")
36
+ due = await client.get_account_due_amount("220123456789")
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ ## CLI
42
+
43
+ Pass credentials directly:
44
+
45
+ ```bash
46
+ mytnb --email user@example.com --password yourpass usage 220123456789
47
+ ```
48
+
49
+ Or via environment variables:
50
+
51
+ ```bash
52
+ export MYTNB_EMAIL=user@example.com
53
+ export MYTNB_PASSWORD=yourpass
54
+ mytnb usage 220123456789
55
+ ```
56
+
57
+ All commands:
58
+
59
+ ```
60
+ mytnb login # Test login, show user info
61
+ mytnb usage <account> # Monthly usage & billing summary
62
+ mytnb usage --daily <account> # Daily usage breakdown
63
+ mytnb usage --json <account> # Full usage data as JSON
64
+ mytnb current-usage <account> # Simplified current usage summary
65
+ mytnb due-amount <account> # Outstanding balance
66
+ mytnb bill-history <account> # Payment history
67
+ ```
68
+
69
+ Global options: `--debug` for full tracebacks, `--version`.
70
+
71
+ Use a config file instead of flags (`--config <path>`, `mytnb.json`, or `~/.config/mytnb/config.json`):
72
+
73
+ ```bash
74
+ mytnb init-config # Generate a starter config file
75
+ ```
76
+
77
+ ## API Architecture
78
+
79
+ myTNB uses two API backends, both handled transparently by this library:
80
+
81
+ | Backend | Domain | Auth | Used for |
82
+ | ----------- | --------------------- | ------------------------------------------- | ----------------------------------- |
83
+ | REST | `api.mytnb.com.my` | JWT + API key | Account listing, eligibility checks |
84
+ | Legacy ASMX | `mytnbapp.tnb.com.my` | Encrypted payloads (AES-256-CBC + RSA-OAEP) | Usage data, billing, services |
85
+
86
+ Request encryption for the ASMX API is automatic — just pass plaintext parameters.
87
+
88
+ ## Data Models
89
+
90
+ | Model | Description |
91
+ | --------------- | ---------------------------------------------------- |
92
+ | `AccountUsage` | Full usage response: metrics, monthly and daily data |
93
+ | `UsageMetric` | Current/average usage (kWh) |
94
+ | `CostMetric` | Current/projected cost (RM) |
95
+ | `BillingMonth` | Monthly billing record with tariff blocks |
96
+ | `DailyUsage` | Daily consumption and cost |
97
+ | `TariffBlock` | Tariff pricing block details |
98
+ | `SMRAccount` | Smart Meter Reading eligibility status |
99
+ | `BREligibility` | Bill rendering opt-in status |
100
+
101
+ ## Geographic Restrictions
102
+
103
+ The myTNB API only accepts connections from **Malaysian IP addresses** and blocks most VPN services. If you get a `GeoBlockedError` (HTTP 403), make sure you are connecting from a Malaysian network without a VPN.
104
+
105
+ ## Error Handling
106
+
107
+ ```python
108
+ from mytnb.exceptions import MyTNBError, APIError, AuthenticationError, GeoBlockedError
109
+
110
+ try:
111
+ client = await MyTNBClient.login("user@example.com", "password")
112
+ async with client:
113
+ usage = await client.get_account_usage_smart("220123456789")
114
+ except GeoBlockedError:
115
+ print("Blocked — connect from a Malaysian IP without VPN")
116
+ except AuthenticationError:
117
+ print("Invalid email or password")
118
+ except APIError as e:
119
+ print(f"API error {e.error_code}: {e.display_message}")
120
+ except MyTNBError as e:
121
+ print(f"Error: {e}")
122
+ ```
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ uv sync --extra dev
128
+ uv run pytest
129
+ uv run pylint src/mytnb/
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,84 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-mytnb"
7
+ version = "0.1.0"
8
+ description = "Python library to interface with the myTNB API (Tenaga Nasional Berhad)"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27",
26
+ "pydantic>=2.0",
27
+ "cryptography>=42.0",
28
+ "tls_client>=1.0",
29
+ "click>=8.3.3",
30
+ "rich>=15.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ "pytest-asyncio>=0.21",
37
+ "pylint>=3.0",
38
+ "ruff>=0.6",
39
+ ]
40
+
41
+ [project.scripts]
42
+ mytnb = "mytnb.cli:main"
43
+
44
+ [project.urls]
45
+ Repository = "https://github.com/danieyal/python-mytnb"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/mytnb"]
49
+
50
+ [tool.pylint.main]
51
+ load-plugins = []
52
+ jobs = 0
53
+
54
+ [tool.pylint.messages_control]
55
+ disable = [
56
+ "missing-module-docstring",
57
+ "missing-class-docstring",
58
+ "missing-function-docstring",
59
+ "too-few-public-methods",
60
+ "too-many-arguments",
61
+ "too-many-instance-attributes",
62
+ "too-many-return-statements",
63
+ "too-many-branches",
64
+ "too-many-locals",
65
+ "line-too-long",
66
+ "fixme",
67
+ "broad-exception-caught",
68
+ "import-error",
69
+ ]
70
+
71
+ [tool.pylint.format]
72
+ max-line-length = 120
73
+
74
+ [tool.pylint.basic]
75
+ good-names = ["i", "j", "k", "v", "e", "f", "d", "ui", "di"]
76
+
77
+ [tool.ruff]
78
+ line-length = 120
79
+
80
+ [tool.ruff.lint]
81
+ select = ["E", "F", "I", "N", "W"]
82
+
83
+ [tool.ruff.lint.isort]
84
+ known-first-party = ["mytnb"]
@@ -0,0 +1,26 @@
1
+ """Python library for the myTNB API."""
2
+
3
+ from mytnb.client import MyTNBClient
4
+ from mytnb.crypto import EncryptedPayload, encrypt_request
5
+ from mytnb.models import (
6
+ AccountUsage,
7
+ BillingMonth,
8
+ CostMetric,
9
+ DailyUsage,
10
+ Metric,
11
+ TariffBlock,
12
+ UsageMetric,
13
+ )
14
+
15
+ __all__ = [
16
+ "MyTNBClient",
17
+ "EncryptedPayload",
18
+ "encrypt_request",
19
+ "AccountUsage",
20
+ "BillingMonth",
21
+ "CostMetric",
22
+ "DailyUsage",
23
+ "Metric",
24
+ "TariffBlock",
25
+ "UsageMetric",
26
+ ]
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m mytnb`."""
2
+
3
+ from mytnb.cli import main
4
+
5
+ main()
@@ -0,0 +1,67 @@
1
+ """Authentication handling for myTNB API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class DeviceInfo:
11
+ """Device information sent with API requests."""
12
+
13
+ device_id: str
14
+ app_version: str = "4.0.2"
15
+ os_type: str = "2" # 1=Android, 2=iOS
16
+ os_version: str = "18.0"
17
+ device_desc: str = "EN"
18
+ version_code: str = "1425"
19
+
20
+ def to_dict(self) -> dict:
21
+ return {
22
+ "deviceId": self.device_id,
23
+ "appVersion": self.app_version,
24
+ "osType": self.os_type,
25
+ "osVersion": self.os_version,
26
+ "deviceDesc": self.device_desc,
27
+ "versionCode": self.version_code,
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class UserInfo:
33
+ """User information for authenticated requests."""
34
+
35
+ user_name: str
36
+ user_id: str
37
+ display_name: str = ""
38
+ role_id: str = "16"
39
+ language: str = "EN"
40
+
41
+ def to_dict(self) -> dict:
42
+ return {
43
+ "RoleId": self.role_id,
44
+ "UserId": self.user_id,
45
+ "UserName": self.user_name,
46
+ "Lang": self.language,
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class Credentials:
52
+ """API credentials for myTNB."""
53
+
54
+ # REST API (api.mytnb.com.my)
55
+ api_key: str
56
+ authorization_token: str
57
+ # Optional bearer token for some endpoints
58
+ bearer_token: Optional[str] = None
59
+ # Channel API key (static JWT)
60
+ channel_api_key: Optional[str] = None
61
+
62
+ # Legacy API (mytnbapp.tnb.com.my)
63
+ secure_key: Optional[str] = None
64
+
65
+ # User context
66
+ user_info: Optional[UserInfo] = None
67
+ device_info: Optional[DeviceInfo] = None