如何使用 PS5 开关 Yeelight 智能灯泡

仅作记录

环境

步骤

相关代码 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 灯泡

效果

PS5 控制 Yeelight 灯泡 效果 – Bilibili

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据