仅作记录
环境
- PS5 一台
- Yeelight 灯泡
- 开启开发者模式
- ASP.NET 6 Web API
步骤
相关代码 PS5-Yeelight-Toggle - GitHub
实现控制灯泡的 WebAPI 应用
PS5 与外界交互的方式很少,但是仍然可以使用浏览器,所以这里选择开发一个简单的 Web API,通过对特定 URI 发送 GET 请求来控制 Yeelight 灯泡
例如: http://{Address}/On、http://{Address}/Off、http://{Address}/Toggle
使用 ASP.NET 6 新建 WebApi,引入 YeelightAPI 依赖包,新建控制器 YeelightControl,内容如下
using Microsoft.AspNetCore.Mvc; using YeelightAPI; using YeelightWebApi.Models; namespace YeelightWebApi.Controllers { [ApiController] [Route("YC")] public class YeelightControl: ControllerBase { private readonly ILogger<YeelightControl> _logger; private readonly IConfiguration _configuration; private readonly DeviceGroup _devices = new(); public YeelightControl(ILogger<YeelightControl> logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; var ips = _configuration.GetSection("YeelightBulbIPs").Get<List<string>>(); ips.ForEach(ip => { _logger.LogDebug(ip); _devices.Add(new Device(ip)); }); _logger.LogDebug($"{_devices.Count}"); } [HttpGet("Toggle")] public async Task<APIResult> Toggle() { try { await _devices.Connect(); await _devices.Toggle(); return new APIResult() { ResultCode = "200", Message = "OK" }; } catch (Exception ex) { _logger.LogError(ex.Message); return new APIResult() { ResultCode = "500", Message = ex.Message }; } finally { _devices.Disconnect(); } } [HttpGet("On")] public async Task<APIResult> On() { try { await _devices.Connect(); await _devices.TurnOn(1000); return new APIResult() { ResultCode = "200", Message = "OK" }; } catch (Exception ex) { _logger.LogError(ex.Message); return new APIResult() { ResultCode = "500", Message = ex.Message }; } finally { _devices.Disconnect(); } } [HttpGet("Off")] public async Task<APIResult> Off() { try { await _devices.Connect(); await _devices.TurnOff(1000); return new APIResult() { ResultCode = "200", Message = "OK" }; } catch (Exception ex) { _logger.LogError(ex.Message); return new APIResult() { ResultCode = "500", Message = ex.Message }; } finally { _devices.Disconnect(); } } [HttpGet("Set/{brightness}")] public async Task<APIResult> Set(int brightness) { try { if (brightness < 0 || brightness > 100) throw new Exception("Invalid brightness"); await _devices.Connect(); await _devices.SetBrightness(value: brightness, smooth: 1000); return new APIResult() { ResultCode = $"200", Message = "OK" }; } catch (Exception ex) { _logger.LogError(ex.Message); return new APIResult() { ResultCode = "500", Message = ex.Message }; } finally { _devices.Disconnect(); } } } }
然后在 appsettings.json 中添加灯泡的 IP
{ //... "YeelightBulbIPs": [ "192.168.xxx.xxx", "192.168.xxx.xxx", ... ], "AllowedHosts": "*" }
这样就可以通过访问 http://{Address}/Toggle 来开关灯泡
DNS 劫持
与 PS4 不同, PS5 没有让用户直接使用的浏览器应用,但实际上 PS5 是内置了浏览器的,需要向聊天室/好友(Game Base)发送可以被识别为链接的域名(youtube.com、bilibili.com 均可以被识别),再点击消息打开浏览器
因此可以通过设置本地 DNS 劫持让 PS5 访问到 WebAPI:将可以被识别的域名解析到 WebAPI 的 IP 上
在路由器的 主机名/IP 绑定中设置域名的 IP 地址
向好友发送链接
修改代码
更改路由
从上图可以看出,只有纯粹的域名才可以被 PS 识别,其他诸如 xxxx.com/123 的链接均无法打开,如果不对上述代码进行修改,PS5 需要访问 bilibili.com/yc/toggle 才可以开关灯泡
去掉手动指定的控制器路由,然后修改 Toggle API 的路由,使访问者在访问根路由时就调用 Toggle 的代码
将控制器内的代码更改为
//... [ApiController] //[Route("YC")] public class YeelightControl: ControllerBase { //...
//HttpGet("Toggle") [HttpGet("~/")] public async Task<APIResult> Toggle() { try { await _devices.Connect(); await _devices.Toggle(); return new APIResult() { ResultCode = "200", Message = "OK" }; } catch (Exception ex) { _logger.LogError(ex.Message); return new APIResult() { ResultCode = "500", Message = ex.Message }; } finally { _devices.Disconnect(); } }
更改监听端口
由于 PS5 也无法识别任何带端口号的域名,所以需要让 WebAPI 监听到 80 端口
可以通过修改项目属性文件 launchSettings.json 来实现(这里只修改了 Kestrel 的监听端口,IIS、WSL 等同理)
{ //... "profiles": { "YeelightWebApi": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://0.0.0.0:80/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://0.0.0.0:80", // Here "dotnetRunMessages": true }, //... } }
设置访问频率限制
如果直接运行上述代码就会发现,PS5 访问域名后灯泡开了一下又关了,似乎访问了两次 /Toggle
猜测可能是 PS5 的浏览器会向指定域名连续发送两次请求,导致灯泡被 toggle 了两次,开关状态没变化
因此需要再次修改代码,引入 Rate Limit,不论 PS5 在一段时间内请求几次,WebAPI 只运行一次 Toggle 操作
这里使用 AspNetCoreRateLimit 实现访问频率限制,关于 AspNetCoreRateLimit 可以查看之前写过的 使用AspNetCoreRateLimit实现IP请求频率限制
引入 AspNetCoreRateLimit 包后,修改 Program.cs 代码
using AspNetCoreRateLimit; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // Configure Rate Limit builder.Services.AddOptions(); builder.Services.AddMemoryCache(); builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting")); builder.Services.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection("IpRateLimitPolicies")); builder.Services.AddInMemoryRateLimiting(); builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>(); //Rate Limit Ends builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // Enable Rate Limit app.UseIpRateLimiting(); app.MapControllers(); app.Run();
然后在 appsettings.json 中配置 Rate Limit
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "YeelightBulbIPs": [ "192.168.xxx.xxx", "192.168.xxx.xxx", //... ], "IpRateLimiting": { // Here "EnableEndpointRateLimiting": false, "StackBlockedRequests": false, "RealIpHeader": "X-Real-IP", "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, "IpWhitelist": [ "127.0.0.1", "::1/10" ], "EndpointWhitelist": [], "ClientWhitelist": [], "GeneralRules": [ { "Endpoint": "*", "Period": "3s", // 每 3 秒只能请求 1 次 "Limit": 1 } ] }, "IpRateLimitPolicies": { "IpRules": [ { "Ip": "192.168.0.0/24", "Rules": [ { "Endpoint": "*", "Period": "3s", "Limit": 1 } ] } ] }, "AllowedHosts": "*" }
这样就实现了 PS5 开关 Yeelight 灯泡