Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
Cerallin committed Aug 7, 2024
0 parents commit 77ae472
Show file tree
Hide file tree
Showing 17 changed files with 549 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"image": "homeassistant/home-assistant:latest",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.autopep8"
]
}
}
}
15 changes: 15 additions & 0 deletions .github/workflows/hacs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Validate HACS

on:
push:
pull_request:

jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
12 changes: 12 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Validate with hassfest

on:
push:
pull_request:

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: home-assistant/actions/hassfest@master
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
custom_components/__pycache__
custom_components/mikan/__pycache__
tests/__pycache__

.pytest_cache
.mypy_cache
.ruff_cache
.coverage
.vscode/

test_hass/
37 changes: 37 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args:
- --pretty
- --show-error-codes
- --show-error-context
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Cerallin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## 蜜柑计划(Mikanani)的 homeassistant 集成

This is a hass integration for [Mikanani](https://mikanani.me).

### 功能

- 提供新番时间表传感器`sensor.bangumi_map`

键是一个一位十进制数字,代表星期N或者是剧场版。0-6 代表从周日开始的一周,7 代表剧场版。

0-6: 周日-周六
7 : 剧场版

### 安装

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)

打开HACS设置并添加本repo (https://github.com/Cerallin/hass-rennigou) 为一个自定义集成(分类要选**Integration**

你也可以点击下方按钮一键安装:
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?category=Integration&repository=hass-rennigou&owner=Cerallin)

## 配置

`config.yaml`里添加一行
```yaml
mikanani: true
```
之后重启HA。
(目前没什么配置,后续再添加一些选项)
27 changes: 27 additions & 0 deletions custom_components/mikan/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.discovery import async_load_platform

from .const import DOMAIN, LOGGER, DEFAULT_SCAN_INTERVAL
from .coodinator import MikanCoordinator

async def async_setup(hass: HomeAssistant, config: ConfigType):
"""Set up the component."""
LOGGER.info("Ciallo~(∠・ω< ) Mikan-Ani")

coordinator = MikanCoordinator(hass)
await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[0] = coordinator

await async_load_platform(hass, 'sensor', DOMAIN, {
"coordinator": coordinator
}, {})

async def async_update_data(now):
await coordinator.async_request_refresh()

async_track_time_interval(hass, async_update_data, DEFAULT_SCAN_INTERVAL)

return True
11 changes: 11 additions & 0 deletions custom_components/mikan/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import logging
from datetime import timedelta

LOGGER = logging.getLogger(__package__)
DEFAULT_SCAN_INTERVAL = timedelta(hours=1)

DOMAIN = "mikanani"

ATTRIBUTION = "第三方蜜柑计划 ha 集成"

