Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59346d641f | ||
|
|
11d770771b | ||
|
|
1b6db7ed45 | ||
|
|
55c4e10fae | ||
|
|
77c06dad61 | ||
|
|
68d250d2f2 | ||
|
|
094a17ac68 | ||
|
|
dbcdeaa3de | ||
|
|
872cac811a | ||
|
|
d324c2fee9 | ||
|
|
4b54cf67ba | ||
|
|
0e00fe3114 | ||
|
|
15d1dc5bb6 | ||
|
|
2af0003843 | ||
|
|
3c227be170 |
@@ -1,16 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
|
||||
17
README.md
17
README.md
@@ -19,6 +19,23 @@ Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with
|
||||
|
||||
[Click here to view supported miner types](https://docs.pyasic.org/en/latest/miners/supported_types/)
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system. Options include:
|
||||
- [venv](https://docs.python.org/3/library/venv.html): included in Python standard library but has fewer features than other options
|
||||
- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv): [pyenv](https://github.com/pyenv/pyenv) plugin for managing virtualenvs
|
||||
```
|
||||
pyenv install <python version number>
|
||||
pyenv virtualenv <python version number> <env name>
|
||||
pyenv activate <env name>
|
||||
```
|
||||
- [conda](https://docs.conda.io/en/latest/)
|
||||
|
||||
##### Installing `pyasic`
|
||||
|
||||
`python -m pip install .` or `poetry install`
|
||||
|
||||
---
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@ Welcome to `pyasic`! `pyasic` uses an asynchronous method of communicating with
|
||||
|
||||
[Click here to view supported miner types](miners/supported_types.md)
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
It is recommended to install `pyasic` in a [virtual environment](https://realpython.com/python-virtual-environments-a-primer/#what-other-popular-options-exist-aside-from-venv) to isolate it from the rest of your system. Options include:
|
||||
- [venv](https://docs.python.org/3/library/venv.html): included in Python standard library but has fewer features than other options
|
||||
- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv): [pyenv](https://github.com/pyenv/pyenv) plugin for managing virtualenvs
|
||||
```
|
||||
pyenv install <python version number>
|
||||
pyenv virtualenv <python version number> <env name>
|
||||
pyenv activate <env name>
|
||||
```
|
||||
- [conda](https://docs.conda.io/en/latest/)
|
||||
|
||||
##### Installing `pyasic`
|
||||
|
||||
`python -m pip install .` or `poetry install`
|
||||
|
||||
---
|
||||
## Getting started
|
||||
---
|
||||
@@ -236,6 +253,7 @@ settings.update("default_antminer_password", "my_pwd")
|
||||
"factory_get_timeout": 3,
|
||||
"get_data_retries": 1,
|
||||
"api_function_timeout": 5,
|
||||
"antminer_mining_mode_as_str": False,
|
||||
"default_whatsminer_password": "admin",
|
||||
"default_innosilicon_password": "admin",
|
||||
"default_antminer_password": "root",
|
||||
|
||||
@@ -106,6 +106,13 @@
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## S19 Pro+ Hydro
|
||||
::: pyasic.miners.antminer.bmminer.X19.S19.BMMinerS19KPro
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## T19
|
||||
::: pyasic.miners.antminer.bmminer.X19.T19.BMMinerT19
|
||||
handler: python
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# pyasic
|
||||
## X21 Models
|
||||
|
||||
## S21
|
||||
::: pyasic.miners.antminer.bmminer.X21.S21.BMMinerS21
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: false
|
||||
heading_level: 4
|
||||
|
||||
## S21 (ePIC)
|
||||
::: pyasic.miners.antminer.epic.X21.S21.ePICS21
|
||||
handler: python
|
||||
|
||||
@@ -81,9 +81,16 @@ details {
|
||||
<li><a href="../antminer/X19#s19-hydro">S19 Hydro</a></li>
|
||||
<li><a href="../antminer/X19#s19-pro-hydro">S19 Pro Hydro</a></li>
|
||||
<li><a href="../antminer/X19#s19-pro_1-hydro">S19 Pro+ Hydro</a></li>
|
||||
<li><a href="../antminer/X19#s19-pro_1-hydro">S19 Pro+ Hydro</a></li>
|
||||
<li><a href="../antminer/X19#t19">T19</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
<summary>X21 Series:</summary>
|
||||
<ul>
|
||||
<li><a href="../antminer/X21#s21">S21</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
|
||||
@@ -12,6 +12,7 @@ Settings options:
|
||||
- `factory_get_timeout`
|
||||
- `get_data_retries`
|
||||
- `api_function_timeout`
|
||||
- `antminer_mining_mode_as_str`
|
||||
- `default_whatsminer_password`
|
||||
- `default_innosilicon_password`
|
||||
- `default_antminer_password`
|
||||
|
||||
@@ -96,7 +96,7 @@ class FanModeManual(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": str(self.speed)}
|
||||
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": str(self.speed)}
|
||||
|
||||
def as_bosminer(self) -> dict:
|
||||
return {
|
||||
@@ -120,7 +120,7 @@ class FanModeImmersion(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwn": "0"}
|
||||
return {"bitmain-fan-ctrl": True, "bitmain-fan-pwm": "0"}
|
||||
|
||||
def as_bosminer(self) -> dict:
|
||||
return {"temp_control": {"mode": "disabled"}}
|
||||
@@ -156,7 +156,10 @@ class FanModeConfig(MinerConfigOption):
|
||||
if web_conf.get("bitmain-fan-ctrl") is not None:
|
||||
fan_manual = web_conf["bitmain-fan-ctrl"]
|
||||
if fan_manual:
|
||||
return cls.manual(speed=web_conf["bitmain-fan-pwm"])
|
||||
speed = int(web_conf["bitmain-fan-pwm"])
|
||||
if speed == 0:
|
||||
return cls.immersion()
|
||||
return cls.manual(speed=speed)
|
||||
else:
|
||||
return cls.normal()
|
||||
else:
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pyasic import settings
|
||||
from pyasic.config.base import MinerConfigOption, MinerConfigValue
|
||||
from pyasic.web.braiins_os.proto.braiins.bos.v1 import (
|
||||
HashrateTargetMode,
|
||||
@@ -39,7 +40,9 @@ class MiningModeNormal(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
@@ -63,7 +66,9 @@ class MiningModeSleep(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "1"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "1"}
|
||||
return {"miner-mode": 1}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
@@ -87,7 +92,9 @@ class MiningModeLPM(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "3"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "3"}
|
||||
return {"miner-mode": 3}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
@@ -108,7 +115,9 @@ class MiningModeHPM(MinerConfigValue):
|
||||
return cls()
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
return {"mode": self.mode}
|
||||
@@ -165,7 +174,9 @@ class MiningModePowerTune(MinerConfigValue):
|
||||
return cls(**cls_conf)
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
def as_wm(self) -> dict:
|
||||
if self.power is not None:
|
||||
@@ -204,7 +215,9 @@ class MiningModeHashrateTune(MinerConfigValue):
|
||||
return cls(dict_conf.get("hashrate"))
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
def as_boser(self) -> dict:
|
||||
return {
|
||||
@@ -239,7 +252,9 @@ class ManualBoardSettings(MinerConfigValue):
|
||||
return cls(freq=dict_conf["freq"], volt=dict_conf["volt"])
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -259,7 +274,9 @@ class MiningModeManual(MinerConfigValue):
|
||||
)
|
||||
|
||||
def as_am_modern(self) -> dict:
|
||||
return {"miner-mode": "0"}
|
||||
if settings.get("antminer_mining_mode_as_str", False):
|
||||
return {"miner-mode": "0"}
|
||||
return {"miner-mode": 0}
|
||||
|
||||
@classmethod
|
||||
def from_vnish(cls, web_overclock_settings: dict) -> "MiningModeManual":
|
||||
|
||||
@@ -31,6 +31,7 @@ from pyasic.miners.models import (
|
||||
S19ProHydro,
|
||||
S19ProPlus,
|
||||
S19ProPlusHydro,
|
||||
S19KPro,
|
||||
)
|
||||
|
||||
|
||||
@@ -92,3 +93,7 @@ class BMMinerS19Hydro(AntminerModern, S19Hydro):
|
||||
|
||||
class BMMinerS19ProPlusHydro(AntminerModern, S19ProPlusHydro):
|
||||
pass
|
||||
|
||||
|
||||
class BMMinerS19KPro(AntminerModern, S19KPro):
|
||||
pass
|
||||
|
||||
@@ -30,5 +30,6 @@ from .S19 import (
|
||||
BMMinerS19ProPlus,
|
||||
BMMinerS19ProPlusHydro,
|
||||
BMMinerS19XP,
|
||||
BMMinerS19KPro,
|
||||
)
|
||||
from .T19 import BMMinerT19
|
||||
|
||||
22
pyasic/miners/antminer/bmminer/X21/S21.py
Normal file
22
pyasic/miners/antminer/bmminer/X21/S21.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright 2022 Upstream Data Inc -
|
||||
# -
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); -
|
||||
# you may not use this file except in compliance with the License. -
|
||||
# You may obtain a copy of the License at -
|
||||
# -
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 -
|
||||
# -
|
||||
# Unless required by applicable law or agreed to in writing, software -
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, -
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from pyasic.miners.backends import AntminerModern
|
||||
from pyasic.miners.models import S21
|
||||
|
||||
|
||||
class BMMinerS21(AntminerModern, S21):
|
||||
pass
|
||||
16
pyasic/miners/antminer/bmminer/X21/__init__.py
Normal file
16
pyasic/miners/antminer/bmminer/X21/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright 2022 Upstream Data Inc -
|
||||
# -
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); -
|
||||
# you may not use this file except in compliance with the License. -
|
||||
# You may obtain a copy of the License at -
|
||||
# -
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 -
|
||||
# -
|
||||
# Unless required by applicable law or agreed to in writing, software -
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, -
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -
|
||||
# See the License for the specific language governing permissions and -
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
from .S21 import BMMinerS21
|
||||
@@ -18,3 +18,4 @@ from .X7 import *
|
||||
from .X9 import *
|
||||
from .X17 import *
|
||||
from .X19 import *
|
||||
from .X21 import *
|
||||
|
||||
@@ -22,6 +22,7 @@ from pyasic.data.error_codes import MinerErrorData, X19Error
|
||||
from pyasic.errors import APIError
|
||||
from pyasic.miners.backends.bmminer import BMMiner
|
||||
from pyasic.miners.backends.cgminer import CGMiner
|
||||
from pyasic.miners.base import BaseMiner
|
||||
from pyasic.miners.data import (
|
||||
DataFunction,
|
||||
DataLocations,
|
||||
@@ -29,6 +30,7 @@ from pyasic.miners.data import (
|
||||
RPCAPICommand,
|
||||
WebAPICommand,
|
||||
)
|
||||
from pyasic.rpc.antminer import AntminerRPCAPI
|
||||
from pyasic.ssh.antminer import AntminerModernSSH
|
||||
from pyasic.web.antminer import AntminerModernWebAPI, AntminerOldWebAPI
|
||||
|
||||
@@ -88,6 +90,9 @@ class AntminerModern(BMMiner):
|
||||
_web_cls = AntminerModernWebAPI
|
||||
web: AntminerModernWebAPI
|
||||
|
||||
_rpc_cls = AntminerRPCAPI
|
||||
web: AntminerRPCAPI
|
||||
|
||||
_ssh_cls = AntminerModernSSH
|
||||
ssh: AntminerModernSSH
|
||||
|
||||
@@ -207,7 +212,7 @@ class AntminerModern(BMMiner):
|
||||
]
|
||||
|
||||
try:
|
||||
rpc_stats = await self.rpc.send_command("stats", new_api=True)
|
||||
rpc_stats = await self.rpc.stats(new_api=True)
|
||||
except APIError:
|
||||
return hashboards
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ class VNish(BMMiner):
|
||||
_web_cls = VNishWebAPI
|
||||
web: VNishWebAPI
|
||||
|
||||
supports_shutdown = True
|
||||
|
||||
firmware = "VNish"
|
||||
|
||||
data_locations = VNISH_DATA_LOC
|
||||
|
||||
@@ -102,7 +102,9 @@ MINER_CLASSES = {
|
||||
"ANTMINER S19 HYDRO": BMMinerS19Hydro,
|
||||
"ANTMINER S19 PRO HYD.": BMMinerS19ProHydro,
|
||||
"ANTMINER S19 PRO+ HYD.": BMMinerS19ProPlusHydro,
|
||||
"ANTMINER S19K PRO": BMMinerS19KPro,
|
||||
"ANTMINER T19": BMMinerT19,
|
||||
"ANTMINER S21": BMMinerS21,
|
||||
},
|
||||
MinerTypes.WHATSMINER: {
|
||||
None: type("WhatsminerUnknown", (BTMiner, WhatsMinerMake), {}),
|
||||
|
||||
@@ -132,3 +132,9 @@ class S19ProPlusHydro(AntMinerMake):
|
||||
expected_chips = 180
|
||||
expected_hashboards = 4
|
||||
expected_fans = 0
|
||||
|
||||
|
||||
class S19KPro(AntMinerMake):
|
||||
raw_model = "S19K Pro"
|
||||
expected_chips = 77
|
||||
expected_fans = 4
|
||||
|
||||
@@ -34,5 +34,6 @@ from .S19 import (
|
||||
S19ProHydro,
|
||||
S19ProPlus,
|
||||
S19ProPlusHydro,
|
||||
S19KPro,
|
||||
)
|
||||
from .T19 import T19
|
||||
|
||||
36
pyasic/rpc/antminer.py
Normal file
36
pyasic/rpc/antminer.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from pyasic.rpc.bmminer import BMMinerRPCAPI
|
||||
|
||||
|
||||
class AntminerRPCAPI(BMMinerRPCAPI):
|
||||
async def stats(self, new_api: bool = False) -> dict:
|
||||
if new_api:
|
||||
return await self.send_command("stats", new_api=True)
|
||||
return await super().stats()
|
||||
|
||||
async def rate(self):
|
||||
return await self.send_command("rate", new_api=True)
|
||||
|
||||
async def pools(self, new_api: bool = False):
|
||||
if new_api:
|
||||
return await self.send_command("pools", new_api=True)
|
||||
return await self.send_command("pools")
|
||||
|
||||
async def reload(self):
|
||||
return await self.send_command("reload", new_api=True)
|
||||
|
||||
async def summary(self, new_api: bool = False):
|
||||
if new_api:
|
||||
return await self.send_command("summary", new_api=True)
|
||||
return await self.send_command("summary")
|
||||
|
||||
async def warning(self):
|
||||
return await self.send_command("warning", new_api=True)
|
||||
|
||||
async def new_api_pools(self):
|
||||
return await self.pools(new_api=True)
|
||||
|
||||
async def new_api_stats(self):
|
||||
return await self.stats(new_api=True)
|
||||
|
||||
async def new_api_summary(self):
|
||||
return await self.summary(new_api=True)
|
||||
@@ -28,6 +28,7 @@ _settings = { # defaults
|
||||
"factory_get_timeout": 3,
|
||||
"get_data_retries": 1,
|
||||
"api_function_timeout": 5,
|
||||
"antminer_mining_mode_as_str": False,
|
||||
"default_whatsminer_rpc_password": "admin",
|
||||
"default_innosilicon_web_password": "admin",
|
||||
"default_antminer_web_password": "root",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pyasic"
|
||||
version = "0.54.2"
|
||||
version = "0.54.8"
|
||||
description = "A simplified and standardized interface for Bitcoin ASICs."
|
||||
authors = ["UpstreamData <brett@upstreamdata.ca>"]
|
||||
repository = "https://github.com/UpstreamData/pyasic"
|
||||
@@ -9,7 +9,7 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
httpx = "^0.26.0"
|
||||
httpx = ">=0.26.0"
|
||||
asyncssh = "^2.14.2"
|
||||
passlib = "^1.7.4"
|
||||
pyaml = "^23.12.0"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License. -
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
from tests.api_tests import *
|
||||
from tests.rpc_tests import *
|
||||
from tests.config_tests import TestConfig
|
||||
from tests.miners_tests import MinersTest
|
||||
from tests.network_tests import NetworkTest
|
||||
|
||||
@@ -128,9 +128,9 @@ class TestConfig(unittest.TestCase):
|
||||
def test_am_modern_serialize(self):
|
||||
correct_config = {
|
||||
"bitmain-fan-ctrl": True,
|
||||
"bitmain-fan-pwn": "90",
|
||||
"bitmain-fan-pwm": "90",
|
||||
"freq-level": "100",
|
||||
"miner-mode": "0",
|
||||
"miner-mode": 0,
|
||||
"pools": [
|
||||
{
|
||||
"url": "stratum+tcp://stratum.test.io:3333",
|
||||
|
||||
35
tests/config_tests/fans.py
Normal file
35
tests/config_tests/fans.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
|
||||
from pyasic.config import FanModeConfig
|
||||
|
||||
|
||||
class TestFanConfig(unittest.TestCase):
|
||||
def test_serialize_and_deserialize(self):
|
||||
for fan_mode in FanModeConfig:
|
||||
with self.subTest(
|
||||
msg=f"Test serialization and deserialization of fan config",
|
||||
fan_mode=fan_mode,
|
||||
):
|
||||
conf = fan_mode()
|
||||
dict_conf = conf.as_dict()
|
||||
self.assertEqual(conf, FanModeConfig.from_dict(dict_conf))
|
||||
|
||||
def test_bosminer_deserialize_and_serialize(self):
|
||||
for fan_mode in FanModeConfig:
|
||||
with self.subTest(
|
||||
msg=f"Test serialization and deserialization of bosminer fan config",
|
||||
fan_mode=fan_mode,
|
||||
):
|
||||
conf = fan_mode()
|
||||
bos_conf = conf.as_bosminer()
|
||||
self.assertEqual(conf, FanModeConfig.from_bosminer(bos_conf))
|
||||
|
||||
def test_am_modern_deserialize_and_serialize(self):
|
||||
for fan_mode in FanModeConfig:
|
||||
with self.subTest(
|
||||
msg=f"Test serialization and deserialization of antminer modern fan config",
|
||||
fan_mode=fan_mode,
|
||||
):
|
||||
conf = fan_mode()
|
||||
am_conf = conf.as_am_modern()
|
||||
self.assertEqual(conf, FanModeConfig.from_am_modern(am_conf))
|
||||
Reference in New Issue
Block a user