From dcd7557de5fa29e3c8f5a84f90a0a387a5fae628 Mon Sep 17 00:00:00 2001 From: George Zhao Date: Thu, 1 Jul 2021 10:58:38 +0800 Subject: [PATCH] First commit and set version to v0.0.2 --- custom_components/tencent_stock/__init__.py | 103 ++++++++++++++++ custom_components/tencent_stock/config | 9 ++ custom_components/tencent_stock/const.py | 7 ++ custom_components/tencent_stock/manifest.json | 12 ++ custom_components/tencent_stock/sensor.py | 113 ++++++++++++++++++ 5 files changed, 244 insertions(+) create mode 100644 custom_components/tencent_stock/__init__.py create mode 100644 custom_components/tencent_stock/config create mode 100644 custom_components/tencent_stock/const.py create mode 100644 custom_components/tencent_stock/manifest.json create mode 100644 custom_components/tencent_stock/sensor.py diff --git a/custom_components/tencent_stock/__init__.py b/custom_components/tencent_stock/__init__.py new file mode 100644 index 0000000..e47e96e --- /dev/null +++ b/custom_components/tencent_stock/__init__.py @@ -0,0 +1,103 @@ +import logging +import async_timeout +import requests +import datetime +import re +from datetime import timedelta +from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant +from.const import (DOMAIN, + REPOS_API_URL, + CONF_EXCHANGE, + CONF_STOCK, + MIN_UPDATE_INTERVAL, + MAX_UPDATE_INTERVAL, + TIME_SLICES) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, hass_config: dict): + config = hass_config[DOMAIN] + coordinator = StockCorrdinator(hass, config) + hass.data[DOMAIN] = coordinator + await coordinator.async_refresh() + hass.async_create_task(discovery.async_load_platform( + hass, "sensor", DOMAIN, config, hass_config)) + + return True + +UPDATE_INTERVAL = timedelta(seconds=MIN_UPDATE_INTERVAL) + + +class StockCorrdinator(DataUpdateCoordinator): + _tencent_data_format = ['stock', 'unused', 'name', '股票代码', '当前价格', '昨收', '今开', '成交量(手)', '外盘', '内盘', + '买一', '买一量(手)', '买二', '买二量(手)', '买三', '买三量(手)', '买四', '买四量(手)', '买五', + '买五量(手)', '卖一', '卖一量(手)', '卖二', '卖二量(手)', '卖三', '卖三量(手)', '卖四', + '卖四量(手)', '卖五', '卖五量(手)', 'unknown1', 'datetime', '涨跌', '涨跌(%)', '最高', '最低', + '价格/成交量(手)/成交额', '成交量(手)', '成交额(万)', '换手率', '市盈率', 'unknown2', '最高1', '最低1', '振幅', + '流通市值', '总市值', '市净率', '涨停价', '跌停价', '量比', '委差', '均价', '市盈(动)', '市盈(静)'] + _tencent_data_patten = re.compile(r'v_([-/\.\w]*)="([\w]*)' + (r'~([-/\.\w]*)' * (len(_tencent_data_format) - 2))) + + def __init__(self, hass, config): + self._hass = hass + self._quest_url = REPOS_API_URL + if config is not None and "time_slices" in config: + self._time_slice = config["time_slices"] + else: + self._time_slice = TIME_SLICES + _LOGGER.debug(self._time_slice) + self._count = MAX_UPDATE_INTERVAL + for exchange, stocks in config["stocks"].items(): + for stock in stocks: + self._quest_url = self._quest_url + exchange + stock + "," + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + + def format_response_data(self, res_data): + res_data = res_data.replace(" ", "") + data = "".join(res_data) + data = self._tencent_data_patten.finditer(data) + result_list = {} + for item in data: + assert len(self._tencent_data_format) == len(item.groups()) + single = dict(zip(self._tencent_data_format, item.groups())) + result_list[single["stock"]] = single + return result_list + + def in_time_slice(self, ): + now_time = datetime.datetime.now() + if now_time.weekday() > 4: + return False + for single_slice in self._time_slice: + begin_time = datetime.datetime.strptime(str(datetime.datetime.now().date()) + single_slice["begin_time"], + '%Y-%m-%d%H:%M') - datetime.timedelta(minutes=2) + end_time = datetime.datetime.strptime(str(datetime.datetime.now().date()) + single_slice["end_time"], + '%Y-%m-%d%H:%M') + datetime.timedelta(minutes=2) + if begin_time <= now_time <= end_time: + return True + return False + + async def _async_update_data(self): + data = self.data + if self.in_time_slice() or self._count >= MAX_UPDATE_INTERVAL: + self._count = 0 + async with async_timeout.timeout(3): + r = await self._hass.async_add_executor_job( + requests.get, + self._quest_url + ) + if r.status_code == 200: + data = self.format_response_data(r.text) + _LOGGER.debug(f"Successful to update stock data {data}") + else: + self._count = self._count + MIN_UPDATE_INTERVAL + _LOGGER.debug(f"Now time not in slice, count = {self._count}") + return data diff --git a/custom_components/tencent_stock/config b/custom_components/tencent_stock/config new file mode 100644 index 0000000..a93bf5c --- /dev/null +++ b/custom_components/tencent_stock/config @@ -0,0 +1,9 @@ +tencent_stock: + time_slices: + - begin_time: '9:15' + end_time: '11:30' + - begin_time: '13:00' + end_time: '15:00' + stocks: + sh: ['000001','600029','600519'] + sz: ['399001','399006'] \ No newline at end of file diff --git a/custom_components/tencent_stock/const.py b/custom_components/tencent_stock/const.py new file mode 100644 index 0000000..8f46666 --- /dev/null +++ b/custom_components/tencent_stock/const.py @@ -0,0 +1,7 @@ +DOMAIN = "tencent_stock" +REPOS_API_URL = "https://qt.gtimg.cn/q=" +CONF_EXCHANGE = "exchange" +CONF_STOCK = "stock" +MIN_UPDATE_INTERVAL = 10 +MAX_UPDATE_INTERVAL = 36000 +TIME_SLICES = [{"begin_time": "9:15", "end_time": "11:30"}, {"begin_time": "13:00", "end_time": "15:00"}] diff --git a/custom_components/tencent_stock/manifest.json b/custom_components/tencent_stock/manifest.json new file mode 100644 index 0000000..0ded25f --- /dev/null +++ b/custom_components/tencent_stock/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "tencent_stock", + "name": "Tencent Stocks", + "version": "v0.0.2", + "config_flow": true, + "documentation": "https://github.com/georgezhao2010/tencent_stock", + "issue_tracker": "https://github.com/georgezhao2010/tencent_stock/issues", + "requirements": [], + "dependencies": [], + "iot_class": "cloud_polling", + "codeowners": ["@georgezhao2010"] +} \ No newline at end of file diff --git a/custom_components/tencent_stock/sensor.py b/custom_components/tencent_stock/sensor.py new file mode 100644 index 0000000..49f0d0c --- /dev/null +++ b/custom_components/tencent_stock/sensor.py @@ -0,0 +1,113 @@ +import logging +from .const import DOMAIN +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.update_coordinator import CoordinatorEntity + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + sensors = [] + coordinator = hass.data[DOMAIN] + for exchange, stocks in discovery_info["stocks"].items(): + for stock in stocks: + sensors.append(StockSensor(coordinator, exchange + stock)) + async_add_devices(sensors, True) + + +class StockSensor(CoordinatorEntity): + def __init__(self, coordinator, stock): + super().__init__(coordinator) + self._coordinator = coordinator + self._stock = stock + self._unique_id = f"{DOMAIN}.stock_{stock}" + self.entity_id = self._unique_id + self._is_index = True if (stock[0:2] == "sh" and stock[2:5] == "000") \ + or (stock[0:2] == "sz" and stock[2:5] == "399") else False + + def get_value(self, key): + data = self._coordinator.data.get(self._stock) + if data is not None: + return data.get(key) + else: + return STATE_UNKNOWN + + @staticmethod + def sign(str_val): + if float(str_val) != 0 and str_val[0:1] != "-": + return "+" + str_val + return str_val + + @property + def name(self): + return f"{self.get_value('name')}({self._stock}) [{self.sign(self.get_value('涨跌'))}" \ + f"({self.sign(self.get_value('涨跌(%)'))}%)]" + + @property + def unique_id(self): + return self._unique_id + + @property + def state(self): + return float(self.get_value('当前价格')) + + @property + def device_id(self): + return self.device_id + + @property + def device_state_attributes(self) -> dict: + ret = { + "股票代码": self.get_value("股票代码"), + "当前价格": self.get_value("当前价格"), + "昨收": self.get_value("昨收"), + "今开": self.get_value("今开"), + "成交量": self.get_value("成交量(手)"), + "外盘": self.get_value("外盘"), + "内盘": self.get_value("内盘"), + "涨跌": self.sign(self.get_value("涨跌")), + "涨跌幅": self.sign(self.get_value("涨跌(%)")) + "%", + "最高": self.get_value("最高"), + "最低": self.get_value("最低"), + "成交额": self.get_value("成交额(万)"), + "振幅": self.get_value("振幅") + "%", + } + + if not self._is_index: + ret.update({ + "卖五(" + self.get_value("卖五") + ")": self.get_value("卖五量(手)"), + "卖四(" + self.get_value("卖四") + ")": self.get_value("卖四量(手)"), + "卖三(" + self.get_value("卖三") + ")": self.get_value("卖三量(手)"), + "卖二(" + self.get_value("卖二") + ")": self.get_value("卖二量(手)"), + "卖一(" + self.get_value("卖一") + ")": self.get_value("卖一量(手)"), + "买一(" + self.get_value("买一") + ")": self.get_value("买一量(手)"), + "买二(" + self.get_value("买二") + ")": self.get_value("买二量(手)"), + "买三(" + self.get_value("买三") + ")": self.get_value("买三量(手)"), + "买四(" + self.get_value("买四") + ")": self.get_value("买四量(手)"), + "买五(" + self.get_value("买五") + ")": self.get_value("买五量(手)"), + "换手率": self.get_value("换手率") + "%", + "市盈率": self.get_value("市盈率"), + "流通市值": self.get_value("流通市值"), + "总市值": self.get_value("总市值"), + "市净率": self.get_value("市净率"), + "量比": self.get_value("量比"), + "均价": self.get_value("均价"), + "涨停价": self.get_value("涨停价"), + "跌停价": self.get_value("跌停价"), + "委差": self.get_value("委差"), + "市盈(动)": self.get_value("市盈(动)"), + "市盈(静)": self.get_value("市盈(静)") + }) + return ret + + @property + def unit_of_measurement(self): + if self._is_index: + return "点" + else: + return "元" + + @property + def icon(self): + return "hass:pulse"