MIKAN_HOST = "https://mikanani.me"
51 changes: 51 additions & 0 deletions custom_components/mikan/coodinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Coordinator for mikanani."""

import aiohttp
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
DOMAIN,
MIKAN_HOST,
LOGGER,
DEFAULT_SCAN_INTERVAL,
)
from .mikan import MikanBangumi, MikanHTMLParser, MikanParseResult


class MikanCoordinator(DataUpdateCoordinator[MikanParseResult]):
"""Class to manage fetching mikanani data."""

config_entry: ConfigEntry

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=DEFAULT_SCAN_INTERVAL,
)

self._parser = MikanHTMLParser()

async def update_token(self):
pass

async def _async_request(self):
async with aiohttp.ClientSession() as session:
async with session.request("GET", MIKAN_HOST) as response:
return await response.text(encoding="UTF-8")

async def _async_update_data(self) -> MikanParseResult:
parser = MikanHTMLParser()

try:
html_text = await self._async_request()
parser.feed(html_text)
except Exception as err:
LOGGER.exception(err)
raise UpdateFailed(err) from err

return parser.parse_result
12 changes: 12 additions & 0 deletions custom_components/mikan/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"domain": "mikanani",
"name": "Mikanani",
"codeowners": ["@Cerallin"],
"dependencies": [],
"documentation": "https://github.com/Cerallin/hass-mikan/blob/master/README.md",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/Cerallin/hass-mikan/issues",
"requirements": [],
"version": "0.1.0"
}
114 changes: 114 additions & 0 deletions custom_components/mikan/mikan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from html.parser import HTMLParser
from dataclasses import dataclass
from enum import Enum

from .const import MIKAN_HOST

type MikanParseResult = dict[int | str, list[MikanBangumi]]


class ParseStates(Enum):
START = 0
DAILY_ENTRY = 1
BANGUMI_ENTRY = 2


@dataclass
class MikanBangumi:
id: int
title: str
link: str
image_link: str
subscribed: bool

def __init__(self) -> None:
self.id: int = 0
self.title: str = ""
self.link: str = ""
self.image_link: str = ""
self.subscribed: bool = False


class MikanHTMLParser(HTMLParser):
"""MikanHTMLParser: 解析 Mikanani 网页番剧信息。
Usage:
html_text = request_func(MIKAN_HOST)
parser = MikanHTMLParser()
parser.feed(html_text)
print(parser.parse_result)
使用简陋的有限状态机实现番剧信息的提取。
提取结果类型为 dict[str, list[MikanBangumi]],该字典的值是一个MikanBangumi列表,
键通常是一个一位十进制数字,代表星期N/剧场版。0-6代表从周日开始的一周,7代表剧场版。
0-6: 周日-周六
7 : 剧场版
"""

def __init__(self) -> None:
super().__init__()

self._bangumi_map : MikanParseResult = {}

self._state = ParseStates.START
self._week = ""

@property
def _bangumi(self) -> MikanBangumi:
"""当前正需记录信息的番剧"""
return self._bangumi_map[self._week][-1]

@property
def parse_result(self) -> MikanParseResult:
"""解析结果"""
return self._bangumi_map

def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
"""从标签解析信息"""

# 不需要考虑重 key 的问题,所以转成 dict
attributes = dict(attrs)

# 一周番剧列表从 div 开始
if tag == 'div':
# 如果 div 有 data-dayofweek 属性则设状态为 DAILY_ENTRY
if (week := attributes.get("data-dayofweek")) is not None:
self._state = ParseStates.DAILY_ENTRY
# 记录 week 编号
# 0-6 为周日-周六
# 7 为剧场版
self._week = int(week) if str.isdigit(week) else week
# 初始化本周番剧列表为空数组
if not self._bangumi_map.get(week):
self._bangumi_map[self._week] = []

# 番剧从 li 开始
if self._state == ParseStates.DAILY_ENTRY and tag == "li":
# 设状态为 BANGUMI_ENTRY
self._state = ParseStates.BANGUMI_ENTRY
# 初始化 MikanBangumi,之后填空
self._bangumi_map[self._week].append(MikanBangumi())

# 当状态为 BANGUMI_ENTRY,开始记录番剧信息
if self._state == ParseStates.BANGUMI_ENTRY:
if tag == "span":
# 封面图片链接
if (image_link := attributes.get("data-src")) is not None:
self._bangumi.image_link = image_link
# 是否已订阅
self._bangumi.subscribed = attributes.get("data-showsubscribed") == "true"
# bangumi id, 如果是数字才记录
if (id := attributes.get("data-bangumiid")) is not None and str.isdigit(id):
self._bangumi.id = int(id)
# 以防万一有多个a标签,要的是有title属性的a标签
elif tag == "a" and (title := attributes.get("title")) is not None:
# 番剧链接
if (link := attributes.get("href")) is not None:
self._bangumi.link = MIKAN_HOST + link
# 番剧标题
self._bangumi.title = title

def handle_endtag(self, tag):
# 当 li 闭合的时候结束 BANGUMI_ENTRY,回到 DAILY_ENTRY
if self._state == ParseStates.BANGUMI_ENTRY and tag == "li":
self._state = ParseStates.DAILY_ENTRY
Loading

0 comments on commit 77ae472

Please sign in to comment.