commit 4e86f2f3b6a5788d88495ecb5a6caf0e64826c1e Author: HassBox Date: Fri Jun 14 09:39:21 2024 +0800 deploy diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..39b0a70 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,36 @@ +name: Release + +on: + release: + types: [published] + +jobs: + release-zip: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: ZIP Component Dir + run: | + cd ${{ github.workspace }}/custom_components/state_grid + zip -r state_grid.zip ./ + + - name: Upload zip to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ github.workspace }}/custom_components/state_grid/state_grid.zip + asset_name: state_grid.zip + tag: ${{ github.ref }} + overwrite: true + + - name: Upload zip to OSS + uses: tvrcgo/upload-to-oss@master + with: + key-id: ${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }} + key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }} + region: ${{ secrets.ALIYUN_OSS_REGION }} + bucket: ${{ secrets.ALIYUN_OSS_BUCKET }} + assets: | + ${{ github.workspace }}/custom_components/state_grid/state_grid.zip:/state_grid.zip + ${{ github.workspace }}/custom_components/state_grid/state_grid.zip:/integration/hass-box/state_grid/${{ github.ref_name }}/state_grid.zip diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..0220b53 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,15 @@ +name: Validate + +on: + push: + pull_request: + +jobs: + validate-hassfest: + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout the repository + uses: actions/checkout@v3.5.3 + + - name: 🏃 Hassfest validation + uses: home-assistant/actions/hassfest@master diff --git a/README.md b/README.md new file mode 100644 index 0000000..c99b3a5 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# 国家电网 + +集成国家电网每日用电等数据,支持多户号,阶梯用电。 + +## 安装/更新 + +#### 方法 1: 通过`Samba`或`SFTP`手动安装 + +下载并复制`custom_components/state_gird`文件夹到 Home Assistant 根目录下的`custom_components`文件夹 + +#### 方法 2: 通过`SSH`或`Terminal & SSH`加载项执行一键安装命令 + +```shell +curl -fsSL get.hassbox.cn/state_grid | bash +``` + +#### 方法 3: 通过`shell_command`服务 + +1. 复制下面的代码到 Home Assistant 配置文件`configuration.yaml` + + ```yaml + shell_command: + install_state_grid: |- + curl -fsSL get.hassbox.cn/state_grid | bash + ``` + +2. 重启 Home Assistant + +3. 在 Home Assistant 开发者工具中调用此服务[`service: shell_command.install_state_grid`](https://my.home-assistant.io/redirect/developer_call_service/?service=shell_command.install_state_grid) diff --git a/custom_components/state_grid/__init__.py b/custom_components/state_grid/__init__.py new file mode 100644 index 0000000..283f772 --- /dev/null +++ b/custom_components/state_grid/__init__.py @@ -0,0 +1,17 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.const import Platform + +from .const import DOMAIN +from .utils.store import async_load_from_store +from .data_client import StateGridDataClient + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + config = await async_load_from_store(hass, "state_grid.config") or None + hass.data[DOMAIN] = StateGridDataClient(hass = hass, config = config) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, Platform.SENSOR) + ) + return True diff --git a/custom_components/state_grid/config_flow.py b/custom_components/state_grid/config_flow.py new file mode 100644 index 0000000..756cce8 --- /dev/null +++ b/custom_components/state_grid/config_flow.py @@ -0,0 +1,270 @@ +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.helpers.selector import selector + + +from .utils.logger import LOGGER +from .const import DOMAIN + +from .data_client import StateGridDataClient + +class StateGridConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if self.hass.data.get(DOMAIN): + return self.async_abort(reason="single_instance_allowed") + + self.data_client = StateGridDataClient(hass=self.hass) + + options = { + "scan_login": "网上国网 App 扫码登录", + "code_login": "手机短信验证码登录", + } + + return self.async_show_menu( + step_id="user", + menu_options=options + ) + + async def async_step_scan_login(self, user_input=None): + result = await self.data_client.get_qr_code() + if result['errcode'] != 0: + return self.async_abort( + reason="get_qr_code_error", + description_placeholders={'errmsg': result["errmsg"]} + ) + + return self.async_show_form( + step_id="check_qr_code", + description_placeholders={ + "qr_image": '' + } + ) + + async def async_step_check_qr_code(self, user_input=None): + result = await self.data_client.check_qr_code() + + if result['errcode'] != 0: + return self.async_abort( + reason="check_qr_code_error", + description_placeholders={'errmsg': result["errmsg"]} + ) + + return self.async_create_entry( + title="国家电网", + data={} + ) + + async def async_step_code_login(self, user_input=None): + errors = {} + if user_input is None: + user_input = {} + else: + phone = user_input['phone'] + result = await self.data_client.send_phone_code(phone) + if result['errcode'] == 0: + return await self.async_step_verfiy_code() + else: + errors["phone"] = result["errmsg"] + + data_schema = { + vol.Required("phone") : selector({ + "text": { + "type": "number" + } + }) + } + + return self.async_show_form( + step_id="code_login", + data_schema=vol.Schema(data_schema), + errors=errors + ) + + async def async_step_verfiy_code(self, user_input=None): + errors = {} + if user_input is None: + user_input = {} + else: + code = user_input['code'] + result = await self.data_client.verfiy_phone_code(code) + if result['errcode'] == 0: + return self.async_create_entry( + title="国家电网", + data={} + ) + + errors["code"] = result["errmsg"] + + data_schema = { + vol.Required("code") : selector({ + "text": { + "type": "number" + } + }) + } + + return self.async_show_form( + step_id="verfiy_code", + data_schema=vol.Schema(data_schema), + errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow(entry: config_entries.ConfigEntry): + return OptionsFlowHandler(entry) + +class OptionsFlowHandler(config_entries.OptionsFlow): + + def __init__(self, config_entry: config_entries.ConfigEntry): + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + self.data_client: StateGridDataClient = self.hass.data[DOMAIN] + if self.data_client.need_login is True: + return await self.async_step_user() + else: + return await self.async_step_debug() + + async def async_step_debug(self, user_input=None): + if user_input is None: + user_input = {} + else: + self.data_client.refresh_interval = int(user_input['refresh_interval']) + self.data_client.is_debug = user_input['is_debug'] + await self.data_client.save_data() + return self.async_create_entry( + title="国家电网", + data={} + ) + + data_schema = { + vol.Required("refresh_interval", default=str(self.data_client.refresh_interval)) : selector({ + "select": { + "options": [ + {"label":"每1小时", "value": "1"}, + {"label":"每2小时", "value": "2"}, + {"label":"每3小时", "value": "3"}, + {"label":"每4小时", "value": "4"}, + {"label":"每5小时", "value": "5"}, + {"label":"每6小时", "value": "6"}, + {"label":"每7小时", "value": "7"}, + {"label":"每8小时", "value": "8"}, + {"label":"每9小时", "value": "9"}, + {"label":"每10小时", "value": "10"}, + {"label":"每11小时", "value": "11"}, + {"label":"每12小时", "value": "12"} + ] + } + }), + vol.Required("is_debug", default=self.data_client.is_debug): selector({ + "boolean": {} + }) + } + + return self.async_show_form( + step_id="debug", + data_schema=vol.Schema(data_schema) + ) + + async def async_step_user(self, user_input=None): + options = { + "scan_login": "网上国网 App 扫码登录", + "code_login": "手机短信验证码登录", + } + + return self.async_show_menu( + step_id="user", + menu_options=options + ) + + async def async_step_scan_login(self, user_input=None): + result = await self.data_client.get_qr_code() + if result['errcode'] != 0: + return self.async_abort( + reason="get_qr_code_error", + description_placeholders={'errmsg': result["errmsg"]} + ) + + return self.async_show_form( + step_id="check_qr_code", + description_placeholders={ + "qr_image": '' + } + ) + + async def async_step_check_qr_code(self, user_input=None): + result = await self.data_client.check_qr_code() + + if result['errcode'] != 0: + return self.async_abort( + reason="check_qr_code_error", + description_placeholders={'errmsg': result["errmsg"]} + ) + + return self.async_create_entry( + title="国家电网", + data={} + ) + + async def async_step_code_login(self, user_input=None): + errors = {} + if user_input is None: + user_input = {} + else: + phone = user_input['phone'] + result = await self.data_client.send_phone_code(phone) + if result['errcode'] == 0: + return await self.async_step_verfiy_code() + else: + errors["phone"] = result["errmsg"] + + data_schema = { + vol.Required("phone") : selector({ + "text": { + "type": "number" + } + }) + } + + return self.async_show_form( + step_id="code_login", + data_schema=vol.Schema(data_schema), + errors=errors + ) + + async def async_step_verfiy_code(self, user_input=None): + errors = {} + if user_input is None: + user_input = {} + else: + code = user_input['code'] + result = await self.data_client.verfiy_phone_code(code) + if result['errcode'] == 0: + return self.async_create_entry( + title="国家电网", + data={} + ) + + errors["code"] = result["errmsg"] + + data_schema = { + vol.Required("code") : selector({ + "text": { + "type": "number" + } + }) + } + + return self.async_show_form( + step_id="verfiy_code", + data_schema=vol.Schema(data_schema), + errors=errors + ) diff --git a/custom_components/state_grid/const.py b/custom_components/state_grid/const.py new file mode 100644 index 0000000..7e1bdab --- /dev/null +++ b/custom_components/state_grid/const.py @@ -0,0 +1,4 @@ +DOMAIN = "state_grid" +PACKAGE_NAME = "custom_components.state_grid" +VERSION = "0.1.1" +VERSION_STORAGE = 1 diff --git a/custom_components/state_grid/coordinator.py b/custom_components/state_grid/coordinator.py new file mode 100644 index 0000000..7fc7c9d --- /dev/null +++ b/custom_components/state_grid/coordinator.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .data_client import StateGridDataClient + +from .const import DOMAIN +from .utils.logger import LOGGER + +class StateGridCoordinator(DataUpdateCoordinator): + + def __init__(self, hass: HomeAssistant) -> None: + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=300) + ) + self.first_setup = True + self.data_client: StateGridDataClient = hass.data[DOMAIN] + + async def _async_update_data(self): + await self.data_client.refresh_data(setup=self.first_setup) + self.first_setup = False + return self.data_client.get_door_account() \ No newline at end of file diff --git a/custom_components/state_grid/data_client.py b/custom_components/state_grid/data_client.py new file mode 100644 index 0000000..ef2193e --- /dev/null +++ b/custom_components/state_grid/data_client.py @@ -0,0 +1,350 @@ +_Al='constType' +_Ak='queryYear' +_Aj='provinceCode' +_Ai='access_token' +_Ah='businessType' +_Ag='qrCodeSerial' +_Af='redirect_url' +_Ae='_access_token' +_Ad='refresh_interval' +_Ac='doorAccountDict' +_Ab='refreshToken' +_Aa='accessToken' +_AZ='/osg-web0004/member/c24/f01' +_AY='BCP_00026' +_AX='serviceCode_smt' +_AW='WEBA10070900' +_AV='serviceType' +_AU='jM_custType' +_AT='jM_busiTypeCode' +_AS='doorNumberManeger' +_AR='month' +_AQ='consType' +_AP='userAccountId' +_AO='powerUserList' +_AN='publicKey' +_AM='WEBA10070800' +_AL='timeDay' +_AK='WEBA10070700' +_AJ='channelNo' +_AI='monthBillList' +_AH='yearTotalCost' +_AG='provinceId' +_AF='proCode' +_AE='loginAccount' +_AD='0000' +_AC='refresh_token' +_AB='skey' +_AA='userInfo' +_A9='0101046' +_A8='account_balance' +_A7='list' +_A6='userName' +_A5='acctId' +_A4='keyCode' +_A3='querytypeCode' +_A2='01010049' +_A1='account' +_A0='resultCode' +_z='quInfo' +_y=True +_x='BCP_000026' +_w='app' +_v='WEBALIPAY_01' +_u='order' +_t='state_grid' +_s='latestBillMonth' +_r='consNo_dst' +_q='consNo' +_p='authFlag' +_o='09' +_n='0101183' +_m='stepelect' +_l='daily_bill_list' +_k='token' +_j=False +_i='0101154' +_h='getday' +_g='bizrt' +_f='clearCache' +_e='orgNo' +_d='timestamp' +_c='promotCode' +_b='errmsg' +_a='01' +_Z='SGAPP' +_Y='devciceId' +_X='devciceIp' +_W='proNo' +_V='tenant' +_U='member' +_T='promotType' +_S='userId' +_R='target' +_Q='srvrt' +_P='subBusiTypeCode' +_O='serialNo' +_N='serCat' +_M='srvCode' +_L='0902' +_K='uscInfo' +_J='busiTypeCode' +_I='channelCode' +_H='code' +_G=None +_F='1' +_E='source' +_D='funcCode' +_C='serviceCode' +_B='errcode' +_A='data' +import json,time,aiohttp,urllib.parse,datetime +from.utils.logger import LOGGER +from.utils.store import async_save_to_store +from.utils.crypt import a,b,c,d,e +configuration={_K:{_U:_L,_X:'',_Y:'',_V:_t},_E:_Z,_R:'32101',_I:_L,_AJ:_L,'toPublish':_a,'siteId':'2012000000033700',_M:'',_O:'',_D:'',_C:{_u:_i,'uploadPic':'0101296','pauseSCode':'0101250','pauseTCode':'0101251','listconsumers':'0101093','messageList':'0101343','submit':'0101003','sbcMsg':'0101210','powercut':'0104514','BkAuth01':'f15','BkAuth02':'f18','BkAuth03':'f02','BkAuth04':'f17','BkAuth05':'f05','BkAuth06':'f16','BkAuth07':'f01','BkAuth08':'f03'},'electricityArchives':{'servicecode':'0104505',_E:_L},'subscriptionList':{_M:'APP_SGPMS_05_030',_O:'22',_I:_L,_D:'22',_R:'-1'},'userInformation':{_C:'01008183',_E:_Z},'userInform':{_C:_n,_E:_Z},'elesum':{_I:_L,_D:_v,_c:_F,_T:_F,_C:'0101143',_E:_w},_A1:{_I:_L,_D:'WEBA1007200'},_AS:{_E:_L,_R:'-1',_I:_o,_AJ:_o,_C:_A2,_D:'WEBA40050000',_K:{_U:_L,_X:'',_Y:'',_V:_t}},'doorAuth':{_E:_Z,_C:'f04'},'xinZ':{_N:'101',_AT:'101','fJ_busiTypeCode':'102',_AU:'03','fJ_custType':'02',_AV:_a,_P:'',_D:_AK,_u:_i,_E:_Z,_A3:_F},'onedo':{_C:_A9,_E:_Z,_D:_AK,'queryType':'03'},'xinHuTongDian':{_N:'110',_J:'211',_P:'21102',_D:'WEBA10071200',_I:_L,_E:_o,_C:_n},'company':{_N:'104',_D:_AK,_AV:'02',_A3:_F,_p:_F,_E:_Z,_u:_i},'charge':{_I:_o,_D:'WEBA10071300',_AJ:'0901',_N:'102',_AU:_a,_AT:'102'},'other':{_I:_o,_D:'WEBA10079700',_N:'129',_J:'999',_P:'21501',_C:_x,_M:'',_O:''},'vatchange':{'submit':'0101003',_J:'320',_P:'',_N:'115',_D:'WEBA10074000',_p:_F},'bill':{_f:_F,_D:_v,_T:_F,_C:_x},_m:{_I:_L,_D:_v,_T:_F,_f:_o,_C:_x,_E:_w},_h:{_I:_L,_f:'11',_D:_v,_c:_F,_T:_F,_C:_x,_E:_w},'mouthOut':{_I:_L,_f:'11',_D:_v,_c:_F,_T:_F,_C:_x,_E:_w},'meter':{_N:'114',_J:'304',_D:'WEBA10071000',_P:'',_C:_A9,_O:''},'complaint':{_J:'005','srvMode':_L,'anonymousFlag':'0','replyMode':_a,'retvisitFlag':_a},'report':{_J:'006'},'tradewinds':{_J:'019'},'somesay':{_J:'091'},'faultrepair':{_D:_AW,_C:_n,_N:'111',_J:'001',_P:'21505'},'electronicInvoice':{_N:'105',_J:'0'},'rename':{_C:_A9,_D:'WEBA10076100',_J:'210',_N:'109',_p:_F,'gh_busiTypeCode':'211','gh_subusi':'21101',_O:'',_M:''},'pause':{_P:'',_C:_A2,_D:'WEBA10073600',_N:'107',_J:'203','jr_busi':'201',_O:'',_M:''},'capacityRecovery':{_C:_A2,_E:_Z,_M:'',_O:'',_D:'WEBA10073700','busiTypeCode_stop':'204','busiTypeCode_less':'202',_J:'202',_P:'',_N:'108',_AL:'5',_p:_F},'electricityPriceChange':{_C:_n,_J:'215',_P:'21502',_N:'113',_p:_F,_AL:'15',_D:'WEBA10073900WEB',_M:'',_O:''},'electricityPriceStrategyChange':{_C:'01008183',_J:'215',_P:'21506',_N:'160',_D:'WEBV00000517WEB',_M:'',_O:''},'eemandValueAdjustment':{_C:_n,_M:'',_O:'',_N:'112',_D:'WEBA10073800',_J:'215',_P:'21504',_p:_F,_AL:'5','getMonthServiceCode':_A9},'businessProgress':{_C:_n,_M:_a,_D:'WEB01'},'increase':{_E:_Z,_O:'',_M:'',_AX:_A2,_C:_i,_u:_i,_D:_AM,_A3:_F,_N:'106',_J:'111',_P:''},'fjincrea':{_N:'105',_J:'110',_P:'',_E:_Z,_D:_AM,_O:'',_M:'',_AX:_A2,_C:_i,_u:_i,_A3:_F},'persIncrea':{_N:'105',_J:'109',_u:_i,_P:'',_E:_Z,_D:_AM,_A3:_F},'fgdChange':{_C:_n,_M:_a,_I:_o,_D:_AW,_J:'215',_P:'21505',_N:'111',_p:_F},'createOrder':{_I:_L,_D:_v,_M:'BCP_000001','chargeMode':'02','conType':_a,'bizTypeId':'BT_ELEC'},'largePopulation':{_J:'383',_D:'WEBA10076800',_P:'',_M:'',_T:'',_c:'',_I:'0901',_N:'383',_C:'',_O:''},'biaoJiCode':{_C:'0104507',_E:'1704',_I:'1704'},'twoGuar':{_J:'402',_P:'40201',_D:'web_twoGuar'},'electTrend':{_C:_AY,_I:_L},'emergency':{_C:_AY,_D:'A10000000',_I:_L},'infoPublic':{_C:'2545454',_E:_w}} +appKey='3def6c365d284881bf1a9b2b502ee68c' +appSecret='ab7357dae64944a197ace37398897f64' +baseApi='https://www.95598.cn/api' +get_request_key_api='/oauth2/outer/c02/f02' +get_qr_code_api='/osg-open-uc0001/member/c8/f24' +get_qr_code_status_api='/osg-web0004/open/c50/f02' +get_qr_code_token_api='/osg-uc0013/member/c4/f04' +send_code_api='/osg-open-uc0001/member/c8/f04' +code_login_api='/osg-uc0013/member/c4/f02' +getCertificationApi='/osg-open-uc0001/member/c8/f11' +get_request_authorize_api='/oauth2/oauth/authorize' +get_web_token_api='/oauth2/outer/getWebToken' +refresh_web_token_api='/oauth2/outer/refresh_web_token' +get_door_number_api='/osg-open-uc0001/member/c9/f02' +get_door_balance_api='/osg-open-bc0001/member/c05/f01' +get_door_bill_api='/osg-open-bc0001/member/c01/f02' +get_door_ladder_api='/osg-open-bc0001/member/c04/f03' +getJiaoFeiRecordApi=_AZ +get_door_daily_bill_api=_AZ +sessionIdControlApiList=[get_qr_code_api,get_qr_code_status_api,get_qr_code_token_api,send_code_api,code_login_api] +keyCodeControlApiList=[get_qr_code_status_api,get_qr_code_token_api,send_code_api,code_login_api,getCertificationApi,get_request_authorize_api,get_web_token_api,refresh_web_token_api,get_door_number_api,get_door_balance_api,get_door_bill_api,get_door_ladder_api,getJiaoFeiRecordApi,get_door_daily_bill_api] +authControlApiList=[get_door_number_api,get_door_balance_api,get_door_bill_api,get_door_ladder_api,getJiaoFeiRecordApi,get_door_daily_bill_api] +tControlApiList=[getCertificationApi,get_door_balance_api,get_door_bill_api,get_door_ladder_api,getJiaoFeiRecordApi,get_door_daily_bill_api] +def json_dumps(data):return json.dumps(data,separators=(',',':'),ensure_ascii=_j) +def normal_round(num,ndigits=0): + A=ndigits + if A==0:return int(num+.5) + else:B=10**A;return int(num*B+.5)/B +def catchFloat(data,key): + if key in data: + try:return normal_round(float(data[key]),2) + except:return 0 + else:return 0 +class StateGridDataClient: + hass=_G;session=_G;keyCode=_G;publicKey=_G;need_login=_j;phone=_G;codeKey=_G;serialNo=_G;qrCodeSerial=_G;userInfo=_G;accountInfo=_G;powerUserList=_G;doorAccountDict={};cookie=[];timestamp=int(time.time()*1000);accessToken=_G;refreshToken=_G;token=_G;expirationDate=_G;refresh_interval=6;is_debug=_j + def __init__(A,hass,config=_G): + B=config;A.hass=hass;C=aiohttp.TCPConnector(ssl=_j);D=aiohttp.CookieJar(quote_cookie=_y);A.session=aiohttp.ClientSession(cookie_jar=D,connector=C) + if B is not _G: + try:A.keyCode=B[_A4];A.publicKey=B[_AN];A.accessToken=B[_Aa];A.refreshToken=B[_Ab];A.token=B[_k];A.userInfo=B[_AA];A.powerUserList=B[_AO];A.doorAccountDict=B[_Ac];A.refresh_interval=B[_Ad];A.is_debug=B['is_debug'] + except Exception as E:LOGGER.error(E) + async def save_data(A):B={};B[_A4]=A.keyCode;B[_AN]=A.publicKey;B[_Aa]=A.accessToken;B[_Ab]=A.refreshToken;B[_k]=A.token;B[_AA]=A.userInfo;B[_AO]=A.powerUserList;B[_Ac]=A.doorAccountDict;B[_Ad]=A.refresh_interval;B['is_debug']=A.is_debug;await async_save_to_store(A.hass,'state_grid.config',B) + def encrypt_post_data(A,data):B={_Ae:A.accessToken[len(A.accessToken)//2:]if A.accessToken else'','_t':A.token[len(A.token)//2:]if A.token else'','_data':data,_d:A.timestamp};return A.encrypt_wapper_data(B) + def encrypt_wapper_data(A,data):B=a(json_dumps(data),A.keyCode);return{_A:B+c(B+str(A.timestamp)),_AB:d(A.keyCode,A.publicKey),_d:str(A.timestamp)} + def handle_request_result_message(E,api,result): + D='message';C='resultMessage';A=result;B=_G + if _A in A and _Q in A[_A]and C in A[_A][_Q]:B=A[_A][_Q][C] + elif _Q in A and C in A[_Q]:B=A[_Q][C] + elif D in A:B=A[D] + else:B=json_dumps(A) + if E.is_debug:LOGGER.error(api+': '+B);LOGGER.error(json_dumps(A)) + return B + async def __fetch_safe(C,api,data): + B=await C.__fetch(api,data) + if _H not in B:return B + A=B[_H] + if 10015==A or 10108==A or 10009==A or 10207==A or 10005==A or 10010==A or 30010==A or 10002==A: + await C.__refresh_token() + if C.need_login is _y:return B + else:return await C.__fetch(api,data) + else:return B + async def __fetch(B,api,data,header=_G): + T='encryptData';S='464606a4-184c-4beb-b442-2ab7761d0796';R='key_code';Q='state';P='sign';O='grant_type';N='application/json;charset=UTF-8';M='Content-Type';L=header;K='client_secret';I='client_id';E=api;B.timestamp=int(time.time()*1000);D=B.timestamp + if B.keyCode is _G:B.keyCode=e(32,16,2) + F=B.keyCode;G={'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36','Accept':N,M:N,'version':'1.0',_E:'0901',_d:str(D),'wsgwType':'web','appKey':appKey};A=data + if E==get_request_key_api:A={I:appKey,K:appSecret};H=a(json_dumps(A),F);A={_A:H+c(H+str(D)),_AB:d(F,'042BC7AD510BF9793B7744C8854C56A8C95DD1027EE619247A332EC6ED5B279F435A23D62441FE861F4B0C963347ECD5792F380B64CA084BE8BE41151F8B8D19C8'),I:appKey,_d:str(D)} + elif E==get_qr_code_api:A={_Ae:'','_t':'','_data':A,_d:D} + elif E==get_request_authorize_api: + A={I:appKey,'response_type':_H,_Af:'/test',_d:D,'rsi':B.token};A=urllib.parse.urlencode(A);G[M]='application/x-www-form-urlencoded; charset=UTF-8';G[_A4]=F + async with B.session.post(baseApi+E,data=A,headers=G)as J:B.session.cookie_jar.update_cookies(J.cookies);C=await J.json();C=b(C[_A],B.token);C=json.loads(C);return C + elif E==get_web_token_api:A={O:'authorization_code',P:c(appKey+str(D)),K:appSecret,Q:S,R:F,I:appKey,_d:D,_H:A[_H]};H=a(json_dumps(A),F);A={_A:H+c(H+str(D)),_AB:d(F,B.publicKey),_d:str(D)} + elif E==refresh_web_token_api:A={O:_AC,P:c(appKey+str(D)),K:appSecret,Q:S,R:F,I:appKey,_d:D,_AC:B.refreshToken};H=a(json_dumps(A),F);A={_A:H+c(H+str(D)),_AB:d(F,B.publicKey),_d:str(D)};E=get_web_token_api + else:A=B.encrypt_post_data(A) + if L is not _G:G.update(L) + if E in sessionIdControlApiList:G['sessionId']='web'+str(D) + if E in keyCodeControlApiList:G[_A4]=F + if E in authControlApiList:G['Authorization']='Bearer '+B.accessToken[:len(B.accessToken)//2] + if E in tControlApiList:G['t']=B.token[:len(B.token)//2] + async with B.session.post(baseApi+E,json=A,headers=G)as J: + C=await J.text() + if C.startswith('{'): + C=json.loads(C) + if T in C:C=b(C[T],F);C=json.loads(C) + return C + async def __get_request_key(A): + A.keyCode=_G;B=await A.__fetch(get_request_key_api,{});C=A.handle_request_result_message('get_request_key_api',B) + if B[_H]==_F:A.keyCode=B[_A][_A4];A.publicKey=B[_A][_AN];return{_B:0} + return{_B:1,_b:C} + async def __get_qr_code(B): + C={_K:{_X:'',_V:_t,_U:_L,_Y:''},_z:{'optType':_a,_O:e(28,10,1)}};A=await B.__fetch(get_qr_code_api,C);D=B.handle_request_result_message('get_qr_code_api',A) + if A[_H]==1: + if A[_A]and A[_A][_Q]and A[_A][_Q][_A0]==_AD:B.qrCodeSerial=A[_A][_g][_Ag];E=A[_A][_g]['qrCode'];return{_B:0,_A:E} + return{_B:1,_b:D} + async def __get_qr_code_status(B): + C={_g:{_Ag:B.qrCodeSerial}};D={_k:'98'+e(10,10,1)};A=await B.__fetch(get_qr_code_status_api,C,D);E=B.handle_request_result_message('get_qr_code_status_api',A) + if _H in A and A[_H]==1: + if _A in A and A[_A]!='null':B.token=A[_A];return{_B:0} + else:return{_B:1,_b:'未使用网上国网 App 扫码或确认登录'} + return{_B:1,_b:E} + async def __get_qr_code_token(B): + C={_K:{_V:_t,_U:_L,'isEncrypt':_y},_k:B.token};A=await B.__fetch(get_qr_code_token_api,C);D=B.handle_request_result_message('get_qr_code_token_api',A) + if _Q in A and _A0 in A[_Q]and A[_Q][_A0]==_AD:B.userInfo=A[_g][_AA];return{_B:0} + return{_B:1,_b:D} + async def __send_code(B,phone): + C=phone;B.phone=C;D={_K:{_X:'',_V:_t,_U:_L,_Y:''},_z:{'sendType':'0',_A1:C,_Ah:'login','accountType':''},'Channels':'web'};A=await B.__fetch(send_code_api,D);E=B.handle_request_result_message('send_code_api',A) + if A[_H]==1: + if A[_A]and A[_A][_Q]and A[_A][_Q][_A0]==_AD:B.codeKey=A[_A][_g]['codeKey'];return{_B:0} + return{_B:1,_b:E} + async def __verfiy_code(A,code): + C={_K:{_X:'',_V:_t,_U:_L,_Y:''},_z:{_A1:A.phone,_Ah:'login',_H:code,'optSys':'ios','pushId':'00000','codeKey':A.codeKey},'Channels':'web'};B=await A.__fetch(code_login_api,C);D=A.handle_request_result_message('code_login_api',B) + if _Q in B and _A0 in B[_Q]and B[_Q][_A0]==_AD:A.token=B[_g][_k];A.userInfo=B[_g][_AA][0];return{_B:0} + return{_B:1,_b:D} + async def __get_request_authorize(B): + A=await B.__fetch(get_request_authorize_api,{});E=B.handle_request_result_message('get_request_authorize_api',A) + if _H in A and A[_H]==_F:C=A[_A][_Af];D=C.rfind('code=');B.authorizeCode=C[D+5:D+5+32];return{_B:0} + return{_B:1,_b:E} + async def __get_web_token(A): + C={_H:A.authorizeCode};B=await A.__fetch(get_web_token_api,C);D=A.handle_request_result_message('get_web_token_api',B) + if _H in B and B[_H]==_F:A.accessToken=B[_A][_Ai];A.refreshToken=B[_A][_AC];return{_B:0} + return{_B:1,_b:D} + async def __refresh_web_token(B): + A=await B.__fetch(refresh_web_token_api,{});C=B.handle_request_result_message('refresh_web_token_api',A) + if _H in A and A[_H]==_F:B.accessToken=A[_A][_Ai];B.refreshToken=A[_A][_AC];return{_B:0} + return{_B:1,_b:C} + async def __get_door_number(C): + A=configuration[_AS];D={_C:A[_C],_E:A[_E],_R:A[_R],_K:{_U:A[_K][_U],_X:A[_K][_X],_Y:A[_K][_Y],_V:A[_K][_V]},_z:{_S:C.userInfo[_S]},_k:C.token};B=await C.__fetch_safe(get_door_number_api,D);E=C.handle_request_result_message('get_door_number_api',B) + if _H in B and B[_H]==1 and _A in B and _g in B[_A]:C.powerUserList=B[_A][_g][_AO];return{_B:0} + return{_B:1,_b:E} + async def __get_door_balance(B,door_account): + A=door_account;E={_A:{_M:'',_O:'',_I:configuration[_A1][_I],_D:configuration[_A1][_D],_A5:B.userInfo[_S],_A6:B.userInfo.get(_AE,B.userInfo.get('nickname',_G)),_T:_F,_c:_F,_AP:B.userInfo[_S],_A7:[{'consNoSrc':A[_r],_AF:A.get(_W,A.get(_AG,_G)),'sceneType':A.get('consSortCode',A.get('elecTypeCode',_G)),_q:A[_q],_e:A[_e]}]},_C:'0101143',_E:configuration[_E],_R:A.get(_W,A.get(_AG,_G))};C=await B.__fetch_safe(get_door_balance_api,E);B.handle_request_result_message('get_door_balance_api',C) + if _H in C and C[_H]==1 and _A in C and _A7 in C[_A]: + D=C[_A][_A7] + if len(D)!=0:A[_A8]=D[0] + async def __get_door_bill(C,door_account,year): + E='dataInfo';D='mothEleList';B=door_account;F={_A:{_A5:C.userInfo[_S],_I:configuration[_I],_f:'11',_AQ:B[_Al],_D:'ALIPAY_01',_e:B[_e],_AF:B[_W],_c:_F,_T:_F,_O:'',_M:'',_A6:'',_Aj:B[_W],_AP:C.userInfo[_S],_q:B[_q],_Ak:year},_C:_x,_E:_w,_R:B[_W]};A=await C.__fetch_safe(get_door_bill_api,F);C.handle_request_result_message('get_door_bill_api',A) + if _H in A and A[_H]==1 and _A in A: + if E in A[_A]:B[_AH]=A[_A][E] + if D in A[_A]: + B[_AI]=A[_A][D] + if len(A[_A][D])!=0:B[_s]=A[_A][D][-1] + async def __get_door_ladder(B,door_account,month): + F='ladder_flag';E=month;A=door_account;G=A[_r];H={_A:{_I:configuration[_m][_I],_D:configuration[_m][_D],_T:configuration[_m][_T],_f:configuration[_m][_f],_q:A[_r],_c:A[_W],_e:A[_e],'queryDate':E,_Aj:A[_W],_AQ:A[_Al],_AP:B.userInfo[_S],_O:'',_M:'',_A6:B.userInfo[_AE],_A5:B.userInfo[_S]},_C:configuration[_m][_C],_E:configuration[_m][_E],_R:A[_W]};C=await B.__fetch(get_door_ladder_api,H);I=B.handle_request_result_message('get_door_ladder_api',C) + if _H in C and C[_H]==1 and _A in C and _A7 in C[_A]: + D=C[_A][_A7] + if len(D)!=0: + D=D[0];D[_AR]=E + if D['electricParticulars']['levelFlag']=='2':A[F]=1 + else:A[F]=0 + B.doorAccountDict[G]['ladder']=D + async def __get_door_daily_bill(B,door_account,year,start_date,end_date): + D='sevenEleList';A=door_account;E={'params1':{_C:configuration[_C],_E:configuration[_E],_R:configuration[_R],_K:{_U:configuration[_K][_U],_X:configuration[_K][_X],_Y:configuration[_K][_Y],_V:configuration[_K][_V]},_z:{_S:B.userInfo[_S]},_k:B.token},'params3':{_A:{_A5:B.userInfo[_S],_q:A[_r],_AQ:_a,'endTime':end_date,_e:A[_e],_Ak:year,_AF:A.get(_W,A.get(_AG,_G)),_O:'',_M:'','startTime':start_date,_A6:B.userInfo[_AE],_D:configuration[_h][_D],_I:configuration[_h][_I],_f:configuration[_h][_f],_c:configuration[_h][_c],_T:configuration[_h][_T]},_C:configuration[_h][_C],_E:configuration[_h][_E],_R:A.get(_W,A.get(_AG,_G))},'params4':'010103'};C=await B.__fetch_safe(get_door_daily_bill_api,E);F=B.handle_request_result_message('get_door_daily_bill_api',C) + if _H in C and C[_H]==1 and _A in C and D in C[_A]:A[_l]=C[_A][D] + async def __get_door_pay_record(A,door_account):B=door_account;D=B[_r];C={'params1':{_C:configuration[_C],_E:configuration[_E],_R:configuration[_R],_K:{_U:configuration[_K][_U],_X:configuration[_K][_X],_Y:configuration[_K][_Y],_V:configuration[_K][_V]},_z:{_S:A.userInfo[_S]},_k:A.token},'params3':{_A:{_A5:A.userInfo[_S],'bgnPayDate':'2023-04-24',_I:configuration[_I],_q:B[_r],'endPayDate':'2024-04-24',_D:'webALIPAY_01','number':100,_e:B[_e],'page':_F,_AF:B[_W],_c:_F,_T:_F,_O:'',_M:'',_A6:A.userInfo[_AE]},_C:'0101051',_E:_a,_R:B[_W]},'params4':'010104'};E=await A.__fetch(getJiaoFeiRecordApi,C) + async def get_qr_code(B): + A=await B.__get_request_key() + if _B in A and A[_B]!=0:return A + return await B.__get_qr_code() + async def check_qr_code(B): + A=await B.__get_qr_code_status() + if _B in A and A[_B]!=0:return A + A=await B.__get_qr_code_token() + if _B in A and A[_B]!=0:return A + return await B.__get_token() + async def send_phone_code(B,phone): + A=await B.__get_request_key() + if _B in A and A[_B]!=0:return A + return await B.__send_code(phone) + async def verfiy_phone_code(B,code): + A=await B.__verfiy_code(code) + if _B in A and A[_B]!=0:return A + return await B.__get_token() + async def __get_token(B): + A=await B.__get_request_key() + if _B in A and A[_B]!=0:return A + A=await B.__get_request_authorize() + if _B in A and A[_B]!=0:return A + A=await B.__get_web_token() + if _B in A and A[_B]!=0:return A + A=await B.__get_door_number() + if _B in A and A[_B]!=0:return A + B.need_login=_j;await B.save_data();return{_B:0,_A:B.powerUserList} + async def __refresh_token(A): + B=await A.__get_request_key() + if _B in B and B[_B]!=0:return + B=await A.__refresh_web_token() + if _B in B and B[_B]==0:A.need_login=_j;await A.save_data() + else:A.need_login=_y;LOGGER.error('国家电网刷新 Token 失败') + async def refresh_data(C,setup=_j): + k='last_month_ele_cost';j='monthEleNum';i='year_ele_cost';h='thisTPq';g='thisNPq';f='thisVPq';e='thisPPq';d='refresh_time';U=setup;T='ladder_level_num';S='ladder_level';R='last_month_ele_num';Q='year_ele_num';L='%Y%m%d';K='day';I='dayElePq';H='balance' + if U is _y:await C.__get_door_number() + if C.need_login is _y:A[d]='刷新 Token 失败,需重新登录!';LOGGER.error('国家电网需要重新登录!');return + l=U or int(time.time()*1000)-C.timestamp>C.refresh_interval*3600*1000 + if l is _j:return + M=datetime.datetime.now();J=M-datetime.timedelta(days=1);m=f"{J.year}-{J.month:02d}-{J.day:02d}";N=J-datetime.timedelta(days=40);n=f"{N.year}-{N.month:02d}-{N.day:02d}" + for A in C.powerUserList: + o=A[_r];C.doorAccountDict[o]=A;await C.__get_door_balance(A) + if _A8 in A: + V=catchFloat(A[_A8],'prepayBal');p=catchFloat(A[_A8],'sumMoney') + if V==0:A[H]=p + else:A[H]=V + W=catchFloat(A[_A8],'historyOwe') + if W>0:A[H]=-W + else:LOGGER.error('国家电网账户余额获取失败!') + if H not in A:A[H]=0 + await C.__get_door_daily_bill(A,M.year,n,m) + if _l not in A:LOGGER.error('国家电网无法获取日用电数据!');return + O=0 + for X in range(10): + D=A[_l][X] + try:float(D[I]);break + except:O=O+1 + for X in range(O):A[_l].pop(0) + D=A[_l][0];E=datetime.datetime.strptime(D[K],L);A['daily_ele_num']=catchFloat(D,I);A['daily_p_ele_num']=catchFloat(D,e);A['daily_v_ele_num']=catchFloat(D,f);A['daily_n_ele_num']=catchFloat(D,g);A['daily_t_ele_num']=catchFloat(D,h);Y=0;Z=0;a=0;b=0;c=0 + for B in A[_l]: + G=datetime.datetime.strptime(B[K],L) + if G.month!=E.month:break + Y+=catchFloat(B,I);Z+=catchFloat(B,e);a+=catchFloat(B,f);b+=catchFloat(B,g);c+=catchFloat(B,h) + A['month_ele_num']=normal_round(Y,2);A['month_p_ele_num']=normal_round(Z,2);A['month_v_ele_num']=normal_round(a,2);A['month_n_ele_num']=normal_round(b,2);A['month_t_ele_num']=normal_round(c,2);P=E-datetime.timedelta(days=E.day);q=f"{P.year}-{P.month:02d}" + if _s not in A or A[_s][_AR]!=q:await C.__get_door_bill(A,P.year) + if _s not in A:LOGGER.error('国家电网无法获取年度用电数据!');return + if _AH in A:A[Q]=catchFloat(A[_AH],'totalEleNum');A[i]=catchFloat(A[_AH],'totalEleCost') + if Q not in A:A[Q]=0;A[i]=0 + if _AI in A:A[R]=catchFloat(A[_s],j);A[k]=catchFloat(A[_s],'monthEleCost') + if R not in A:A[R]=0;A[k]=0 + r=datetime.datetime.strptime(A[_s][_AR],'%Y%m') + if r.month==12: + F=0 + for B in A[_l]: + G=datetime.datetime.strptime(B[K],L) + if G.month!=12:break + F+=catchFloat(B,I) + else: + F=0 + for B in A[_AI]:F+=catchFloat(B,j) + s=len(A[_AI]) + for B in A[_l]: + G=datetime.datetime.strptime(B[K],L) + if G.month<=s:break + F+=catchFloat(B,I) + if F<=2760:A[S]='第一阶梯';A[T]=1 + elif F<=4800:A[S]='第二阶梯';A[T]=2 + else:A[S]='第三阶梯';A[T]=3 + A['daily_lasted_date']=f"{E.year}-{E.month:02d}-{E.day:02d}";A[d]=datetime.datetime.strftime(M,'%Y-%m-%d %H:%M:%S') + await C.save_data() + async def get_door_account_list(A):return list(A.doorAccountDict.values()) + def get_door_account(A):return A.doorAccountDict \ No newline at end of file diff --git a/custom_components/state_grid/manifest.json b/custom_components/state_grid/manifest.json new file mode 100644 index 0000000..0d07444 --- /dev/null +++ b/custom_components/state_grid/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "state_grid", + "name": "国家电网", + "codeowners": ["@hassbox"], + "config_flow": true, + "documentation": "https://hassbox.cn/", + "iot_class": "cloud_polling", + "version": "0.1.1" +} diff --git a/custom_components/state_grid/sensor.py b/custom_components/state_grid/sensor.py new file mode 100644 index 0000000..0a11fe9 --- /dev/null +++ b/custom_components/state_grid/sensor.py @@ -0,0 +1,179 @@ +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +import datetime + +from .const import DOMAIN, VERSION +from .data_client import StateGridDataClient +from .coordinator import StateGridCoordinator + +UNIT_DU = "度" +UNIT_YUAN = "元" + +ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".state_grid_" + +SENSOR_TYPES = [ + { + "key":"balance", + "name": "账户余额", + "unit_of_measurement": UNIT_YUAN, + "native_unit_of_measurement": UNIT_YUAN + }, + { + "key": "year_ele_num", + "name": "年度累计用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "year_ele_cost", + "name": "年度累计电费", + "unit_of_measurement": UNIT_YUAN, + "native_unit_of_measurement": UNIT_YUAN + }, + { + "key": "last_month_ele_num", + "name": "上个月用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "last_month_ele_cost", + "name": "上个月电费", + "unit_of_measurement": UNIT_YUAN, + "native_unit_of_measurement": UNIT_YUAN + }, + { + "key": "month_ele_num", + "name": "当月累计用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "month_p_ele_num", + "name": "当月累计峰用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "month_v_ele_num", + "name": "当月累计谷用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "month_n_ele_num", + "name": "当月累计平用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "month_t_ele_num", + "name": "当月累计尖用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_ele_num", + "name": "日总用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_p_ele_num", + "name": "日峰用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_v_ele_num", + "name": "日谷用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_n_ele_num", + "name": "日平用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_t_ele_num", + "name": "日尖用电", + "unit_of_measurement": UNIT_DU, + "native_unit_of_measurement": UNIT_DU + }, + { + "key": "daily_lasted_date", + "name": "日最新用电日期" + }, + { + "key": "refresh_time", + "name": "最近刷新时间" + } +] + +SENSOR_TYPES_FOR_LADDER = [ + { + "key": "ladder_level", + "name": "当前阶梯" + }, +] + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + coordinator = StateGridCoordinator(hass) + await coordinator.async_config_entry_first_refresh() + data_client: StateGridDataClient = hass.data[DOMAIN] + door_account_list = await data_client.get_door_account_list() + for door_account in door_account_list: + async_add_entities( + [StateGridSensor(door_account, sensor_type, entry.entry_id, coordinator) for sensor_type in SENSOR_TYPES] + ) + # if door_account["ladder_flag"] == 1: + # async_add_entities( + # StateGridSensor(door_account, sensor_type, entry.entry_id) + # for sensor_type in SENSOR_TYPES_FOR_LADDER + # ) + +class StateGridSensor(CoordinatorEntity[StateGridCoordinator], SensorEntity): + + _attr_has_entity_name = True + + def __init__( + self, door_account, sensor_type, entry_id: str, coordinator: StateGridCoordinator, + ) -> None: + super().__init__(coordinator) + self.door_account = door_account + self.sensor_type = sensor_type + self.entity_id = SENSOR_DOMAIN + ".state_grid" + "_" + door_account["consNo_dst"] + "_" + sensor_type["key"] + self._attr_name = sensor_type["name"] + self._attr_unique_id = entry_id + "-" + door_account["consNo_dst"] + "-" + sensor_type["key"] + + + if "native_unit_of_measurement" in sensor_type: + self._attr_native_unit_of_measurement = sensor_type["native_unit_of_measurement"] + + self._attr_device_info = { + "name": door_account["elecAddr_dst"], + "identifiers": {(DOMAIN, door_account["consNo_dst"])}, + "sw_version": VERSION, + "manufacturer": "HassBox", + "model": "户号:" + door_account["consName_dst"] + " - " + door_account["consNo_dst"] + } + + @property + def native_value(self): + data = self.coordinator.data[self.door_account["consNo_dst"]] + return data[self.sensor_type["key"]] + \ No newline at end of file diff --git a/custom_components/state_grid/strings.json b/custom_components/state_grid/strings.json new file mode 100644 index 0000000..7a23efe --- /dev/null +++ b/custom_components/state_grid/strings.json @@ -0,0 +1,74 @@ +{ + "config": { + "flow_title": "国家电网", + "step": { + "user": { + "title": "国家电网", + "description": "集成国家电网可以选择网上国网 App 扫码登录或短信验证码登录。如登录异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + }, + "options": { + "step": { + "debug": { + "title": "国家电网", + "description": "国家电网当日用电等数据一般隔天才会刷新,且各地区时间不一,可根据情况适当调整接口轮询间隔。", + "data": { + "refresh_interval": "轮询间隔", + "is_debug": "调试模式" + }, + "data_description": { + "is_debug": "打开调试开关后,将会打印所有请求日志。可在[配置]-[系统]-[日志]中查看。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + } + }, + "user": { + "title": "重新登录国家电网", + "description": "刷新 Token 异常,需重新登录。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + } +} diff --git a/custom_components/state_grid/translations/en.json b/custom_components/state_grid/translations/en.json new file mode 100644 index 0000000..7a23efe --- /dev/null +++ b/custom_components/state_grid/translations/en.json @@ -0,0 +1,74 @@ +{ + "config": { + "flow_title": "国家电网", + "step": { + "user": { + "title": "国家电网", + "description": "集成国家电网可以选择网上国网 App 扫码登录或短信验证码登录。如登录异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + }, + "options": { + "step": { + "debug": { + "title": "国家电网", + "description": "国家电网当日用电等数据一般隔天才会刷新,且各地区时间不一,可根据情况适当调整接口轮询间隔。", + "data": { + "refresh_interval": "轮询间隔", + "is_debug": "调试模式" + }, + "data_description": { + "is_debug": "打开调试开关后,将会打印所有请求日志。可在[配置]-[系统]-[日志]中查看。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + } + }, + "user": { + "title": "重新登录国家电网", + "description": "刷新 Token 异常,需重新登录。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + } +} diff --git a/custom_components/state_grid/translations/zh-Hans.json b/custom_components/state_grid/translations/zh-Hans.json new file mode 100644 index 0000000..7a23efe --- /dev/null +++ b/custom_components/state_grid/translations/zh-Hans.json @@ -0,0 +1,74 @@ +{ + "config": { + "flow_title": "国家电网", + "step": { + "user": { + "title": "国家电网", + "description": "集成国家电网可以选择网上国网 App 扫码登录或短信验证码登录。如登录异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + }, + "options": { + "step": { + "debug": { + "title": "国家电网", + "description": "国家电网当日用电等数据一般隔天才会刷新,且各地区时间不一,可根据情况适当调整接口轮询间隔。", + "data": { + "refresh_interval": "轮询间隔", + "is_debug": "调试模式" + }, + "data_description": { + "is_debug": "打开调试开关后,将会打印所有请求日志。可在[配置]-[系统]-[日志]中查看。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + } + }, + "user": { + "title": "重新登录国家电网", + "description": "刷新 Token 异常,需重新登录。\n如需反馈异常,可至 **HassBox全屋智能** 微信公众号联系客服。" + }, + "code_login": { + "title": "短信验证码登录", + "description": "手机号需先在网上电网 App 注册并绑定户号", + "data": { + "phone": "请输入手机号" + } + }, + "verfiy_code": { + "title": "短信验证码登录", + "data": { + "code": "请输入验证码" + } + }, + "check_qr_code": { + "title": "网上国网 App 扫码登录", + "description": "\n {qr_image} \n\n\n 请使用 网上国网 App 扫码登录,确认登录后再点击提交。" + } + }, + "abort": { + "single_instance_allowed": "该集成已经配置过了,且只能配置一次。若要重新配置,请先删除旧集成。", + "get_qr_code_error": "{errmsg}", + "check_qr_code_error": "{errmsg}" + } + } +} diff --git a/custom_components/state_grid/utils/crypt.py b/custom_components/state_grid/utils/crypt.py new file mode 100644 index 0000000..cfafd2d --- /dev/null +++ b/custom_components/state_grid/utils/crypt.py @@ -0,0 +1,232 @@ +_G='Data length error!' +_F='%08x' +_E='utf8' +_D='%%0%dx' +_C=None +_B='utf-8' +_A='p' +import random,urllib.parse,base64,binascii +from math import ceil +import copy +def str_to_bytes(input_str): + A=[] + for B in input_str: + C=urllib.parse.quote(B) + if len(C)==1:A.append(ord(B)) + else: + for D in C.split('%')[1:]:A.append(int('0x'+D,16)) + return A +def bytes_to_hex(bytes_input): + B='' + for C in bytes_input: + A=hex(C)[2:] + if len(A)==1:A='0'+A + B+=A + return B +def string_to_hex(s):return''if s==''else bytes_to_hex(str_to_bytes(s)) +def d(data,publicKey):A=data;A=string_to_hex(A).encode();B=BB(public_key=publicKey,mode=1);C=B.encrypt(A);return'04'+C.hex() +def e(e=_C,c=_C,n=_C): + D='0123456789';A=list(D) + if n==1:A=list(D) + else:A=list('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') + B=[] + if c is _C:c=len(A) + if e: + for E in range(e):B.append(A[int(random.random()*c)]) + else: + for C in range(36): + if C in[8,13,18,23]:B.append('-') + elif C==14:B.append('4') + else: + e=int(random.random()*16) + if C==19:B.append(A[3&e|8]) + else:B.append(A[e]) + return''.join(B) +def a(data,key):B=data;A=key;B=B.encode(_B);A=A.encode(_B);C=AA(padding_mode=3);C.set_key(A,SM4_ENCRYPT);D=C.crypt_cbc(A[0:8]+A[-8:],B);E=base64.b64encode(D);return E.decode() +def b(data,key):B=data;A=key;B=B.encode(_B);A=A.encode(_B);D=base64.b64decode(B);C=AA(padding_mode=3);C.set_key(A,SM4_DECRYPT);E=C.crypt_cbc(A[0:8]+A[-8:],D);return E.decode() +def c(data):A=data;A=A.encode(_B);A=bytes_to_list(A);return m_hash(A) +xor=lambda a,b:list(map(lambda x,y:x^y,a,b)) +rotl=lambda x,n:x<>32-n&4294967295 +get_uint32_be=lambda key_data:key_data[0]<<24|key_data[1]<<16|key_data[2]<<8|key_data[3] +put_uint32_be=lambda n:[n>>24&255,n>>16&255,n>>8&255,n&255] +pkcs7_padding=lambda data,block=16:data+[16-len(data)%block for A in range(16-len(data)%block)] +zero_padding=lambda data,block=16:data+[0 for A in range(16-len(data)%block)] +pkcs7_unpadding=lambda data:data[:-data[-1]] +zero_unpadding=lambda data,i=1:data[:-i]if data[-i]==0 else i+1 +list_to_bytes=lambda data:b''.join([bytes((A,))for A in data]) +bytes_to_list=lambda data:[A for A in data] +random_hex=lambda x:''.join([random.choice('0123456789abcdef')for A in range(x)]) +def pboc_padding(data,block=16): + B=block;A=data;A=A.hex().upper();B=B*2 + if len(A)%B!=0:A=A+'80' + while len(A)%B!=0:A=A+'00' + return bytes_to_list(bytes.fromhex(A)) +def iso9797m2_padding(data,block=16): + B=block;A=data;A=A.hex().upper();B=B*2;A=A+'80' + while len(A)%B!=0:A=A+'00' + return bytes_to_list(bytes.fromhex(A)) +def pboc_unpadding(data): + A=data + if len(A)<16:raise Exception(_G) + if len(A)==16:0 + else: + while A[-1:]!=[128]:A.pop() + A.pop() + return A +def iso9797m2_unpadding(data): + A=data + if len(A)<=16:raise Exception(_G) + while A[-1:]!=[128]:A.pop() + A.pop();return A +default_ecc_table={'n':'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123',_A:'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF','g':'32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0','a':'FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC','b':'28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93'} +class BB: + def __init__(A,public_key,ecc_table=default_ecc_table,mode=1,asn1=False):'\n mode: 0-C1C2C3, 1-C1C3C2 (default is 1)\n ';C=ecc_table;B=public_key;A.public_key=B[2:]if B.startswith('04')and len(B)==130 else B;A.para_len=len(C['n']);A.ecc_a3=(int(C['a'],base=16)+3)%int(C[_A],base=16);A.ecc_table=C;assert mode in(0,1),'mode must be one of (0, 1)';A.mode=mode;A.asn1=asn1 + def _kg(B,k,Point): + C=Point;C='%s%s'%(C,'1');E='8' + for G in range(B.para_len-1):E+='0' + F=int(E,16);A=C;D=False + for H in range(B.para_len*4): + if D:A=B._double_point(A) + if k&F!=0: + if D:A=B._add_point(A,C) + else:D=True;A=C + k=k<<1 + return B._convert_jacb_to_nor(A) + def _double_point(A,Point): + G=Point;N=len(G);J=2*A.para_len + if N>1)-D)%int(A.ecc_table[_A],base=16) + else:E=(F+(F>>1)-D)%int(A.ecc_table[_A],base=16) + B=B*E%int(A.ecc_table[_A],base=16);Q=(B-I)%int(A.ecc_table[_A],base=16);M=_D%A.para_len;M=M*3;return M%(P,Q,O) + def _add_point(A,P1,P2): + E=2*A.para_len;J=len(P1);M=len(P2) + if JD:D=D+64 + for A in range(C,D):B.append(0) + E=H*8;I=[E%256] + for A in range(7):E=int(E/256);I.append(E%256) + for A in range(8):B.append(I[7-A]) + J=round(len(B)/64);K=[] + for A in range(0,J):K.append(B[A*64:(A+1)*64]) + F=[];F.append(IV) + for A in range(0,J):F.append(m_cf(F[A],K[A])) + L=F[A+1];G='' + for A in L:G='%s%08x'%(G,A) + return G +def m_kdf(z,klen): + A=klen;A=int(A);C=1;D=ceil(A/32);E=[A for A in bytes.fromhex(z.decode(_E))];B='' + for G in range(D):F=E+[A for A in binascii.a2b_hex((_F%C).encode(_E))];B=B+m_hash(F);C+=1 + return B[0:A*2] +SM4_BOXES_TABLE=[214,144,233,254,204,225,61,183,22,182,20,194,40,251,44,5,43,103,154,118,42,190,4,195,170,68,19,38,73,134,6,153,156,66,80,244,145,239,152,122,51,84,11,67,237,207,172,98,228,179,28,169,201,8,232,149,128,223,148,250,117,143,63,166,71,7,167,252,243,115,23,186,131,89,60,25,230,133,79,168,104,107,129,178,113,100,218,139,248,235,15,75,112,86,157,53,30,36,14,94,99,88,209,162,37,34,124,59,1,33,120,135,212,0,70,87,159,211,39,82,76,54,2,231,160,196,200,158,234,191,138,210,64,199,56,181,163,247,242,206,249,97,21,161,224,174,93,164,155,52,26,85,173,147,50,48,245,140,177,227,29,246,226,46,130,102,202,96,192,41,35,171,13,83,78,111,213,219,55,69,222,253,142,47,3,255,106,114,109,108,91,81,141,27,175,146,187,221,188,127,17,217,92,65,31,16,90,216,10,193,49,136,165,205,123,189,45,116,208,18,184,229,180,176,137,105,151,74,12,150,119,126,101,185,241,9,197,110,198,132,24,240,125,236,58,220,77,32,121,238,95,62,215,203,57,72] +SM4_FK=[2746333894,1453994832,1736282519,2993693404] +SM4_CK=[462357,472066609,943670861,1415275113,1886879365,2358483617,2830087869,3301692121,3773296373,4228057617,404694573,876298825,1347903077,1819507329,2291111581,2762715833,3234320085,3705924337,4177462797,337322537,808926789,1280531041,1752135293,2223739545,2695343797,3166948049,3638552301,4110090761,269950501,741554753,1213159005,1684763257] +SM4_ENCRYPT=0 +SM4_DECRYPT=1 +NoPadding=0 +ZERO=1 +ISO9797M2=2 +PKCS7=3 +PBOC=4 +class AA: + def __init__(A,mode=SM4_ENCRYPT,padding_mode=PKCS7):A.sk=[0]*32;A.mode=mode;A.padding_mode=padding_mode + @classmethod + def _round_key(E,ka):A=[0,0,0,0];B=put_uint32_be(ka);A[0]=SM4_BOXES_TABLE[B[0]];A[1]=SM4_BOXES_TABLE[B[1]];A[2]=SM4_BOXES_TABLE[B[2]];A[3]=SM4_BOXES_TABLE[B[3]];C=get_uint32_be(A[0:4]);D=C^rotl(C,13)^rotl(C,23);return D + @classmethod + def _f(B,x0,x1,x2,x3,rk): + def A(ka):A=[0,0,0,0];C=put_uint32_be(ka);A[0]=SM4_BOXES_TABLE[C[0]];A[1]=SM4_BOXES_TABLE[C[1]];A[2]=SM4_BOXES_TABLE[C[2]];A[3]=SM4_BOXES_TABLE[C[3]];B=get_uint32_be(A[0:4]);D=B^rotl(B,2)^rotl(B,10)^rotl(B,18)^rotl(B,24);return D + return x0^A(x1^x2^x3^rk) + def set_key(B,key,mode): + D=key;D=bytes_to_list(D);E=[0,0,0,0];C=[0]*36;E[0]=get_uint32_be(D[0:4]);E[1]=get_uint32_be(D[4:8]);E[2]=get_uint32_be(D[8:12]);E[3]=get_uint32_be(D[12:16]);C[0:4]=xor(E[0:4],SM4_FK[0:4]) + for A in range(32):C[A+4]=C[A]^B._round_key(C[A+1]^C[A+2]^C[A+3]^SM4_CK[A]);B.sk[A]=C[A+4] + B.mode=mode + if mode==SM4_DECRYPT: + for F in range(16):G=B.sk[F];B.sk[F]=B.sk[31-F];B.sk[31-F]=G + def one_round(E,sk,in_put): + D=in_put;C=[];A=[0]*36;A[0]=get_uint32_be(D[0:4]);A[1]=get_uint32_be(D[4:8]);A[2]=get_uint32_be(D[8:12]);A[3]=get_uint32_be(D[12:16]) + for B in range(32):A[B+4]=E._f(A[B],A[B+1],A[B+2],A[B+3],sk[B]) + C+=put_uint32_be(A[35]);C+=put_uint32_be(A[34]);C+=put_uint32_be(A[33]);C+=put_uint32_be(A[32]);return C + def crypt_ecb(A,input_data): + B=input_data + if A.mode==SM4_ENCRYPT: + if A.padding_mode==NoPadding:0 + if A.padding_mode==ZERO:B=zero_padding(bytes_to_list(B)) + if A.padding_mode==ISO9797M2:B=iso9797m2_padding(B) + if A.padding_mode==PKCS7:B=pkcs7_padding(bytes_to_list(B)) + if A.padding_mode==PBOC:B=pboc_padding(B) + E=len(B);D=0;C=[] + while E>0:C+=A.one_round(A.sk,B[D:D+16]);D+=16;E-=16 + if A.mode==SM4_DECRYPT: + if A.padding_mode==NoPadding:0 + if A.padding_mode==ZERO:return list_to_bytes(zero_unpadding(C)) + if A.padding_mode==ISO9797M2:return list_to_bytes(iso9797m2_unpadding(C)) + if A.padding_mode==PKCS7:return list_to_bytes(pkcs7_unpadding(C)) + if A.padding_mode==PBOC:return list_to_bytes(pboc_unpadding(C)) + return list_to_bytes(C) + def crypt_cbc(A,iv,input_data): + E=iv;C=input_data;B=0;D=[];G=[0]*16;E=bytes_to_list(E) + if A.mode==SM4_ENCRYPT: + if A.padding_mode==NoPadding:0 + if A.padding_mode==ZERO:C=zero_padding(bytes_to_list(C)) + if A.padding_mode==ISO9797M2:C=iso9797m2_padding(C) + if A.padding_mode==PKCS7:C=pkcs7_padding(bytes_to_list(C)) + if A.padding_mode==PBOC:C=pboc_padding(C) + F=len(C) + while F>0:G[0:16]=xor(C[B:B+16],E[0:16]);D+=A.one_round(A.sk,G[0:16]);E=copy.deepcopy(D[B:B+16]);B+=16;F-=16 + return list_to_bytes(D) + else: + F=len(C) + while F>0:D+=A.one_round(A.sk,C[B:B+16]);D[B:B+16]=xor(D[B:B+16],E[0:16]);E=copy.deepcopy(C[B:B+16]);B+=16;F-=16 + if A.padding_mode==NoPadding:0 + if A.padding_mode==ZERO:return list_to_bytes(zero_unpadding(D)) + if A.padding_mode==ISO9797M2:return list_to_bytes(iso9797m2_unpadding(D)) + if A.padding_mode==PKCS7:return list_to_bytes(pkcs7_unpadding(D)) + if A.padding_mode==PBOC:return list_to_bytes(pboc_unpadding(D)) + return list_to_bytes(D) \ No newline at end of file diff --git a/custom_components/state_grid/utils/logger.py b/custom_components/state_grid/utils/logger.py new file mode 100644 index 0000000..7876c10 --- /dev/null +++ b/custom_components/state_grid/utils/logger.py @@ -0,0 +1,5 @@ +import logging + +from ..const import PACKAGE_NAME + +LOGGER: logging.Logger = logging.getLogger(PACKAGE_NAME) diff --git a/custom_components/state_grid/utils/store.py b/custom_components/state_grid/utils/store.py new file mode 100644 index 0000000..99fb580 --- /dev/null +++ b/custom_components/state_grid/utils/store.py @@ -0,0 +1,64 @@ +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store +from homeassistant.util import json as json_util + +from ..const import VERSION_STORAGE +from .logger import LOGGER + +_LOGGER = LOGGER + +class StateGridStore(Store): + """A subclass of Store that allows multiple loads in the executor.""" + + def load(self): + """Load the data from disk if version matches.""" + try: + data = json_util.load_json(self.path) + except ( + BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except + ) as exception: + _LOGGER.critical( + "Could not load '%s', restore it from a backup or delete the file: %s", + self.path, + exception, + ) + if data == {} or data["version"] != self.version: + return None + return data["data"] + + +def _get_store_for_key(hass, key, encoder): + """Create a Store object for the key.""" + return StateGridStore(hass, VERSION_STORAGE, key, encoder=encoder, atomic_writes=True) + + +def get_store_for_key(hass, key): + """Create a Store object for the key.""" + return _get_store_for_key(hass, key, JSONEncoder) + + +async def async_load_from_store(hass, key): + """Load the retained data from store and return de-serialized data.""" + return await get_store_for_key(hass, key).async_load() or {} + + +async def async_save_to_store(hass, key, data): + """Generate dynamic data to store and save it to the filesystem. + + The data is only written if the content on the disk has changed + by reading the existing content and comparing it. + + If the data has changed this will generate two executor jobs + + If the data has not changed this will generate one executor job + """ + current = await async_load_from_store(hass, key) + if current is None or current != data: + await get_store_for_key(hass, key).async_save(data) + + +async def async_remove_store(hass, key): + """Remove a store element that should no longer be used.""" + if "/" not in key: + return + await get_store_for_key(hass, key).async_remove() diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..b36480e --- /dev/null +++ b/install.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# curl -fsSL get.hassbox.cn/state_grid | bash +set -e + +export LC_ALL=en_US.UTF-8 + +RED_COLOR='\033[0;31m' +GREEN_COLOR='\033[0;32m' +GREEN_YELLOW='\033[1;33m' +NO_COLOR='\033[0m' + +declare haPath +declare -a paths=( + "$PWD" + "$PWD/config" + "/config" + "$HOME/.homeassistant" + "/usr/share/hassio/homeassistant" +) + +function info () { echo -e "${GREEN_COLOR}INFO: $1${NO_COLOR}";} +function warn () { echo -e "${GREEN_YELLOW}WARN: $1${NO_COLOR}";} +function error () { echo -e "${RED_COLOR}ERROR: $1${NO_COLOR}";} + +function checkRequirement () { + if [ -z "$(command -v "$1")" ]; then + warn "'$1' 未安装,准备安装..." + apt install $1 + fi +} + +info "检查依赖项" + +checkRequirement "wget" +checkRequirement "unzip" + +for path in "${paths[@]}"; do + if [ -n "$haPath" ]; then + break + fi + + if [ -f "$path/.HA_VERSION" ]; then + haPath="$path" + fi +done + +if [ -z "$haPath" ]; then + echo + error "找不到 Home Assistant 根目录" + exit 1 +fi + +cd "$haPath" +if [ ! -d "$haPath/custom_components" ]; then + mkdir "$haPath/custom_components" +fi + +cd "$haPath/custom_components" + +info "检查依赖项 ok" + +info "下载 state_grid 集成包" +wget "https://get.hasss.cn/state_grid.zip" >/dev/null 2>&1 +info "下载 HassBox 集成包 ok" + +info "state_grid 集成包解压" +if [ -d "$haPath/custom_components/state_grid" ]; then + rm -R "$haPath/custom_components/state_grid" +fi +mkdir "$haPath/custom_components/state_grid" +unzip "$haPath/custom_components/state_grid.zip" -d "$haPath/custom_components/state_grid" >/dev/null 2>&1 +rm "$haPath/custom_components/state_grid.zip" +info "HassBox 集成包解压 ok" + +info "安装成功!请重启 Home Assistant!如需其他帮助,可至 HassBox全屋智能 微信公众号联系客服" \ No newline at end of file