├── .gitignore ├── README.md ├── custom_components └── chineseholiday │ ├── __init__.py │ ├── holiday.json │ ├── holiday.py │ ├── lunar.py │ ├── manifest.json │ ├── sensor.py │ ├── term.py │ └── test.py ├── hacs.json ├── snapshot.png └── snaptshot_1.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | holiday.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chinese Holiday 中国节假日日历插件 2 | ## 日历及节假日显示组件 3 | 可以显示中国节假日, 周年, 纪念日, 生日等(支持农历和阴历)日历插件, 同时, 支持计算某个日期和时间已经过去了N年N月N天N小时N秒. 4 | 5 | ![示例图](https://github.com/Crazysiri/chineseholiday/blob/master/snaptshot_1.png) 6 | 7 | 8 | # 安装 9 | ## HACS 安装(建议使用HACS安装和配置) 10 | 0. 手动添加自定义存储库 https://github.com/Crazysiri/chineseholiday 11 | 1. HACS 添加集成 > 搜索 ```中国节假日日历```,点击下载 12 | 13 | 配合此集成的[前端卡片](https://github.com/Crazysiri/chineseholiday_card) 14 | 15 | ## 手动安装 16 | 下载 /custom_components/chineseholida 下的所有文件 17 | 复制到 \config\custom_components 18 | 重启Home Assistant 19 | 此时应该可以在 配置 > 设备与服务 > 添加集成内搜索"chineseholiday" 或者 "中国节假日日历插件" 20 | 21 | ## 安装卡片,使用以下配置: 22 | 23 | ``` 24 | 在页面添加下面的源 25 | resources: 26 | - type: module 27 | url: /local/custom-lovelace/ch_calendar-card/ch_calendar-card.js 28 | 29 | 在添加卡片, 可以直接找到中国节假日日历的卡片, 或者 手动添加卡片, 填写下面的卡片配置代码 30 | 31 | 卡片配置 32 | - type: 'custom:ch_calendar-card' 33 | entity: sensor.holiday 34 | icons: /local/custom-lovelace/ch_calendar-card/icons/ 35 | 36 | ``` 37 | 38 | # 配置: 39 | 在configuration.yaml文件里,添加你的各种日期, 重启生效. 40 | 41 | ``` 42 | sensor: 43 | - platform: chineseholiday 44 | name: holiday 45 | solar_anniversary: 46 | '0121': 47 | - aa生日 48 | - cc生日 49 | '20200220': #这样配置会在显示的时候略有不一样,会以 bb生日(1岁) 的形式显示, 文案不包含 ‘生日’ 的统一显示为周年 xx纪念日(1周年)。 50 | - bb生日 #卡片前端aa生日(1岁) 51 | - aa和bb结婚纪念日 #卡片前端显示xx纪念日(1周年) 52 | lunar_anniversary: 53 | '0321': 54 | - aa农历生日 55 | calculate_age: #通过配置一个 'aa和bb结婚周年' '2022-10-10 10:23:10'(过去的时间),1. 自动生成未来的时间将近的周年纪念日, 2.计算这个时间已经过去了N年N月N天N小时N秒 56 | - date: '2022-10-10 10:23:10' 57 | name: 'aa和bb结婚周年' 58 | notify_script_name: 'test' #调用脚本名字 59 | notify_times: #早上9点10分调用 13:00:00 下午1点调用 60 | - "09:10:00" 61 | - "13:00:00" 62 | notify_principles: #调用脚本规则 63 | '14|7|1': #未来某个日期(下面每个date字段对应)离现在还有 14 天 7天 1天时调用脚本 64 | - date: "0101" #需要调用脚本的日期 65 | solar: False ##没填solar的默认为True 即阳历. false就是阴历, true是阳历 66 | - date: "0102" #需要调用脚本的日期 solar 不写 默认为True 即阳历 67 | '0': #0即为当天调用 68 | #*下面两种是特殊情况采用name,只有父亲节和母亲节 ,也就是填了name就不要填date,填name的只有这两种情况 69 | - name: "母亲节" 70 | - name: "父亲节" 71 | 72 | ``` 73 | 74 | 75 | # 更新 76 | 77 | 78 | + #### 2023.07.12 更新 79 | 80 | 版本 0.2.0 81 | 82 | 1.支持HACS 83 | 84 | 2.Polymer升级为Lit 85 | 86 | ############################################################################# 87 | 88 | + #### 2020.06.03 更新 89 | 90 | 1.修复当天的纪念日 不显示的问题 91 | 92 | 2.custom ui的 间距问题(应该是新版本导致的) 93 | 94 | 3.点击custom ui 可以进入控件的详情 95 | 96 | 4.控件详情的汉化 97 | 98 | 5.在最近的纪念日之后增加 接下来的纪念日,现在能同时显示更多自定义的纪念日了(目前支持两个,看后期需要增加yaml配置) 99 | 100 | ############################################################################# 101 | 102 | + #### 2020.03.30 更新 可以配置生日显示为:xx生日(10岁) 103 | 104 | ``` 105 | #只对 下面文案中包含'生日'的有效 例如 - aa生日 在显示的时候变成 aa生日(1岁) 106 | # 文案不包含 ‘生日’ 的统一显示为周年 xx纪念日(1周年)。 107 | solar_anniversary: 108 | '20200121': #该位置加入年即可 109 | - aa生日 #卡片前端aa生日(1岁) 110 | - xx纪念日 #卡片前端显示xx纪念日(1周年) 111 | ``` 112 | 113 | 其它优化:根据论坛一个哥们的代码,将休息日,节假日,工作日改成icon(https://bbs.hassbian.com/forum.php?mod=viewthread&tid=9615&page=1#pid315704) 114 | 115 | 116 | ############################################################################# 117 | 118 | + #### 2020.02.19 更新 加入custom UI 119 | 120 | ``` 121 | ui-lovelace.yaml 122 | 123 | resources: 124 | - type: module 125 | url: /local/custom-lovelace/ch_calendar-card/ch_calendar-card.js 126 | 127 | 卡片配置 128 | - type: 'custom:ch_calendar-card' 129 | entity: sensor.holiday 130 | icons: /local/custom-lovelace/ch_calendar-card/icons/ 131 | 132 | 133 | ``` 134 | 135 | ############################################################################# 136 | 137 | + #### 2020.02.08 - version:0.1.3 138 | 139 | 新增功能: 140 | 141 | 1.外部调用脚本功能,支持母亲节和父亲节设置 142 | 143 | 详见最下面的脚本配置。 144 | 145 | 2.修复由于 utc 时间导致过一天不更新时间的bug 146 | 147 | ############################################################################# 148 | 149 | 150 | + #### 2020.02.06 - version:0.1.2 151 | 152 | 优化已有功能: 153 | 154 | 1.节气通过算法计算得出某年正确的值,取代现有写死的数据(节气每年都不一样,省得每年都改) 155 | 156 | ############################################################################# 157 | 158 | 159 | + #### 2020.02.19 更新 加入custom UI 160 | 161 | ``` 162 | ui-lovelace.yaml 163 | 164 | resources: 165 | - type: module 166 | url: /local/custom-lovelace/ch_calendar-card/ch_calendar-card.js 167 | 168 | 卡片配置 169 | - type: 'custom:ch_calendar-card' 170 | entity: sensor.holiday 171 | icons: /local/custom-lovelace/ch_calendar-card/icons/ 172 | 173 | 174 | ``` 175 | 176 | ############################################################################# 177 | 178 | + #### 2020.02.04 更新 - version:0.1.1 179 | 180 | 新增调用外部脚本机制: 181 | 182 | 目前是早上9点调用脚本,以后有需求可能会改成yaml配置 183 | 184 | 可以实现的功能:10.1国庆节还有14天的时候通知 185 | 186 | 配置文件(可以配置多个规则,但目前只有一个脚本): 187 | 188 | ``` 189 | notify_script_name: 'test' //调用脚本名字 190 | notify_times: //早上9点10分调用 13:00:00 下午1点调用 191 | - "09:10:00" 192 | - "13:00:00" 193 | notify_principles: //调用脚本规则 194 | '14|7|1': //未来某个日期(下面每个date字段对应)离现在还有 14 天 7天 1天时调用脚本 195 | - date: "1001" //需要调用脚本的日期 196 | solar: False //非阳历 即阴历 197 | - date: "1002" //需要调用脚本的日期 solar 不写 默认为True 即阳历 198 | ``` 199 | 200 | ios的通知脚本可以是: 201 | 202 | 注:下面的脚本中 message 是回调的拼接好的字符串,必须是这个字段名, 203 | message的内容大概为:距离xx生日还有xx天 204 | 205 | ``` 206 | test: 207 | sequence: 208 | - service: notify.mobile_app_xxx 209 | data_template: 210 | title: "节假日提醒" 211 | message: "{{ message }}" 212 | ``` 213 | 214 | 215 | ![示例图](https://github.com/Crazysiri/chineseholiday/blob/master/snapshot.png) 216 | 217 | + #### 参考 218 | 参考: 219 | 1. https://bbs.hassbian.com/thread-9133-1-1.html 220 | 2. https://bbs.hassbian.com/forum.php?mod=viewthread&tid=1237&highlight=农历 221 | 222 | 个人感觉有些地方不太适合我的场景的,所以重构了部分代码,增加了一些功能,去掉了一些功能。 223 | 224 | + #### 感谢: 225 | 1. WalterDSU提供Readme优化建议 https://github.com/WalterDSU 226 | 227 | 228 | 229 | 去掉的功能: 230 | 231 | 1.最近的纪念日 232 | 233 | 理由: 234 | 235 | 因为年份是固定的,所以每次(每年)还得修改 236 | 237 | 而且我也没找到使用场景 238 | 239 | 240 | 241 | 增加的功能: 242 | 243 | 1.每年的纪念日(包括阳历和阴历) 244 | 245 | 理由: 246 | 247 | 可纪念生日等每年都有的日子 248 | 249 | 250 | 251 | 优化的点: 252 | 253 | 1.全部纪念日可通过configuration.yaml配置文件配置而不用改代码 254 | 255 | 2.增加sqlite数据库 存取网络获取的节假日信息,因为发现L大的只获取当月的,会有有法定节日但是不显示的小问题,所以本插件采取数据库一次性获取6个月的数据,每天更新一次 256 | 257 | 3.其它:增加节气显示,增加星期显示等 258 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/holiday.json: -------------------------------------------------------------------------------- 1 | {"update_time": "2023-01-02", "2023": {"0101": 2, "0102": 1, "0107": 1, "0108": 1, "0114": 1, "0115": 1, "0121": 1, "0122": 2, "0123": 2, "0124": 2, "0125": 1, "0126": 1, "0127": 1, "0128": 0, "0129": 0, "0204": 1, "0205": 1, "0211": 1, "0212": 1, "0218": 1, "0219": 1, "0225": 1, "0226": 1, "0304": 1, "0305": 1, "0311": 1, "0312": 1, "0318": 1, "0319": 1, "0325": 1, "0326": 1, "0401": 1, "0402": 1, "0405": 2, "0408": 1, "0409": 1, "0415": 1, "0416": 1, "0422": 1, "0423": 0, "0429": 1, "0430": 1, "0501": 2, "0502": 1, "0503": 1, "0506": 0, "0507": 1, "0513": 1, "0514": 1, "0520": 1, "0521": 1, "0527": 1, "0528": 1, "0603": 1, "0604": 1, "0610": 1, "0611": 1, "0617": 1, "0618": 1, "0622": 2, "0623": 1, "0624": 1, "0625": 0}} -------------------------------------------------------------------------------- /custom_components/chineseholiday/holiday.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding=utf-8 3 | #holiday 模块:1.从服务端获取数据并、2.存入数据库 3.从数据库读数库 4 | 5 | import requests 6 | import datetime 7 | from datetime import datetime as datetime_class 8 | from datetime import timedelta 9 | import time 10 | import json 11 | 12 | import logging 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | import sqlite3 17 | import os 18 | 19 | holiday_database_path = os.path.dirname(os.path.realpath(__file__))+'/data.db' 20 | holiday_status_json_path = os.path.dirname(os.path.realpath(__file__))+'/holiday.json'#节假日状态json 21 | class HolidayDatabase: 22 | conn = None 23 | cursor = None 24 | 25 | def __init__(self): 26 | self.connect() 27 | self.create_table('holiday',[{'key':'date','type':'varchar not null UNIQUE'},{'key':'json','type':'text'},{'key':'updateDate','type':'varchar not null'}]) 28 | 29 | def connect(self): 30 | 31 | self.conn = sqlite3.connect(holiday_database_path,check_same_thread=False) 32 | 33 | self.cursor = self.conn.cursor() 34 | 35 | pass 36 | 37 | """ 38 | name:表名 39 | keys:json [{'key','type'},{'key':'type'}] 40 | 41 | 默认创建ID字段 主键 42 | """ 43 | def create_table(self,name,keys): 44 | try: 45 | insert_keys = '' 46 | for key in keys: 47 | insert_keys += ',' + key['key'] + ' ' + key['type'] 48 | self.cursor.execute('CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE %s);' % (name,insert_keys)) 49 | 50 | self.conn.commit() 51 | # self.conn.close() 52 | 53 | return True 54 | except Exception as e: 55 | return False 56 | 57 | """ 58 | name: 表名 59 | keys:需要插入的key 数组 60 | values:需要插入的value 数组 61 | """ 62 | def insert_values(self,name,keys,values): 63 | try: 64 | flags = [] 65 | for i in range(0,len(keys)): 66 | flags.append('?') 67 | keys = ','.join(keys) 68 | flags = ','.join(flags) 69 | sql = "INSERT INTO %s (%s) VALUES (%s)" % (name,keys,flags) 70 | self.cursor.execute(sql,tuple(values)) 71 | self.conn.commit() 72 | return True 73 | except Exception as e: 74 | return False 75 | 76 | """ 77 | keys 和 values的 大小需要一样 78 | name:表名 79 | keys:需要更新的key 数组 80 | values:需要更新的value 数组 81 | condtion:条件(id = 1) 82 | """ 83 | def update_values(self,name,keys,values,condition): 84 | try: 85 | set_value_array = [] 86 | for i in range(len(keys)): 87 | key = keys[i] 88 | value = values[i] 89 | if not value: 90 | value = "''" 91 | set_value_array.append("%s = ?" % key) 92 | 93 | sql = "update %s set %s where %s;" % (name,','.join(set_value_array),condition) 94 | self.cursor.execute(sql,tuple(values)) 95 | self.conn.commit() 96 | return True 97 | except Exception as e: 98 | return False 99 | 100 | #以下两个方法为数据库的应用方法 一个是更新数据 一个是获取数据 101 | 102 | #json 是字符串的json数据 103 | def setData(self,dateString,json,updateDate): 104 | status = self.insert_values('holiday',['date','json','updateDate'],[dateString,json,updateDate]) 105 | if not status: 106 | self.update_values('holiday',['date','json','updateDate'],[dateString,json,updateDate],'date = %s' % dateString) 107 | 108 | def getData(self,condition='where 1'): 109 | keys = ['date','json','updateDate'] 110 | sql = "SELECT %s from holiday %s;" % (','.join(keys), condition) 111 | _LOGGER.debug("HolidayDatabase:"+sql) 112 | cursor = self.cursor.execute(sql) 113 | results = [] 114 | for row in cursor: 115 | result = {} 116 | for i in range(len(keys)): 117 | result[keys[i]] = row[i] 118 | results.append(result) 119 | 120 | return results 121 | 122 | class Holiday: 123 | """ 124 | public methods 125 | 126 | is_holiday 是否是节日 127 | is_holiday_today 今天是否是节假日 128 | getHoliday 获取节假日 129 | 130 | """ 131 | 132 | database = None 133 | session = None 134 | """docstring for Holiday.""" 135 | def __init__(self): 136 | self._holiday_json = {} 137 | self.database = HolidayDatabase() 138 | session = requests.session() 139 | requests.adapters.DEFAULT_RETRIES = 5 # 增加重连次数 140 | session.keep_alive = False 141 | # session.proxies = {"https": "47.100.104.247:8080", "http": "36.248.10.47:8080", } 142 | self.session = session 143 | 144 | self.get_holidays_from_disk() #从本地获取缓存的 节假日数据 145 | 146 | """ 147 | 通过该api可以计算某一天, 148 | n = 1 就是加一天,即明天 149 | n = -1 就是减一天,即昨天 150 | 依此类推 151 | """ 152 | @classmethod 153 | def day(cls,n): 154 | return datetime_class.utcnow() + timedelta(hours=8) + timedelta(hours=n * 24) 155 | 156 | @classmethod 157 | def today(cls): 158 | return Holiday.day(0) 159 | 160 | @classmethod 161 | def tomorrow(cls): 162 | return Holiday.day(1) 163 | 164 | #根据节假日 计算最近一次假日的放假策略 165 | #参数 在 30 - 45 天之内的显示 166 | def nearest_holiday_info(self,min_days=30,max_days=45): 167 | today = Holiday.today() 168 | for y in self._holiday_json: 169 | if y == 'update_time': 170 | continue 171 | dates = self._holiday_json[y] # {"0101":1,"0102":2} 172 | for m in dates: 173 | t = dates[m] 174 | if t == 2: #找到假日 175 | d = '{}-{}-{}'.format(y,m[0:2],m[2:]) 176 | date = datetime_class.strptime(d,'%Y-%m-%d') 177 | start = date 178 | end = date 179 | before_start_workdays = [] #串休日 180 | after_end_workdays = [] #串休日 181 | 182 | #在距离 节日 30 - 40天 之间的显示 找到最近的一个 直接return 183 | if (date - today).days >= min_days and (date - today).days <= max_days: 184 | #找到前后连续的节假日 185 | while self.is_holiday_status(start) != 0: 186 | start = start - timedelta(days=1) 187 | while self.is_holiday_status(end) != 0: 188 | end = end + timedelta(days=1) 189 | #因为这里会多计算一次 所以得到的是前一天和后一天 190 | last_weekend = start 191 | next_weekend = end 192 | while self.is_holiday_status(last_weekend) == 0: 193 | invert = False 194 | if last_weekend.weekday() == 5 or last_weekend.weekday() == 6: 195 | invert = True 196 | before_start_workdays.append({'date':last_weekend,'invert':invert}) 197 | last_weekend = last_weekend - timedelta(days=1) 198 | while self.is_holiday_status(next_weekend) == 0: 199 | invert = False 200 | if next_weekend.weekday() == 5 or next_weekend.weekday() == 6: 201 | invert = True 202 | after_end_workdays.append({'date':next_weekend,'invert':invert}) 203 | next_weekend = next_weekend + timedelta(days=1) 204 | 205 | start = start + timedelta(days=1) 206 | end = end - timedelta(days=1) 207 | before = "" 208 | after = "" 209 | before_start_workdays.reverse() 210 | for item in before_start_workdays: 211 | date = item['date'] 212 | invert = item['invert'] 213 | before += " {}/{}".format(date.month,date.day) 214 | if invert: 215 | before += "(串休日,周{})".format(date.weekday()+1) 216 | for item in after_end_workdays: 217 | date = item['date'] 218 | invert = item['invert'] 219 | after += " {}/{}".format(date.month,date.day) 220 | if invert: 221 | after += "(串休日,周{})".format(date.weekday()+1) 222 | info = "{}(周{})-{} 放假 共{}天\n据上一次休息{}天 {} \n据下一次休息{}天 {}".format(start.strftime('%m/%d'),start.weekday()+1,end.strftime('%m/%d'),(end-start).days+1,(start-last_weekend).days-1,before,(next_weekend-end).days-1,after) 223 | _LOGGER.debug("Holiday:nearest_holiday_info:"+info) 224 | return info 225 | return '' 226 | 227 | def get_holidays_from_disk(self): 228 | try: 229 | with open(holiday_status_json_path,'r') as f: 230 | self._holiday_json = json.load(f) 231 | except Exception as e: 232 | _LOGGER.debug("Holiday:get_holidays_from_disk:"+str(e)) 233 | 234 | def get_holidays_from_server(self,days=15): 235 | """ 236 | 判断是否节假日, api 来自百度 apistore: [url]https://www.kancloud.cn/xiaoggvip/holiday_free/1606802[/url] 237 | :param day: 日期, 格式为 '20160404' 238 | :return: bool 239 | 另一个api 240 | holiday_api = 'http://timor.tech/api/holiday/info/{0}'.format(day) 241 | 242 | """ 243 | if not os.path.exists(os.path.dirname(holiday_status_json_path)): 244 | _LOGGER.debug("Holiday:get_holidays_from_server:not exists") 245 | os.mkdir(os.path.dirname(holiday_status_json_path)) 246 | data = {} 247 | date = '2020-01-01' #这个是默认时间,数据库读不到 取当天肯定会执行更新逻辑 248 | #从服务器拿数据 249 | try: 250 | with open(holiday_status_json_path,'r') as f: 251 | data = json.load(f) 252 | except Exception as e: 253 | _LOGGER.debug("Holiday:get_holidays_from_server:read holiday error!"+str(e)) 254 | 255 | if data and 'update_time' in data: 256 | date = data['update_time'] 257 | # 计算今天和未来一个日期的天数差值 258 | today = Holiday.today() 259 | today_str = today.strftime('%Y-%m-%d') 260 | last_update = datetime_class.strptime(date,'%Y-%m-%d') 261 | interval = today - last_update 262 | _LOGGER.debug("Holiday:get_holidays_from_server:" + str(interval.days) + "," + str(days) ) 263 | if interval.days > days or days == 0: 264 | data = {} 265 | data['update_time'] = today_str 266 | for i in range(today.month,today.month + 6): 267 | year = today.year 268 | month = i 269 | #这里只支持1间隔不到1年的 270 | if month > 12: 271 | year = today.year + 1 272 | month = month - 12 273 | if str(year) not in data: 274 | data[str(year)] = {} 275 | year_dict = data[str(year)] 276 | try: 277 | result = self.get_holidays_from_server_one_month(year,month,year_dict) 278 | time.sleep(1) 279 | except Exception as e: 280 | _LOGGER.debug("Holiday:get_holidays_from_server:year:" + str(year) + ",month:"+ str(month) + ",dict:" + str(year_dict) + ",error:"+str(e)) 281 | 282 | 283 | with open(holiday_status_json_path,'w') as f: 284 | json.dump(data,f) 285 | 286 | self._holiday_json = data 287 | 288 | else: 289 | _LOGGER.debug("Holiday:get_holidays_from_server:not need update!") 290 | 291 | 292 | def get_holidays_from_server_one_month(self,year,month,year_dict): 293 | #year_dict 是为了方便进来传值的,否则这里返回了,外面还得遍历一遍 294 | # https://blog.bitefu.net/post/31.html 295 | d = "{}{:0>2d}".format(year,month) 296 | api = 'http://tool.bitefu.net/jiari/' 297 | params = {'d': d ,'info':1} 298 | rep = requests.get(api, params) 299 | if rep.status_code != 200 or d not in rep.json(): #请求失败或者没有数据都不能存 300 | _LOGGER.debug("Holiday:get_holidays_from_server_one_month:ad request or no data!") 301 | return 302 | 303 | data = {} 304 | result = rep.json() 305 | for key in result[d]: 306 | t = int(result[d][key]['type']) 307 | w = int(result[d][key]['week2']) 308 | #节假日 1 2 或者 本应该是周六日的确实工作日的要存 309 | if (t == 1 or t == 2) or ((w == 6 or w == 7) and t == 0): 310 | year_dict[key] = result[d][key]['type'] 311 | 312 | def is_holiday_status(self,date): 313 | self.get_holidays_from_server() 314 | _LOGGER.debug("Holiday:is_holiday_status:year" + str(date.year)) 315 | 316 | y_str = str(date.year) 317 | h_dict = {} 318 | if y_str in self._holiday_json: 319 | h_dict = self._holiday_json[y_str] 320 | 321 | m = "{:0>2d}".format(date.month) 322 | d = "{:0>2d}".format(date.day) 323 | key = '%s%s' % (m,d) 324 | status = 0 325 | if key in h_dict: 326 | status = h_dict[key] 327 | else: 328 | w = date.weekday() 329 | if w > 4: 330 | status = 1 331 | else: 332 | status = 0 333 | return status 334 | 335 | 336 | def is_holiday(self,date): 337 | d = {0:'工作日',1:'休息日',2:'节假日'} 338 | status = self.is_holiday_status(date) 339 | return d[status] 340 | 341 | def is_holiday_today(self): 342 | """ 343 | 判断今天是否时节假日 344 | :return: bool 345 | """ 346 | today = Holiday.today() 347 | return self.is_holiday(today) 348 | 349 | def is_holiday_tomorrow(self): 350 | """ 351 | 判断明天是否时节假日 352 | :return: bool 353 | """ 354 | day = Holiday.tomorrow() 355 | return self.is_holiday(day) 356 | 357 | #获取节日数据 358 | def holiday_handle(self,list): 359 | subkey = {'date': '阳历日期','nlyf': '农历月份','nl': '农历','w1': '天气','jq': '节气', 'hmax': '最高温度', 'hmin': '最低温度', 'hgl': '降水概率', 'fe': '阴历节日', 'yl': '阳历节日', 'wk': '星期', 'time': '发布时间'} 360 | results = {} 361 | for dict in list: 362 | subdict = {value: dict[key] for key, value in subkey.items()} 363 | if subdict['阴历节日'] != '' or subdict['阳历节日']!= '': 364 | year = int(subdict['阳历日期'][0:4],base=10); 365 | month = (int(subdict['阳历日期'][4:6],base=10) if int(subdict['阳历日期'][4:6],base=10) >=10 else int(subdict['阳历日期'][5:6],base=10)) 366 | day = (int(subdict['阳历日期'][6:8],base=10) if int(subdict['阳历日期'][6:8],base=10) >=10 else int(subdict['阳历日期'][7:8],base=10)) 367 | hlday = subdict['阴历节日']+subdict['阳历节日']; 368 | ##print(datetime.date(year=year, month=month, day=day).str()+"-"+hlday) 369 | results[datetime.date(year=year, month=month, day=day)] = hlday 370 | return results 371 | 372 | #days 每几天更新数据 days = 0 则每次更新 373 | def getHoliday(self,days = 1): 374 | last_date = '2020-01-01' #这个是默认时间,数据库读不到 取当天肯定会执行更新逻辑 375 | try: 376 | last_date = self.database.getData('LIMIT 1')[0]['updateDate'] 377 | except Exception as e: 378 | _LOGGER.debug("Holiday:getHoliday:database get last object!"+str(e)) 379 | 380 | # 计算今天和未来一个日期的天数差值 381 | now_str = datetime_class.now().strftime('%Y-%m-%d') 382 | today = datetime_class.strptime(now_str, "%Y-%m-%d") 383 | last_update = datetime_class.strptime(last_date,'%Y-%m-%d') 384 | interval = today - last_update 385 | #从服务器拿数据 386 | if interval.days > days or days == 0: 387 | list = self.getholidayForNMonths() 388 | try: 389 | for subList in list: 390 | #一次获取 是一个subList 391 | for dict in subList: 392 | # print(dict) 393 | self.database.setData(dict['date'],json.dumps(dict),today.strftime('%Y-%m-%d')) 394 | 395 | except Exception as e: 396 | _LOGGER.debug("Holiday:getholidayForNMonths:"+str(e)) 397 | 398 | 399 | #从本地数据库拿数据 400 | results = self.database.getData() 401 | list = [] 402 | for result in results: 403 | r = json.loads(result['json']) 404 | list.append(r) 405 | return self.holiday_handle(list) 406 | 407 | #n 获取从该月开始的往后n个月的数据 ,这里n要小于12 因为year += 1 408 | def getholidayForNMonths(self,n=6): 409 | # return self.getonline40dholiday('101010100',datetime.date.today().strftime('%Y%m%d')) 410 | year_str = time.strftime("%Y", time.localtime()) 411 | month_str = time.strftime("%m", time.localtime()) 412 | year = int(year_str) 413 | month = int(month_str) 414 | list = [] 415 | for i in range(0,n): 416 | m = month 417 | y = year 418 | if m + i > 12: 419 | y += 1 420 | m = m + i - 12 421 | else: 422 | m = month + i 423 | # print('y:'+str(y) + ' m:'+str(m)) 424 | results = self.getonline40dholiday('101010100',str(y),"{:0>2d}".format(m)) 425 | sub_list = [] 426 | for r in results: 427 | #有阴历或阳历节日的 428 | if r['fe'] != '' or r['yl'] != '': 429 | sub_list.append(r) 430 | list.append(sub_list) 431 | return list 432 | 433 | #year month 需要字符串 '2010' '01' 434 | def getonline40dholiday(self,citycode,year,month): 435 | url = "http://d1.weather.com.cn/calendar_new/"+year+"/"+citycode+"_"+year+month+".html"; 436 | # print(url) 437 | headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", 438 | "Referer": "http://www.weather.com.cn/weather40d/"+citycode+".shtml"} 439 | res = self.session.get(url, headers=headers) 440 | holiday_list = [] 441 | try: 442 | json_str = res.content.decode(encoding='utf-8')[11:] 443 | holiday_list = json.loads(json_str) 444 | except Exception as e: 445 | _LOGGER.error(f"getonline40dholiday,error:{e}") 446 | _LOGGER.error(f"getonline40dholiday,result:{res.text}") 447 | return holiday_list 448 | 449 | def main(): 450 | # Holiday().nearest_holiday_info(14,45) 451 | # print(Holiday().is_holiday_today()) 452 | # print(Holiday.day(-1)) 453 | pass 454 | 455 | if __name__ == '__main__': 456 | main() 457 | """ 458 | {"update_time": "2020-06-09", "2020": {"0606": 1, "0607": 1, "0613": 1, "0614": 1, "0620": 1, "0621": 1, "0625": 2, "0626": 1, "0627": 1, "0628": 0, "0704": 1, "0705": 1, "0711": 1, "0712": 1, "0718": 1, "0719": 1, "0725": 1, "0726": 1, "0801": 1, "0802": 1, "0808": 1, "0809": 1, "0815": 1, "0816": 1, "0822": 1, "0823": 1, "0829": 1, "0830": 1, "0905": 1, "0906": 1, "0912": 1, "0913": 1, "0919": 1, "0920": 1, "0926": 1, "0927": 0, "1001": 2, "1002": 2, "1003": 2, "1004": 1, "1005": 1, "1006": 1, "1007": 1, "1008": 1, "1010": 0, "1011": 1, "1017": 1, "1018": 1, "1024": 1, "1025": 1, "1031": 1, "1101": 1, "1107": 1, "1108": 1, "1114": 1, "1115": 1, "1121": 1, "1122": 1, "1128": 1, "1129": 1}} 459 | """ 460 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/lunar.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | # coding=utf-8 3 | # Created: 20/07/2012 4 | # Copyright: http://www.cnblogs.com/txw1958/ 5 | ''' 6 | A Chinese Calendar Library in Python 7 | ''' 8 | 9 | 10 | import os, io, sys, re, time, datetime, base64 11 | from datetime import timedelta 12 | path = os.path.dirname(os.path.realpath(__file__)) 13 | sys.path.append(path) 14 | import term 15 | from term import jieqi 16 | 17 | __version__ = "$Rev: 123 $" 18 | __all__ = ['LunarDate'] 19 | 20 | 21 | solar_year = 1900 22 | solar_month = 1 23 | solar_day = 31 24 | solar_weekday = 0 25 | 26 | lunar_year = 0 27 | lunar_month = 0 28 | lunar_day = 0 29 | lunar_isLeapMonth = False 30 | # 0620#aa 生日# #bb 纪念日# 31 | # '([\w+?\#?\(?\)?\d+\s?·?]*)' | solar_Fstv | #aa 生日# #bb 纪念日# 32 | # '([\w+?\#?\s?]*)' | festival_handle,weekday_Fstv | #aa 生日# #bb 纪念日# 33 | # '([\w+?\#?]*)' | solar_Term | #aa 34 | def festival_handle(params,month,day): 35 | month_str = "{:0>2d}".format(month) 36 | day_str = "{:0>2d}".format(day) 37 | # pattern = "(" + month_str + day_str + ")([\w+?\#?\s?]*)" 38 | # pattern = "(%s%s)#([\s\S]+?)#"%(month_str,day_str) 39 | # pattern = '#([\s\S]+?)#' 40 | md = month_str+day_str 41 | for key,value in params.items(): 42 | if md in key[-4:]: 43 | return ','.join(params[key]) 44 | return None 45 | 46 | class LunarDate(object): 47 | _startDate = datetime.date(1900, 1, 31) 48 | 49 | 50 | @staticmethod 51 | def fromSolarDate(year, month, day): 52 | #通过公历年月日生成农历 53 | #@return LunarDate 54 | solarDate = datetime.date(year, month, day) 55 | offset = (solarDate - LunarDate._startDate).days 56 | return LunarDate._fromOffset(offset) 57 | 58 | @staticmethod 59 | def _enumMonth(yearInfo): 60 | months = [(i, 0) for i in range(1, 13)] 61 | leapMonth = yearInfo % 16 62 | if leapMonth == 0: 63 | pass 64 | elif leapMonth <= 12: 65 | months.insert(leapMonth, (leapMonth, 1)) 66 | else: 67 | raise ValueError("yearInfo %r mod 16 should in [0, 12]" % yearInfo) 68 | 69 | for month, isLeapMonth in months: 70 | if isLeapMonth: 71 | days = (yearInfo >> 16) % 2 + 29 72 | else: 73 | days = (yearInfo >> (16 - month)) % 2 + 29 74 | yield month, days, isLeapMonth 75 | 76 | @classmethod 77 | def _fromOffset(cls, offset): 78 | def _calcMonthDay(yearInfo, offset): 79 | for month, days, isLeapMonth in cls._enumMonth(yearInfo): 80 | if offset < days: 81 | break 82 | offset -= days 83 | return (month, offset + 1, isLeapMonth) 84 | 85 | offset = int(offset) 86 | 87 | for idx, yearDay in enumerate(Info.yearDays()): 88 | if offset < yearDay: 89 | break 90 | offset -= yearDay 91 | year = 1900 + idx 92 | 93 | yearInfo = Info.yearInfos[idx] 94 | print('yearInfo') 95 | print(year) 96 | print(idx) 97 | month, day, isLeapMonth = _calcMonthDay(yearInfo, offset) 98 | return LunarDate(year, month, day, isLeapMonth) 99 | 100 | @classmethod 101 | def today(cls): 102 | res = datetime.date.today() 103 | return cls.fromSolarDate(res.year, res.month, res.day) 104 | 105 | 106 | def __init__(self, year, month, day, isLeapMonth=False): 107 | global lunar_year 108 | global lunar_month 109 | global lunar_day 110 | global lunar_isLeapMonth 111 | 112 | lunar_year = int(year) 113 | lunar_month = int(month) 114 | lunar_day = int(day) 115 | lunar_isLeapMonth = bool(isLeapMonth) 116 | 117 | self.year = year 118 | self.month = month 119 | self.day = day 120 | self.isLeapMonth = bool(isLeapMonth) 121 | 122 | def __str__(self): 123 | return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.isLeapMonth) 124 | 125 | __repr__ = __str__ 126 | 127 | def toSolarDate(self): 128 | #输出公历 129 | #return datetime 130 | def _calcDays(yearInfo, month, day, isLeapMonth): 131 | isLeapMonth = int(isLeapMonth) 132 | res = 0 133 | ok = False 134 | for _month, _days, _isLeapMonth in self._enumMonth(yearInfo): 135 | if (_month, _isLeapMonth) == (month, isLeapMonth): 136 | if 1 <= day <= _days: 137 | res += day - 1 138 | return res 139 | else: 140 | #这里特殊处理一下,如果是12-30 说明是大年三十,但有些是没有三十的,所以往前推一天 141 | if month == 12 and day == 30: 142 | return _calcDays(yearInfo,12,29,isLeapMonth) 143 | raise ValueError("day out of range") 144 | res += _days 145 | 146 | raise ValueError("month out of range") 147 | 148 | offset = 0 149 | if self.year < 1900 or self.year >= 2050: 150 | raise ValueError('year out of range [1900, 2050)') 151 | yearIdx = self.year - 1900 152 | for i in range(yearIdx): 153 | offset += Info.yearDays()[i] 154 | 155 | offset += _calcDays(Info.yearInfos[yearIdx], self.month, self.day, self.isLeapMonth) 156 | return self._startDate + datetime.timedelta(days=offset) 157 | 158 | def __sub__(self, other): 159 | if isinstance(other, LunarDate): 160 | return self.toSolarDate() - other.toSolarDate() 161 | elif isinstance(other, datetime.date): 162 | return self.toSolarDate() - other 163 | elif isinstance(other, datetime.timedelta): 164 | res = self.toSolarDate() - other 165 | return LunarDate.fromSolarDate(res.year, res.month, res.day) 166 | raise TypeError 167 | 168 | def __rsub__(self, other): 169 | if isinstance(other, datetime.date): 170 | return other - self.toSolarDate() 171 | 172 | def __add__(self, other): 173 | if isinstance(other, datetime.timedelta): 174 | res = self.toSolarDate() + other 175 | return LunarDate.fromSolarDate(res.year, res.month, res.day) 176 | raise TypeError 177 | 178 | def __radd__(self, other): 179 | return self + other 180 | 181 | def __lt__(self, other): 182 | return self - other < datetime.timedelta(0) 183 | 184 | def __le__(self, other): 185 | return self - other <= datetime.timedelta(0) 186 | 187 | 188 | class ChineseWord(): 189 | def weekday_str(tm): 190 | a = '星期一 星期二 星期三 星期四 星期五 星期六 星期日'.split() 191 | return a[tm] 192 | def week_num_str(num): 193 | a = '零 一 二 三 四 五 六 七 八 九 十 十一 十二 十三 十四 十五\ 194 | 十六 十七 十八 十九 二十 二十一 二十二 二十三 二十四\ 195 | 二十五 二十六 二十七 二十八 二十九 三十 三十一 三十二\ 196 | 三十三 三十四 三十五 三十六 三十七 三十八 三十九 四十\ 197 | 四十一 四十二 四十三 四十四 四十五 四十六 四十七 四十八\ 198 | 四十九 五十 五十一 五十二 五十三 五十四 五十五 五十六\ 199 | 五十七 五十八 五十九 六十 六十一 六十二'.split() 200 | return a[num] 201 | 202 | def solarTerm(year, month, day): 203 | a = '小寒 大寒 立春 雨水 惊蛰 春分\ 204 | 清明 谷雨 立夏 小满 芒种 夏至\ 205 | 小暑 大暑 立秋 处暑 白露 秋分\ 206 | 寒露 霜降 立冬 小雪 大雪 冬至'.split() 207 | return 208 | 209 | def day_lunar(ld): 210 | a = '初一 初二 初三 初四 初五 初六 初七 初八 初九 初十\ 211 | 十一 十二 十三 十四 十五 十六 十七 十八 十九 廿十\ 212 | 廿一 廿二 廿三 廿四 廿五 廿六 廿七 廿八 廿九 三十'.split() 213 | return a[ld - 1] 214 | 215 | def month_lunar(le, lm): 216 | a = '正月 二月 三月 四月 五月 六月 七月 八月 九月 十月 十一月 十二月'.split() 217 | if le: 218 | return "闰" + a[lm - 1] 219 | else: 220 | return a[lm - 1] 221 | 222 | def year_lunar(ly): 223 | y = ly 224 | tg = '甲 乙 丙 丁 戊 己 庚 辛 壬 癸'.split() 225 | dz = '子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥'.split() 226 | sx = '鼠 牛 虎 兔 龙 蛇 马 羊 猴 鸡 狗 猪'.split() 227 | return tg[(y - 4) % 10] + dz[(y - 4) % 12] + sx[(y - 4) % 12] + '年' 228 | 229 | class Festival(): 230 | 231 | _solar_festival = {'0101': ['元旦节'], '0202': ['世界湿地日'], '0210': ['国际气象节'], '0214': ['情人节'], '0301': ['国际海豹日'], '0303': ['全国爱耳日'], '0305': ['学雷锋纪念日'], '0308': ['妇女节'], '0312': ['植树节', '孙中山逝世纪念日'], '0314': ['国际警察日'], '0315': ['消费者权益日'], '0317': ['中国国医节', '国际航海日'], '0321': ['世界森林日', '消除种族歧视国际日', '世界儿歌日'], '0322': ['世界水日'], '0323': ['世界气象日'], '0324': ['世界防治结核病日'], '0325': ['全国中小学生安全教育日'], '0330': ['巴勒斯坦国土日'], '0401': ['愚人节', '全国爱国卫生运动月(四月)', '税收宣传月(四月)'], '0407': ['世界卫生日'], '0422': ['世界地球日'], '0423': ['世界图书和版权日'], '0424': ['亚非新闻工作者日'], '0501': ['劳动节'], '0504': ['青年节'], '0505': ['碘缺乏病防治日'], '0508': ['世界红十字日'], '0512': ['国际护士节'], '0515': ['国际家庭日'], '0517': ['国际电信日'], '0518': ['国际博物馆日'], '0520': ['全国学生营养日'], '0523': ['国际牛奶日'], '0531': ['世界无烟日'], '0601': ['国际儿童节'], '0605': ['世界环境保护日'], '0606': ['全国爱眼日'], '0617': ['防治荒漠化和干旱日'], '0623': ['国际奥林匹克日'], '0625': ['全国土地日'], '0626': ['国际禁毒日'], '0701': ['中国共·产党诞辰', '香港回归纪念日', '世界建筑日'], '0702': ['国际体育记者日'], '0707': ['抗日战争纪念日'], '0711': ['世界人口日'], '0730': ['非洲妇女日'], '0801': ['建军节'], '0808': ['中国男子节(爸爸节)'], '0815': ['抗日战争胜利纪念'], '0908': ['国际扫盲日', '国际新闻工作者日'], '0909': ['毛·泽东逝世纪念'], '0910': ['中国教师节'], '0914': ['世界清洁地球日'], '0916': ['国际臭氧层保护日'], '0918': ['九·一八事变纪念日'], '0920': ['国际爱牙日'], '0927': ['世界旅游日'], '0928': ['孔子诞辰'], '1001': ['国庆节', '世界音乐日', '国际老人节'], '1002': ['国庆节假日', '国际和平与民主自由斗争日'], '1003': ['国庆节假日'], '1004': ['世界动物日'], '1006': ['老人节'], '1008': ['全国高血压日', '世界视觉日'], '1009': ['世界邮政日', '万国邮联日'], '1010': ['辛亥革命纪念日', '世界精神卫生日'], '1013': ['世界保健日', '国际教师节'], '1014': ['世界标准日'], '1015': ['国际盲人节(白手杖节)'], '1016': ['世界粮食日'], '1017': ['世界消除贫困日'], '1022': ['世界传统医药日'], '1024': ['联合国日'], '1031': ['世界勤俭日'], '1107': ['十月社会主义革命纪念日'], '1108': ['中国记者日'], '1109': ['全国消防安全宣传教育日'], '1110': ['世界青年节'], '1111': ['光棍节', '国际科学与和平周(本日所属的一周)'], '1112': ['孙中山诞辰纪念日'], '1114': ['世界糖尿病日'], '1116': ['国际宽容日'], '1117': ['国际大学生节', '世界学生节'], '1120': ['彝族年'], '1121': ['彝族年', '世界问候日', '世界电视日'], '1122': ['彝族年'], '1129': ['国际声援巴勒斯坦人民国际日'], '1201': ['世界艾滋病日'], '1203': ['世界残疾人日'], '1205': ['国际经济和社会发展志愿人员日'], '1208': ['国际儿童电视日'], '1209': ['世界足球日'], '1210': ['世界人权日'], '1212': ['西安事变纪念日'], '1213': ['南京大屠杀(1937年)纪念日'], '1220': ['澳门回归纪念'], '1221': ['国际篮球日'], '1224': ['平安夜'], '1225': ['圣诞节'], '1226': ['毛·泽东诞辰纪念日']} 232 | 233 | _lunar_festival = {'0101': ['春节'], '0115': ['元宵节'], '0202': ['春龙节'], '0505': ['端午节'], '0707': ['七夕情人节'], '0715': ['中元节'], '0815': ['中秋节'], '0909': ['重阳节'], '1208': ['腊八节'], '1223': ['小年'], '1229': ['除夕']} 234 | 235 | _is_create_weekday = False #是否创建了某月第几个周末的节日 236 | _weekday_festival = {'0150': ['世界防治麻风病日'], '0520': ['母亲节'], '0530': ['全国助残日'], '0630': ['父亲节'], '0730': ['被奴役国家周'], '0932': ['国际和平日'], '0940': ['国际聋人节', '世界儿童日'], '0950': ['世界海事日'], '1011': ['国际住房日'], '1013': ['国际减轻自然灾害日(减灾日)'], '1144': ['感恩节']} 237 | 238 | _weekday_festival_reserse = {} #这个字典用来记录 用节日名字做为key 实际日期做为value的 数据 239 | 240 | _solar_term = {} 241 | 242 | # _winter_solstice = {} 243 | # 244 | # _summer_solstice = {} 245 | 246 | @classmethod 247 | def lunar_Fstv(cls,lunar_month, lunar_day): 248 | #农历节日 249 | return festival_handle(Festival._lunar_festival,lunar_month,lunar_day) 250 | 251 | #国历节日 252 | @classmethod 253 | def solar_Fstv(cls,solar_month, solar_day): 254 | return festival_handle(Festival._solar_festival,solar_month,solar_day) 255 | 256 | @classmethod 257 | def _create_weekday_festival(cls): 258 | 259 | if cls._is_create_weekday: 260 | return 261 | cls._is_create_weekday = True 262 | 263 | year = datetime.date.today().year 264 | 265 | for key,value in cls._weekday_festival.items(): 266 | month = int(key[:2]) 267 | w = int(key[3:]) 268 | n = int(key[2]) 269 | first = datetime.date(year, month, 1).weekday() + 1#该月的第一天星期几 270 | day = 1 + 7 - first + w + (n - 1) * 7 271 | if day > 30: # 如果有计算错误 此处30需要改成当月天数 272 | day = day - 7 #此处只减一个7,因为上面数据最大为5,而实际上每月最少有4个星期n,所以减1即可 273 | month_str = "{:0>2d}".format(month) 274 | day_str = "{:0>2d}".format(day) 275 | date_str = month_str + day_str 276 | for k in value: 277 | cls._weekday_festival_reserse[k] = date_str 278 | cls._solar_festival[date_str] = value 279 | 280 | @classmethod 281 | def _create_terms(cls): 282 | #计算节气 并且 把清明节放入节日中,获取夏至和冬至 283 | if not Festival._solar_term: 284 | terms = jieqi().creat_year_jieqi(datetime.date.today().year) 285 | for item in terms: 286 | comps = item['time'].split('-') 287 | if item['name'] == '清明': 288 | Festival._solar_festival[comps[1]+comps[2]] = ['清明节'] 289 | Festival._solar_term[comps[1]+comps[2]] = [item['name']] 290 | #24节气 291 | @classmethod 292 | def solar_Term(cls,solar_month, solar_day): 293 | return festival_handle(Festival._solar_term,solar_month,solar_day) 294 | 295 | 296 | class Info(): 297 | yearInfos = [ 298 | # /* encoding: 299 | # b bbbbbbbbbbbb bbbb 300 | # bit# 1 111111000000 0000 301 | # 6 543210987654 3210 302 | # . ............ .... 303 | # month# 000000000111 304 | # M 123456789012 L 305 | # 306 | # b_j = 1 for long month, b_j = 0 for short month 307 | # L is the leap month of the year if 1<=L<=12; NO leap month if L = 0. 308 | # The leap month (if exists) is long one iff M = 1. 309 | # */ 310 | 0x04bd8, # /* 1900 */ 311 | 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950,# /* 1905 */ 312 | 0x16554, 0x056a0, 0x09ad0, 0x055d2, 0x04ae0,# /* 1910 */ 313 | 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540,# /* 1915 */ 314 | 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, 0x04970,# /* 1920 */ 315 | 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54,# /* 1925 */ 316 | 0x02b60, 0x09570, 0x052f2, 0x04970, 0x06566,# /* 1930 */ 317 | 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60,# /* 1935 */ 318 | 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, 0x0d4a0,# /* 1940 */ 319 | 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0,# /* 1945 */ 320 | 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, 0x06ca0,# /* 1950 */ 321 | 0x0b550, 0x15355, 0x04da0, 0x0a5d0, 0x14573,# /* 1955 */ 322 | 0x052d0, 0x0a9a8, 0x0e950, 0x06aa0, 0x0aea6,# /* 1960 */ 323 | 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260,# /* 1965 */ 324 | 0x0f263, 0x0d950, 0x05b57, 0x056a0, 0x096d0,# /* 1970 */ 325 | 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250,# /* 1975 */ 326 | 0x0d558, 0x0b540, 0x0b5a0, 0x195a6, 0x095b0,# /* 1980 */ 327 | 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50,# /* 1985 */ 328 | 0x06d40, 0x0af46, 0x0ab60, 0x09570, 0x04af5,# /* 1990 */ 329 | 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58,# /* 1995 */ 330 | 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, 0x0c960,# /* 2000 */ 331 | 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0,# /* 2005 */ 332 | 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, 0x0a950,# /* 2010 */ 333 | 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0,# /* 2015 */ 334 | 0x0a5b0, 0x15176, 0x052b0, 0x0a930, 0x07954,# /* 2020 */ 335 | 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6,# /* 2025 */ 336 | 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, 0x05aa0,# /* 2030 */ 337 | 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0,# /* 2035 */ 338 | 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, 0x0b5a0,# /* 2040 */ 339 | 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0,# /* 2045 */ 340 | 0x0aa50, 0x1b255, 0x06d20, 0x0ada0 # /* 2049 */ 341 | ] 342 | 343 | def yearInfo2yearDay(yearInfo): 344 | yearInfo = int(yearInfo) 345 | 346 | res = 29 * 12 347 | 348 | leap = False 349 | if yearInfo % 16 != 0: 350 | leap = True 351 | res += 29 352 | 353 | yearInfo //= 16 354 | 355 | for i in range(12 + leap): 356 | if yearInfo % 2 == 1: 357 | res += 1 358 | yearInfo //= 2 359 | return res 360 | 361 | def yearDays(): 362 | yearDays = [Info.yearInfo2yearDay(x) for x in Info.yearInfos] 363 | return yearDays 364 | 365 | def day2LunarDate(offset): 366 | offset = int(offset) 367 | res = LunarDate() 368 | 369 | for idx, yearDay in enumerate(yearDays()): 370 | if offset < yearDay: 371 | break 372 | offset -= yearDay 373 | res.year = 1900 + idx 374 | 375 | class SolarDate(): 376 | 377 | def __init__(self): 378 | global solar_year 379 | global solar_month 380 | global solar_day 381 | global solar_weekday 382 | 383 | t = datetime.datetime.utcnow() + timedelta(hours=8) 384 | #time.localtime() 385 | solar_year = t.year 386 | solar_month = t.month 387 | solar_day = t.day 388 | solar_weekday = t.weekday() 389 | self.year = solar_year 390 | self.month = solar_month 391 | self.day = solar_day 392 | self.weekday = solar_weekday 393 | 394 | def __str__(self): 395 | return 'LunarDate(%d, %d, %d, %d)' % (self.year, self.month, self.day, self.isLeapMonth) 396 | 397 | 398 | class CalendarToday: 399 | 400 | _solar = None 401 | _lunar = None 402 | def __init__(self): 403 | _solar = SolarDate() 404 | _lunar = LunarDate.fromSolarDate(solar_year,solar_month,solar_day) 405 | 406 | def _solar_festival(self): 407 | #公历节日 408 | s = Festival.solar_Fstv(solar_month, solar_day) 409 | if s: 410 | return s 411 | return '' 412 | 413 | def _lunar_festival(self): 414 | #农历节日 415 | s = Festival.lunar_Fstv(lunar_month, lunar_day) 416 | if s: 417 | return s 418 | return '' 419 | 420 | 421 | def festival_description(self): 422 | return self._lunar_festival() + self._solar_festival() 423 | 424 | def solar_Term(self): 425 | #今日节气 426 | return Festival.solar_Term(solar_month,solar_day) 427 | 428 | def solar_date_description(self):#阳历时间 429 | #2000年01月01日 430 | return str(solar_year) + "年" + str(solar_month) + "月" + str(solar_day) + "日" 431 | 432 | def solar_week_number(self): 433 | return datetime.datetime(int(solar_year), int(solar_month), int(solar_day)).isocalendar()[1] 434 | 435 | def solar_week_number_description(self):#周数 ChineseWord.week_num_str 436 | return "第"+ChineseWord.week_num_str(self.solar_week_number()) + "周" 437 | #return "第"+str(datetime.datetime(int(solar_year), int(solar_month), int(solar_day)).isocalendar()[1]) + "周" 438 | 439 | def week_description(self): 440 | #星期几 441 | return ChineseWord.weekday_str(solar_weekday) 442 | 443 | def lunar_date_description(self): 444 | #正月初一 445 | return ChineseWord.year_lunar(lunar_year) + ' ' + ChineseWord.month_lunar(lunar_isLeapMonth,lunar_month) + ChineseWord.day_lunar(lunar_day) 446 | 447 | def solar(self): 448 | return solar_year,solar_month,solar_day 449 | 450 | def lunar(self): 451 | return lunar_year,lunar_month,lunar_day 452 | 453 | @classmethod 454 | def lunar_to_solar(cls,year,month,day): 455 | l = LunarDate(year,month,day,False) 456 | return l.toSolarDate() 457 | 458 | 459 | #date '20000101' 周岁 阳历 460 | @classmethod 461 | def get_age_by_birth_solar(cls,year,month,day): 462 | if solar_month < month: #8 9 463 | return solar_year - year - 1 464 | elif solar_month == month: 465 | if solar_day < day: 466 | return solar_year - year - 1 467 | else: 468 | return solar_year - year 469 | else: # 8 7 470 | return solar_year - year 471 | 472 | # 传进来的是阴历,要转换成周岁 473 | @classmethod 474 | def get_age_by_birth_lunar_to_solar(cls,year,month,day): 475 | res = CalendarToday.lunar_to_solar(year,month,day) 476 | y = res.year 477 | m = res.month 478 | d = res.day 479 | CalendarToday() 480 | return cls.get_age_by_birth_solar(y, m, d) 481 | 482 | #date '20000101' 虚岁 阴历 483 | @classmethod 484 | def get_age_by_birth_lunar(cls,year,month,day): 485 | return lunar_year - year + 1 486 | 487 | Festival._create_terms() 488 | Festival._create_weekday_festival() 489 | 490 | def main(): 491 | cal = CalendarToday() 492 | print(cal.solar_Term()) 493 | print(cal.festival_description()) 494 | print(cal.solar_date_description()) 495 | print(cal.week_description()) 496 | print(cal.lunar_date_description()) 497 | print(cal.solar()) 498 | print(cal.lunar()) 499 | ltos = CalendarToday.lunar_to_solar(2024,12, 25) 500 | date_str = ltos.strftime('%Y%m%d') 501 | print('l to s:', date_str) 502 | print(Festival.solar_Term(2,4)) 503 | print(ChineseWord.year_lunar(2020)) 504 | print(CalendarToday.get_age_by_birth_solar(1988, 8, 22)) 505 | print(CalendarToday.get_age_by_birth_lunar_to_solar(1988, 7, 11)) 506 | print(CalendarToday.get_age_by_birth_solar(2022, 8, 20)) 507 | print(CalendarToday.get_age_by_birth_solar(2022, 7, 20)) 508 | print(CalendarToday.get_age_by_birth_lunar_to_solar(1988,7, 11)) 509 | 510 | if __name__ == '__main__': 511 | main() 512 | 513 | 514 | """ 515 | idx = 2020 - 1900 516 | yearInfo = Info.yearInfos[idx] 517 | isLeapMonth = False 518 | for _month, _days, _isLeapMonth in LunarDate._enumMonth(yearInfo): 519 | if _isLeapMonth: 520 | isLeapMonth = _isLeapMonth 521 | break 522 | """ 523 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "chineseholiday", 3 | "name": "chineseholiday", 4 | "version": 0.2, 5 | "documentation": "https://github.com/Crazysiri/chineseholiday", 6 | "requirements": [], 7 | "dependencies": [], 8 | "codeowners": [ 9 | "@Crazysiri" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/sensor.py: -------------------------------------------------------------------------------- 1 | #! usr/bin/python 2 | #coding=utf-8 3 | """ 4 | 中国节假日 5 | 版本:0.1.3 6 | """ 7 | from homeassistant.helpers.entity import Entity 8 | from homeassistant.core import callback 9 | import homeassistant.helpers.config_validation as cv 10 | from homeassistant.helpers import event as evt 11 | import voluptuous as vol 12 | import logging 13 | from homeassistant.util import Throttle 14 | from homeassistant.components.sensor import PLATFORM_SCHEMA 15 | from homeassistant.const import ( 16 | CONF_NAME) 17 | from homeassistant.helpers.entity import generate_entity_id 18 | import datetime 19 | from datetime import timedelta 20 | import time 21 | from . import holiday 22 | from . import lunar 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | """ 27 | cal = lunar.CalendarToday() 28 | print(cal.solar_Term()) 29 | print(cal.festival_description()) 30 | print(cal.solar_date_description()) 31 | print(cal.week_description()) 32 | print(cal.lunar_date_description()) 33 | print(cal.solar()) 34 | print(cal.lunar()) 35 | """ 36 | 37 | _Log=logging.getLogger(__name__) 38 | 39 | DEFAULT_NAME = 'chinese_holiday' 40 | CONF_UPDATE_INTERVAL = 'update_interval' 41 | CONF_SOLAR_ANNIVERSARY = 'solar_anniversary' 42 | CONF_LUNAR_ANNIVERSARY = 'lunar_anniversary' 43 | CONF_CALCULATE_AGE = 'calculate_age' 44 | CONF_CALCULATE_AGE_DATE = 'date' 45 | CONF_CALCULATE_AGE_NAME = 'name' 46 | 47 | CONF_NOTIFY_SCRIPT_NAME = 'notify_script_name' 48 | CONF_NOTIFY_TIMES = 'notify_times' 49 | CONF_NOTIFY_PRINCIPLES = 'notify_principles' 50 | CONF_NOTIFY_PRINCIPLES_DATE = 'date' 51 | CONF_NOTIFY_PRINCIPLES_NAME = 'name' 52 | CONF_NOTIFY_PRINCIPLES_SOLAR = 'solar' 53 | 54 | # CALCULATE_AGE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ 55 | # vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, 56 | # vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, 57 | # })) 58 | 59 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 60 | vol.Optional(CONF_NOTIFY_TIMES,default=['09:00:00']): [cv.time], 61 | vol.Optional(CONF_NOTIFY_SCRIPT_NAME, default=''): cv.string, 62 | vol.Optional('show_detail',default=True): cv.boolean, 63 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 64 | vol.Optional(CONF_SOLAR_ANNIVERSARY, default={}): { 65 | str : [str] 66 | }, 67 | vol.Optional(CONF_LUNAR_ANNIVERSARY, default={}): { 68 | str : [str] 69 | }, 70 | vol.Optional(CONF_CALCULATE_AGE,default=[]): [ 71 | { 72 | vol.Optional(CONF_CALCULATE_AGE_DATE): cv.string, 73 | vol.Optional(CONF_CALCULATE_AGE_NAME): cv.string, 74 | } 75 | ], 76 | vol.Optional(CONF_NOTIFY_PRINCIPLES,default={}): { 77 | str : [ 78 | { 79 | vol.Optional(CONF_NOTIFY_PRINCIPLES_NAME,default=''): cv.string, 80 | vol.Optional(CONF_NOTIFY_PRINCIPLES_DATE,default=''): cv.string, 81 | vol.Optional(CONF_NOTIFY_PRINCIPLES_SOLAR,default=True): cv.boolean, 82 | } 83 | ] 84 | }, 85 | vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(hours=8)): (vol.All(cv.time_period, cv.positive_timedelta)), 86 | }) 87 | 88 | 89 | #公历 纪念日 每年都有的 90 | # {'0101':['aa生日','bb生日']} 91 | SOLAR_ANNIVERSARY = {} 92 | 93 | #农历 纪念日 每年都有的 94 | # {'0101':['aa生日','bb生日']} 95 | LUNAR_ANNIVERSARY = {} 96 | 97 | #纪念日 指定时间的(出生日到今天的计时或今天到某一天还需要的时间例如金婚) 98 | CALCULATE_AGE = {} 99 | 100 | NOTIFY_PRINCIPLES = {} 101 | # '2010-10-10 08:23:12': 'xx', 102 | 103 | def setup_platform(hass, config, add_devices, discovery_info=None): 104 | """Set up the movie sensor.""" 105 | 106 | name = config[CONF_NAME] 107 | interval = config.get(CONF_UPDATE_INTERVAL) 108 | global SOLAR_ANNIVERSARY 109 | global LUNAR_ANNIVERSARY 110 | global CALCULATE_AGE 111 | global NOTIFY_PRINCIPLES 112 | SOLAR_ANNIVERSARY = config[CONF_SOLAR_ANNIVERSARY] 113 | LUNAR_ANNIVERSARY = config[CONF_LUNAR_ANNIVERSARY] 114 | CALCULATE_AGE = config[CONF_CALCULATE_AGE] 115 | NOTIFY_PRINCIPLES = config[CONF_NOTIFY_PRINCIPLES] 116 | script_name = config[CONF_NOTIFY_SCRIPT_NAME] 117 | notify_times = config[CONF_NOTIFY_TIMES] 118 | show_detail = config['show_detail'] 119 | sensors = [ChineseHolidaySensor(hass, name,notify_times,script_name, interval,show_detail)] 120 | add_devices(sensors, True) 121 | 122 | 123 | class ChineseHolidaySensor(Entity): 124 | 125 | _holiday = None 126 | _lunar = None 127 | 128 | def __init__(self, hass, name,notify_times,script_name, interval,show_detail): 129 | """Initialize the sensor.""" 130 | self.client_name = name 131 | self._show_detail = show_detail 132 | self._state = None 133 | self._hass = hass 134 | self._script_name = script_name 135 | self._notify_times = notify_times 136 | self._holiday = holiday.Holiday() 137 | self._lunar = lunar.CalendarToday() 138 | self.attributes = {} 139 | self.localizedAttributes = {} #汉化的attributes 用来显示 140 | self.entity_id = generate_entity_id( 141 | 'sensor.{}', self.client_name, hass=self._hass) 142 | self.update = Throttle(interval)(self._update) 143 | self.setListener() #设置脚本通知的定时器 144 | self.setUpdateListener() #设置更新时间,凌晨00:00:15秒,15秒就是过了一天随便定定 145 | @property 146 | def name(self): 147 | """Return the name of the sensor.""" 148 | return '节假日' 149 | 150 | @property 151 | def state(self): 152 | """Return the state of the sensor.""" 153 | return self._state 154 | 155 | @property 156 | def tomorrow_state(self): 157 | return self._tomorrow_state 158 | 159 | @property 160 | def icon(self): 161 | """Icon to use in the frontend, if any.""" 162 | return 'mdi:calendar-today' 163 | 164 | # @property 165 | # def state_attributes(self): 166 | # return self.attributes 167 | 168 | @property 169 | def extra_state_attributes(self): 170 | """Return the state attributes. for new version like 2022:4.3""" 171 | return self.localizedAttributes 172 | 173 | @property 174 | def device_state_attributes(self): 175 | """Return the state attributes.""" 176 | return self.localizedAttributes 177 | 178 | #更新为两处,一处为Throttle 默认8小时,此处为第二处 是每天凌晨12:01更新 179 | def setUpdateListener(self): 180 | 181 | @callback 182 | def _listener_callback(_): 183 | self.setUpdateListener() 184 | self._hass.async_add_executor_job(self._update) 185 | 186 | self._updateListener = None 187 | 188 | now = datetime.datetime.utcnow() + timedelta(hours=8) 189 | notify_date_str = now.strftime('%Y-%m-%d') + ' ' + str('00:00:15') #目前预设是每天9点通知 190 | notify_date = datetime.datetime.strptime(notify_date_str, "%Y-%m-%d %H:%M:%S") 191 | notify_date = notify_date + timedelta(days=1) #明天的时间 192 | # notify_date = now + timedelta(seconds=10) 193 | 194 | self._updateListener = evt.async_track_point_in_time( 195 | self._hass, _listener_callback, notify_date 196 | ) 197 | 198 | def setListener(self): 199 | 200 | @callback 201 | def _date_listener_callback(_): 202 | self.setListener() #重设定时器 203 | self.notify() #执行通知 204 | 205 | self._listener = None 206 | # now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 207 | notify_dates = [] 208 | now = datetime.datetime.utcnow() + timedelta(hours=8) 209 | #_notify_times 要保证按时间顺序,否则这里的逻辑容易出错 210 | for notify_time in self._notify_times: 211 | notify_date_str = now.strftime('%Y-%m-%d') + ' ' + str(notify_time) #目前预设是每天9点通知 212 | notify_date = datetime.datetime.strptime(notify_date_str, "%Y-%m-%d %H:%M:%S") 213 | notify_dates.append(notify_date) 214 | 215 | notify_d = None 216 | for date in notify_dates: 217 | 218 | # notify_date = now + timedelta(seconds=10) 219 | if date > now: 220 | notify_d = date 221 | break 222 | 223 | if not notify_d: 224 | #上面发现预设的提醒时间已经没有今天的了,那么拿最小的时间加1天也就是第二天再提醒,所以_notify_times要保证按顺序 225 | notify_d = notify_dates[0] 226 | _LOGGER.info('小于') 227 | notify_d = notify_d + timedelta(days=1) #已经过了就设置为明天的时间 228 | _LOGGER.info('notify_date') 229 | _LOGGER.info(notify_d) 230 | self._listener = evt.async_track_point_in_time( 231 | self._hass, _date_listener_callback, notify_d 232 | ) 233 | 234 | 235 | 236 | def notify(self): 237 | import threading 238 | def call_service_script(message): 239 | _LOGGER.info('begin call') 240 | _LOGGER.info(message) 241 | self._hass.services.call('script',self._script_name,{'message':message}) 242 | _LOGGER.info('end call') 243 | 244 | #[{'days':1,'list':['国庆节']}] 245 | def dates_need_to_notify(): 246 | """ 247 | { 248 | '14|7|1':[{'date':'0101','solar':True}] 249 | } 250 | """ 251 | dates = [] 252 | for key,value in NOTIFY_PRINCIPLES.items(): 253 | _LOGGER.info(key) 254 | _LOGGER.info(value) 255 | days = key.split('|') #解析需要匹配的天 14|7|1 分别还有14,7,1天时推送 256 | for item in value: 257 | date = item['date'] #0101 格式的日期字符串 258 | solar = item['solar'] #是否是公历 259 | name = item['name'] #名称这个是个特殊逻辑,只有Festival._weekday_festival中记录的才会用,因为这里记录的每年时间不固定 260 | fes_date = None 261 | fes_list = [] 262 | 263 | #name和date是互斥的,因为name就是为了母亲节父亲节设计的 264 | if name: 265 | try: 266 | fes_list = [name] 267 | date_str = str(self._lunar.solar()[0])+lunar.Festival._weekday_festival_reserse[name] #20200101 268 | fes_date = datetime.datetime.strptime(date_str,'%Y%m%d').date() 269 | except Exception as e: 270 | pass 271 | elif date: 272 | if solar: 273 | date_str = str(self._lunar.solar()[0])+date #20200101 274 | fes_date = datetime.datetime.strptime(date_str,'%Y%m%d').date() 275 | try: 276 | fes_list = lunar.Festival._solar_festival[date] 277 | except Exception as e: 278 | pass 279 | try: 280 | fes_list += SOLAR_ANNIVERSARY[date] 281 | except Exception as e: 282 | pass 283 | else: 284 | month = int(date[:2]) 285 | day = int(date[2:]) 286 | fes_date = lunar.CalendarToday.lunar_to_solar(self._lunar.solar()[0],month,day)#下标和位置 287 | try: 288 | fes_list = lunar.Festival._lunar_festival[date] 289 | except Exception as e: 290 | pass 291 | try: 292 | fes_list += LUNAR_ANNIVERSARY[date] 293 | except Exception as e: 294 | pass 295 | 296 | now_str = datetime.datetime.now().strftime('%Y-%m-%d') 297 | today = datetime.datetime.strptime(now_str, "%Y-%m-%d").date() 298 | diff = (fes_date - today).days 299 | 300 | if (str(diff) in days) and fes_list: 301 | item['day'] = diff 302 | item['list'] = fes_list 303 | dates.append(item) 304 | 305 | return dates 306 | 307 | if self._script_name and NOTIFY_PRINCIPLES: 308 | dates = dates_need_to_notify() 309 | messages = [] 310 | for item in dates: 311 | days = item['day'] 312 | fes_list = item['list'] 313 | if days == 0: 314 | messages.append('今天是 ' + ','.join(fes_list)) 315 | else: 316 | messages.append('距离 ' + ','.join(fes_list) + '还有' + str(days) + '天') 317 | if messages: 318 | t1 = threading.Thread(target=call_service_script,args=(','.join(messages),)) 319 | t1.start() 320 | 321 | #计算纪念日(每年都有的) count 返回n条 默认只返回1条 322 | def calculate_anniversary(self,count=1): 323 | def anniversary_handle(l,age): 324 | 325 | l_new = [] 326 | if age != -1: #年龄-1的时候就是没有年份,而且name里得有生日才加这个 327 | for name in l: 328 | if '生日' in name: 329 | l_new.append('%s(%s岁)' % (name,age)) 330 | else: 331 | l_new.append('%s(%s周年)' % (name,age)) 332 | l = l_new; 333 | return ','.join(l) 334 | """ 335 | { 336 | '20200101':[{'anniversary':'0101#xx生日#','solar':True}] 337 | } 338 | """ 339 | anniversaries = {} 340 | 341 | for key,value in LUNAR_ANNIVERSARY.items(): 342 | if len(key) == 8: #带年 343 | year = int(key[:4]) 344 | month = int(key[4:6]) 345 | day = int(key[6:]) 346 | age = lunar.CalendarToday.get_age_by_birth_lunar_to_solar(year,month,day) + 1 #周岁 347 | else: 348 | month = int(key[:2]) 349 | day = int(key[2:]) 350 | age = -1 351 | 352 | y = self._lunar.lunar()[0] 353 | if month < self._lunar.lunar()[1]: 354 | y += 1 355 | elif month == self._lunar.lunar()[1]: 356 | if day < self._lunar.lunar()[2]: 357 | y += 1 358 | 359 | solar_date = lunar.CalendarToday.lunar_to_solar(y,month,day)#下标和位置 360 | 361 | date_str = solar_date.strftime('%Y%m%d') 362 | self._lunar = lunar.CalendarToday() 363 | try: 364 | l = anniversaries[date_str] 365 | except Exception as e: 366 | anniversaries[date_str] = [] 367 | l = anniversaries[date_str] 368 | l.append({'anniversary':anniversary_handle(value,age),'solar':False}) 369 | 370 | for key,value in SOLAR_ANNIVERSARY.items(): 371 | 372 | if len(key) == 8: #带年 373 | year = int(key[:4]) 374 | month = int(key[4:6]) 375 | day = int(key[6:]) 376 | key = key[4:] #剩下的 377 | age = lunar.CalendarToday.get_age_by_birth_solar(year,month,day) + 1#周岁 378 | else: 379 | age = -1 380 | date_str = str(self._lunar.solar()[0])+key #20200101 381 | try: 382 | l = anniversaries[date_str] 383 | except Exception as e: 384 | anniversaries[date_str] = [] 385 | l = anniversaries[date_str] 386 | l.append({'anniversary':anniversary_handle(value,age),'solar':True}) 387 | 388 | 389 | #根据key 排序 因为key就是日期字符串 390 | l=sorted(anniversaries.items(),key=lambda x:x[0]) 391 | #找到第一个大于今天的纪念日 392 | cur = 0 393 | results = [] 394 | for item in l: 395 | key = item[0] 396 | annis = item[1] #纪念日数组 397 | now_str = datetime.datetime.now().strftime('%Y-%m-%d') 398 | today = datetime.datetime.strptime(now_str, "%Y-%m-%d") 399 | last_update = datetime.datetime.strptime(key,'%Y%m%d') 400 | days = (last_update - today).days 401 | if days > 0 and cur < count: #只有大于今天的才会显示,今天的会在纪念日中显示 402 | cur += 1 403 | results.append((key,days,annis)) 404 | return results 405 | 406 | #今天是否是自定义的纪念日(阴历和阳历) 407 | def custom_anniversary(self): 408 | l_month = self._lunar.lunar()[1] 409 | l_day = self._lunar.lunar()[2] 410 | s_month = self._lunar.solar()[1] 411 | s_day = self._lunar.solar()[2] 412 | l_anni = lunar.festival_handle(LUNAR_ANNIVERSARY,l_month,l_day) 413 | s_anni = lunar.festival_handle(SOLAR_ANNIVERSARY,s_month,s_day) 414 | anni = '' 415 | if l_anni: 416 | anni += l_anni 417 | if s_anni: 418 | anni += s_anni 419 | return anni 420 | 421 | 422 | def calculate_age(self): 423 | if not CALCULATE_AGE: 424 | return 425 | now_day = datetime.datetime.now() 426 | count_dict = {} 427 | past_calculate_age_count = 0 428 | future_calculate_age_count = 0 429 | past_dates = [] #记录所有过去的节日 430 | future_dates = [] #记录所有将来的节日 431 | 432 | for item in CALCULATE_AGE: 433 | date = item[CONF_CALCULATE_AGE_DATE] 434 | name = item[CONF_CALCULATE_AGE_NAME] 435 | key = datetime.datetime.strptime(date,'%Y-%m-%d %H:%M:%S') 436 | if (now_day - key).total_seconds() > 0: 437 | total_seconds = int((now_day - key).total_seconds()) 438 | year, remainder = divmod(total_seconds,60*60*24*365) 439 | day, remainder = divmod(remainder,60*60*24) 440 | hour, remainder = divmod(remainder,60*60) 441 | minute, second = divmod(remainder,60) 442 | date_attributes = {} 443 | date_attributes['name'] = name 444 | self.localizedAttributes[str(past_calculate_age_count + 1) + '.过去纪念日'] = name 445 | date_attributes['date'] = date 446 | self.localizedAttributes[str(past_calculate_age_count + 1) + '.过去纪念日日期'] = date 447 | date_attributes['interval'] = total_seconds 448 | date_attributes['description'] = '{}年{}天{}小时{}分钟{}秒'.format(year,day,hour,minute,second) 449 | self.localizedAttributes[str(past_calculate_age_count + 1) + '.已经过去'] = '{}年{}天{}小时{}分钟{}秒'.format(year,day,hour,minute,second) 450 | past_dates.append(date_attributes) 451 | past_calculate_age_count += 1 452 | 453 | #变成自动每年自动增加的,就相当于设置一个过去的日期会生成一个未来的日期,并名字变成xx n 周年/周岁 454 | counter = 0 455 | while (now_day - key).total_seconds() > 0: 456 | date = str(key.year+1) + date[4:] 457 | key = datetime.datetime.strptime(date,'%Y-%m-%d %H:%M:%S') 458 | counter += 1 459 | 460 | if '生日' in name: 461 | name = name + ' ' + str(counter) + '周岁' 462 | else: 463 | name = name + ' ' + str(counter) + '周年' 464 | 465 | 466 | if (now_day - key).total_seconds() < 0: 467 | total_seconds = int((key - now_day ).total_seconds()) 468 | year, remainder = divmod(total_seconds,60*60*24*365) 469 | day, remainder = divmod(remainder,60*60*24) 470 | hour, remainder = divmod(remainder,60*60) 471 | minute, second = divmod(remainder,60) 472 | date_attributes = {} 473 | date_attributes['name'] = name 474 | self.localizedAttributes[str(future_calculate_age_count + 1) + '.未来纪念日'] = name 475 | date_attributes['date'] = date 476 | self.localizedAttributes[str(future_calculate_age_count + 1) + '.未来纪念日日期'] = date 477 | date_attributes['interval'] = total_seconds 478 | date_attributes['description'] = '{}年{}天{}小时{}分钟{}秒'.format(year,day,hour,minute,second) 479 | self.localizedAttributes[str(future_calculate_age_count + 1) + '.还有'] = '{}年{}天{}小时{}分钟{}秒'.format(year,day,hour,minute,second) 480 | future_dates.append(date_attributes) 481 | future_calculate_age_count += 1 482 | 483 | self.attributes['past_dates'] = past_dates 484 | self.attributes['future_dates'] = future_dates 485 | 486 | 487 | def nearest_holiday(self): 488 | '''查找离今天最近的法定节假日,并显示天数''' 489 | now_day = datetime.date.today() 490 | count_dict = {} 491 | results = self._holiday.getHoliday() 492 | for key in results.keys(): 493 | if (key - now_day).days > 0: 494 | count_dict[key] = (key - now_day).days 495 | nearest_holiday_dict = {} 496 | if count_dict: 497 | nearest_holiday_dict['name'] = results[min(count_dict)] 498 | nearest_holiday_dict['date'] = min(count_dict).isoformat() 499 | nearest_holiday_dict['day'] = str((min(count_dict)-now_day).days) 500 | 501 | return nearest_holiday_dict 502 | 503 | def _update(self): 504 | _LOGGER.info('update ...') 505 | self.attributes = {} #重置attributes 506 | self.localizedAttributes = {} 507 | self._lunar = lunar.CalendarToday()#重新赋值 508 | 509 | self._state = self._holiday.is_holiday_today() 510 | self._tomorrow_state = self._holiday.is_holiday_tomorrow() 511 | self.attributes['tomorrow_state'] = self._tomorrow_state 512 | self.attributes['solar'] = self._lunar.solar_date_description() 513 | self.localizedAttributes['今天'] = self._lunar.solar_date_description() 514 | # self.attributes['今天'] = datetime.date.today().strftime('%Y{y}%m{m}%d{d}').format(y='年', m='月', d='日') 515 | self.attributes['week'] = self._lunar.week_description() 516 | self.localizedAttributes['星期'] = self._lunar.week_description() 517 | self.attributes['lunar'] = self._lunar.lunar_date_description() 518 | self.localizedAttributes['农历'] = self._lunar.lunar_date_description() 519 | self.attributes['week_number'] = self._lunar.solar_week_number() 520 | self.localizedAttributes['周数'] = self._lunar.solar_week_number_description() 521 | term = self._lunar.solar_Term() 522 | if term: 523 | self.attributes['term'] = term 524 | self.localizedAttributes['节气'] = term 525 | festival = self._lunar.festival_description() 526 | if festival: 527 | self.attributes['festival'] = festival 528 | self.localizedAttributes['节日'] = festival 529 | 530 | custom = self.custom_anniversary() 531 | if custom: 532 | self.attributes['anniversary'] = custom 533 | self.localizedAttributes['纪念日'] = custom 534 | 535 | #这里传的数字 控制 显示几个 自定义的纪念日 536 | results = self.calculate_anniversary(5) 537 | _LOGGER.info(f'anniversaries: ${results}') 538 | 539 | self.attributes['next_anniversaries'] = [] 540 | self.localizedAttributes['接下来的纪念日'] = [] 541 | #拼接接下来的纪念日 542 | for i in range(0,len(results)): 543 | key,days,annis = results[i] 544 | s = '' 545 | for anni in annis: 546 | s += anni['anniversary'] 547 | if i == 0: 548 | self.attributes['nearest_anniversary'] = s 549 | self.localizedAttributes['最近的纪念日'] = s 550 | self.attributes['nearest_anniversary_date'] = key 551 | self.localizedAttributes['最近的纪念日日期'] = key 552 | self.attributes['nearest_anniversary_days'] = days 553 | self.localizedAttributes['最近的纪念日还有'] = str(days) + '天' 554 | else: 555 | next_anniversaries = self.attributes['next_anniversaries'] 556 | next_anniversaries.append({'date':key,'name':s,'days':days}) 557 | next_anniversaries_local = self.localizedAttributes['接下来的纪念日'] 558 | next_anniversaries_local.append('距离纪念日 %s-%s 还有 %s 天 ' % (s,key,days)) 559 | 560 | nearest = self.nearest_holiday() 561 | if nearest: 562 | self.attributes['nearest_holiday'] = nearest['name'] 563 | self.localizedAttributes['最近的节日'] = nearest['name'] 564 | self.attributes['nearest_holiday_date'] = nearest['date'] 565 | self.localizedAttributes['最近的节日日期'] = nearest['date'] 566 | self.attributes['nearest_holiday_days'] = int(nearest['day']) 567 | self.localizedAttributes['最近的节日还有'] = str(nearest['day']) + '天' 568 | self.calculate_age() 569 | 570 | info = self._holiday.nearest_holiday_info(12,45) 571 | if info: 572 | self.attributes['holiday_info'] = info 573 | self.localizedAttributes['节假日放假详情'] = info 574 | 575 | if self._show_detail: 576 | self.localizedAttributes['data'] = self.attributes 577 | 578 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/term.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.7 2 | # -*- coding:utf-8 -*- 3 | 4 | ''' 5 | 整体思路 6 | 1:根据公式算出节气日期 1900 年到 2100 200 年的时间 7 | 2:特殊的年份进行纠正 8 | ''' 9 | import sys 10 | import json 11 | import gc 12 | import os 13 | 14 | default_encoding = 'utf-8' 15 | if sys.getdefaultencoding() != default_encoding: 16 | reload(sys) 17 | 18 | class jieqi: 19 | # 计算节气的C常量组 20 | C_list_21 = [3.87, 18.73, 5.63, 20.646, 4.81, 20.1, 5.52, 21.04, 5.678, 21.37, 7.108, 22.83, 7.5, 23.13, 7.646, 23.042, 8.318, 23.438, 7.438, 22.36, 7.18, 21.94, 5.4055, 20.12] 21 | 22 | C_list_20 = [4.6295, 19.4599, 6.3826, 21.4155, 5.59,20.888, 6.318, 21.86, 6.5, 22.2, 7.928, 23.65, 8.35, 23.95, 8.44, 23.822, 9.098, 24.218, 8.218, 23.08, 7.9, 22.6, 6.11, 20.84] 23 | 24 | # 节气名称组 25 | name_Arr = ["立春", "雨水", "惊蛰", "春分", "清明", "谷雨", "立夏", "小满", "芒种", "夏至", "小暑", "大暑", "立秋", "处暑", "白露", "秋分", "寒露", "霜降", "立冬", "小雪", "大雪", "冬至", "小寒", "大寒"] 26 | 27 | def __init__(self): 28 | self.c_list=[] 29 | 30 | ## 特殊年份特殊节气进行纠正 31 | def rectify_year(self,year,jieqiid,day): 32 | ## 特殊年份 33 | rectify_year = [2026,2084,1911,2008,1902,1928,1925,2016,1922,2002,1927,1942,2089,2089,1978,1954,1918,2021,1982,2082,2019,2021] 34 | ## 特殊节气 35 | rectify_jieqi = [1,3,6,7,8,9,10,10,11,12,14,15,17,18,19,20,21,21,22,22,23] 36 | ## 偏移量 37 | rectify_offset = [-1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,1,-1,1] 38 | pop2 = -1 39 | if year in rectify_year: 40 | if year == 2089: 41 | pop1 = rectify_year.index(year) ## 找到位置 42 | pop2 = pop1+1 43 | else: 44 | pop1 = rectify_year.index(year) ## 找到位置 45 | 46 | if rectify_jieqi[pop1] == jieqiid: 47 | day = day + int(rectify_offset[pop1]) 48 | if rectify_jieqi[pop2] == jieqiid: 49 | day = day + int(rectify_offset[pop2]) 50 | return day 51 | 52 | 53 | #计算节气日期,并创建文件 54 | def creat_year_jieqi(self,year): 55 | year_pre = year//100 56 | if year_pre == 19: 57 | C_arr = self.C_list_20 58 | elif year_pre == 20: 59 | C_arr = self.C_list_21 60 | 61 | year_num = year%100 62 | list_arr = [] 63 | for i in range(0, 24): 64 | C = C_arr[i] 65 | ## 注意:凡闰年3月1日前闰年数要减一,即:L=[(Y-1)/4],因为小寒、大寒、立春、雨水这两个节气都小于3月1日,所以 y = y-1 66 | if i == 0 or i == 1 or i == 22 or i == 23: 67 | if self.comrun(year): 68 | days = (year_num * 0.2422 + C) // 1 - ((year_num-1)// 4) 69 | else: 70 | days = (year_num * 0.2422 + C) // 1 - (year_num // 4) 71 | else: 72 | days = (year_num * 0.2422 + C) // 1 - (year_num // 4) 73 | 74 | ## 特殊年份节气进行纠正 75 | days = self.rectify_year(year,i,days) 76 | 77 | days = int(days) 78 | days = '%02d' % days 79 | y = int(year_num // 1) 80 | m = i // 2 + 2 81 | if m == 13: 82 | m = 1 83 | m = '%02d' % m 84 | y = '%02d' % y 85 | strs = "{3}{0}-{1}-{2}".format(str(y), str(m), str(days),str(year_pre)) 86 | item = dict(name=self.name_Arr[i], jieqiid=str(i + 1), time=strs) 87 | list_arr.append(item) 88 | return list_arr 89 | 90 | ## 算是否是闰年 91 | def comrun(self,year): 92 | i = 0 93 | if (year % 4) != 0 : 94 | i=0 95 | elif ((year % 100) == 0) & ((year % 400) != 0): 96 | i=0 97 | else: 98 | i=1 99 | return i 100 | 101 | # jieqi = jieqi() 102 | 103 | # print(jieqi.creat_year_jieqi(2020)) 104 | #jieqi.read_json_file('2006') 105 | #jieqi.check_all_file() 106 | # 107 | # for i in range(1900,2100): 108 | # jieqi.creat_year_jieqi(i) 109 | -------------------------------------------------------------------------------- /custom_components/chineseholiday/test.py: -------------------------------------------------------------------------------- 1 | #! usr/bin/python 2 | #coding=utf-8 3 | 4 | import datetime 5 | from datetime import timedelta 6 | import holiday 7 | import lunar 8 | 9 | SOLAR_ANNIVERSARY = [ 10 | "0620#aa 生日# #bb 纪念日#", 11 | "0721#cc 生日#" 12 | ] 13 | #农历 纪念日 每年都有的 14 | LUNAR_ANNIVERSARY = [ 15 | "0602#cc 农历生日#", 16 | ] 17 | 18 | CALCULATE_AGE = [ 19 | { 20 | 'date':'2010-10-10 08:23:12', 21 | 'name':'xxx' 22 | } 23 | ] 24 | 25 | # _lunar = lunar.CalendarToday() 26 | 27 | def calculate_age(): 28 | if not CALCULATE_AGE: 29 | return 30 | now_day = datetime.datetime.now() 31 | count_dict = {} 32 | for item in CALCULATE_AGE: 33 | date = item[CONF_CALCULATE_AGE_DATE] 34 | name = item[CONF_CALCULATE_AGE_NAME] 35 | key = datetime.datetime.strptime(date,'%Y-%m-%d %H:%M:%S') 36 | if (now_day - key).total_seconds() > 0: 37 | total_seconds = int((now_day - key).total_seconds()) 38 | year, remainder = divmod(total_seconds,60*60*24*365) 39 | day, remainder = divmod(remainder,60*60*24) 40 | hour, remainder = divmod(remainder,60*60) 41 | minute, second = divmod(remainder,60) 42 | self.attributes['离'+name+'过去'] = '{}年 {} 天 {} 小时 {} 分钟 {} 秒'.format(year,day,hour,minute,second) 43 | if (now_day - key).total_seconds() < 0: 44 | total_seconds = int((key - now_day ).total_seconds()) 45 | year, remainder = divmod(total_seconds,60*60*24*365) 46 | day, remainder = divmod(remainder,60*60*24) 47 | hour, remainder = divmod(remainder,60*60) 48 | minute, second = divmod(remainder,60) 49 | self.attributes['离'+name+'还差'] = '{}年 {} 天 {} 小时 {} 分钟 {} 秒'.format(year,day,hour,minute,second) 50 | 51 | 52 | def custom_anniversary(): 53 | lunar_month = _lunar.lunar()[1] 54 | lunar_day = _lunar.lunar()[2] 55 | solar_month = _lunar.solar()[1] 56 | solar_day = _lunar.solar()[2] 57 | lunar_anni = lunar.festival_handle(LUNAR_ANNIVERSARY,lunar_month,lunar_day) 58 | solar_anni = lunar.festival_handle(SOLAR_ANNIVERSARY,solar_month,solar_day) 59 | anni = '' 60 | if lunar_anni: 61 | anni += lunar_anni 62 | if solar_anni: 63 | anni += solar_anni 64 | return anni 65 | 66 | #计算纪念日(每年都有的) 67 | def calculate_anniversary(): 68 | def anniversary_handle(input_str): 69 | list = input_str.split('#') 70 | annis = [] 71 | for i in range(1,len(list)): 72 | s = list[i] 73 | s = s.strip() 74 | if s: 75 | annis.append(s) 76 | return ','.join(annis) 77 | """ 78 | { 79 | '20200101':[{'anniversary':'0101#xx生日#','solar':True}] 80 | } 81 | """ 82 | anniversaries = {} 83 | 84 | for l in LUNAR_ANNIVERSARY: 85 | date_str = l.split('#')[0] 86 | month = int(date_str[:2]) 87 | day = int(date_str[2:]) 88 | solar_date = lunar.CalendarToday.lunar_to_solar(_lunar.solar()[0],month,day)#下标和位置 89 | date_str = solar_date.strftime('%Y%m%d') 90 | try: 91 | list = anniversaries[date_str] 92 | except Exception as e: 93 | anniversaries[date_str] = [] 94 | list = anniversaries[date_str] 95 | list.append({'anniversary':anniversary_handle(l),'solar':False}) 96 | 97 | for s in SOLAR_ANNIVERSARY: 98 | date_str = s.split('#')[0] 99 | date_str = str(_lunar.solar()[0])+date_str #20200101 100 | try: 101 | list = anniversaries[date_str] 102 | except Exception as e: 103 | anniversaries[date_str] = [] 104 | list = anniversaries[date_str] 105 | list.append({'anniversary':anniversary_handle(s),'solar':True}) 106 | 107 | 108 | #根据key 排序 因为key就是日期字符串 109 | list=sorted(anniversaries.items(),key=lambda x:x[0]) 110 | #找到第一个大于今天的纪念日 111 | for item in list: 112 | key = item[0] 113 | annis = item[1] #纪念日数组 114 | now_str = datetime.datetime.now().strftime('%Y-%m-%d') 115 | today = datetime.datetime.strptime(now_str, "%Y-%m-%d") 116 | last_update = datetime.datetime.strptime(key,'%Y%m%d') 117 | days = (last_update - today).days 118 | if days > 0: 119 | return key,days,annis 120 | return None,None,None 121 | 122 | import asyncio 123 | import itertools 124 | import sys 125 | import time 126 | 127 | 128 | async def spin(): 129 | for i in itertools.cycle('|/-\\'): 130 | write,flush = sys.stdout.write,sys.stdout.flush 131 | write(i) 132 | flush() 133 | write('\x08'*len(i)) 134 | try: 135 | await asyncio.sleep(1) 136 | except asyncio.CancelledError: 137 | break 138 | 139 | 140 | async def slow_f(): 141 | await asyncio.sleep(3) 142 | return 3 143 | 144 | 145 | async def sup(): 146 | spiner = asyncio.Task(spin()) 147 | print("spiner:",spiner) 148 | r = await slow_f() 149 | spiner.cancel() 150 | return r 151 | 152 | def debug(func): 153 | def wrapper(*args,**kwargs): 154 | print('[DEBUG]:enter {}()'.format(func.__name__)) 155 | r = func(*args,**kwargs) 156 | print(r) 157 | return r + 1 158 | return wrapper 159 | 160 | @debug 161 | def say_hello(): 162 | print('hello!') 163 | return 1 164 | @debug 165 | def say_goodbye(): 166 | print('hello!') 167 | return 2 168 | 169 | def main(): 170 | print(say_hello()) 171 | print(say_goodbye()) 172 | # wrapper = log(test) 173 | # wrapper('i m a man') 174 | # loop = asyncio.get_event_loop() 175 | # r = loop.run_until_complete(sup()) 176 | # loop.close() 177 | # print('r:',r) 178 | 179 | 180 | 181 | 182 | return 183 | 184 | import os 185 | print(os.path.dirname(os.path.realpath(__file__))) 186 | print(calculate_anniversary()) 187 | print(lunar.festival_handle(SOLAR_ANNIVERSARY,6,20)) 188 | stFtv = [ 189 | "0150#世界防治麻风病日#", #一月的最后一个星期日(月倒数第一个星期日) 190 | "0520#母亲节#", 191 | "0530#全国助残日#", 192 | "0630#父亲节#", 193 | "0730#被奴役国家周#", 194 | "0932#国际和平日#", 195 | "0940#国际聋人节# #世界儿童日#", 196 | "0950#世界海事日#", 197 | "1011#国际住房日#", 198 | "1013#国际减轻自然灾害日(减灾日)#", 199 | "1144#感恩节#" 200 | ] 201 | toDict(stFtv) 202 | 203 | lunar.Festival._create_weekday_festival() 204 | 205 | def toDict(list): 206 | dict = {} 207 | for s in list: 208 | comps = s.split('#') 209 | fs = [] 210 | for comp in comps: 211 | comp = comp.strip() 212 | if comp and comp != comps[0]: 213 | fs.append(comp) 214 | dict[str(comps[0])] = fs 215 | print(dict) 216 | pass 217 | 218 | if __name__ == '__main__': 219 | main() 220 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "中国节假日日历", 3 | "domains": ["sensor"], 4 | "render_readme": true, 5 | "homeassistant": "2023.5.0", 6 | "country": ["CN"] 7 | } 8 | -------------------------------------------------------------------------------- /snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazysiri/chineseholiday/814564fefcdef18b41cde0de67720ac57f8f6642/snapshot.png -------------------------------------------------------------------------------- /snaptshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crazysiri/chineseholiday/814564fefcdef18b41cde0de67720ac57f8f6642/snaptshot_1.png --------------------------------------------------------------------------------