├── tuya-spring-boot-starter-sample ├── src │ ├── main │ │ ├── resources │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── tuya │ │ │ └── open │ │ │ └── spring │ │ │ └── boot │ │ │ └── sample │ │ │ ├── ability │ │ │ ├── model │ │ │ │ ├── IssueParam.java │ │ │ │ ├── Firmware.java │ │ │ │ ├── DeviceSpecification.java │ │ │ │ ├── Device.java │ │ │ │ ├── DeviceProperties.java │ │ │ │ └── DeviceDetail.java │ │ │ ├── api │ │ │ │ ├── DeviceConnector.java │ │ │ │ └── ThingConnector.java │ │ │ └── messaging │ │ │ │ ├── msg │ │ │ │ ├── DeviceOnlineMessage.java │ │ │ │ ├── DeviceOfflineMessage.java │ │ │ │ ├── DeviceNameUpdate.java │ │ │ │ └── DevicePropertyMessage.java │ │ │ │ └── TuyaMessageListener.java │ │ │ ├── TuyaSpringBootStarterSampleApplication.java │ │ │ ├── service │ │ │ ├── DeviceService.java │ │ │ └── SIService.java │ │ │ ├── web │ │ │ ├── DeviceController.java │ │ │ └── SIController.java │ │ │ └── config │ │ │ └── CustomOkHttpClient.java │ └── test │ │ └── java │ │ └── com │ │ └── tuya │ │ └── open │ │ └── spring │ │ └── boot │ │ └── sample │ │ ├── service │ │ └── SIServiceTest.java │ │ └── TuyaSpringBootStarterSampleApplicationTests.java ├── .gitignore └── pom.xml ├── tuya-spring-boot-starter ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ ├── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ └── spring.factories │ │ └── _tuya-default.properties │ │ └── java │ │ └── com │ │ └── tuya │ │ └── connector │ │ └── open │ │ └── spring │ │ └── boot │ │ └── TuyaAutoConfiguration.java └── pom.xml ├── tuya-api ├── src │ ├── test │ │ ├── java │ │ │ └── com │ │ │ │ └── tuya │ │ │ │ └── connector │ │ │ │ └── open │ │ │ │ └── api │ │ │ │ ├── SecurityInfo.java │ │ │ │ ├── abilities │ │ │ │ └── DeviceAbility.java │ │ │ │ ├── connectors │ │ │ │ └── device │ │ │ │ │ ├── IndustryDeviceConnector.java │ │ │ │ │ ├── DeviceConnector.java │ │ │ │ │ └── ConnectorTest.java │ │ │ │ ├── model │ │ │ │ └── Device.java │ │ │ │ ├── token │ │ │ │ └── TuyaTokenTest.java │ │ │ │ └── errorprocessor │ │ │ │ └── TokenValidErrorProcessorTest.java │ │ └── resources │ │ │ └── logback-test.xml │ └── main │ │ └── java │ │ └── com │ │ └── tuya │ │ └── connector │ │ └── open │ │ └── api │ │ ├── token │ │ ├── TokenConnector.java │ │ ├── TuyaToken.java │ │ └── TuyaTokenManager.java │ │ ├── model │ │ ├── PageResultWithTotal.java │ │ └── PageResult.java │ │ ├── context │ │ ├── TuyaContext.java │ │ └── TuyaContextManager.java │ │ ├── errorprocessor │ │ └── TokenInvalidErrorProcessor.java │ │ ├── config │ │ └── TuyaRegionConfig.java │ │ └── header │ │ └── TuyaHeaderProcessor.java └── pom.xml ├── tuya-messaging ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── tuya │ │ │ └── connector │ │ │ └── open │ │ │ └── messaging │ │ │ ├── SecurityInfo.java │ │ │ └── MessageTest.java │ └── main │ │ └── java │ │ └── com │ │ └── tuya │ │ └── connector │ │ └── open │ │ └── messaging │ │ ├── autoconfig │ │ ├── EnableDynamicMessaging.java │ │ ├── EnableMessaging.java │ │ ├── MessageScanRegister.java │ │ ├── MessageProperties.java │ │ ├── TuyaMessageDataSource.java │ │ ├── MessageAutoConfiguration.java │ │ └── DynamicMessageManager.java │ │ ├── event │ │ ├── UnknownMessage.java │ │ ├── OnlineMessage.java │ │ ├── OfflineMessage.java │ │ ├── RoomDeleteMessage.java │ │ ├── DeleteMessage.java │ │ ├── NameUpdateMessage.java │ │ ├── UserDeleteMessage.java │ │ ├── UserUpdateMessage.java │ │ ├── UserRegisterMessage.java │ │ ├── DpNameUpdateMessage.java │ │ ├── RoomSortMessage.java │ │ ├── HomeCreateMessage.java │ │ ├── RoomCreateMessage.java │ │ ├── RoomNameUodateMessage.java │ │ ├── BindUserMessage.java │ │ ├── UpgradeStatusMessage.java │ │ ├── ShareMessage.java │ │ ├── HomeUpdateMessage.java │ │ ├── HomeDeleteMessage.java │ │ ├── AutomationExternalActionMessage.java │ │ ├── DeviceDpCommandMessage.java │ │ ├── DeviceSignalMessage.java │ │ ├── StatusReportMessage.java │ │ ├── SceneExecuteMessage.java │ │ └── BaseTuyaMessage.java │ │ ├── SourceMessage.java │ │ ├── MessageFactory.java │ │ ├── AESBase64Utils.java │ │ ├── MessageRegister.java │ │ └── TuyaMessageDispatcher.java └── pom.xml ├── tuya-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── tuya │ │ └── connector │ │ └── open │ │ └── common │ │ ├── constant │ │ ├── EnvConstant.java │ │ └── TuyaRegion.java │ │ └── util │ │ └── Sha256Util.java └── pom.xml ├── .gitignore ├── .github └── workflows │ └── maven-deploy.yml ├── README_zh.md ├── README.md ├── pom.xml └── LICENSE /tuya-spring-boot-starter-sample/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | connector.ak= 2 | connector.sk= -------------------------------------------------------------------------------- /tuya-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | com.tuya.connector.open.spring.boot.TuyaAutoConfiguration -------------------------------------------------------------------------------- /tuya-spring-boot-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.tuya.connector.open.spring.boot.TuyaAutoConfiguration -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/SecurityInfo.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api; 2 | 3 | /** 4 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 5 | * @since 2021/3/26 11:19 上午 6 | */ 7 | public class SecurityInfo { 8 | public static final String AK = "*********"; 9 | public static final String SK = "*********"; 10 | } 11 | -------------------------------------------------------------------------------- /tuya-messaging/src/test/java/com/tuya/connector/open/messaging/SecurityInfo.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | /** 4 | *

TODO 5 | * 6 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 7 | * @since 2021/3/26 11:21 上午 8 | */ 9 | public class SecurityInfo { 10 | public static final String AK = "*****"; 11 | public static final String SK = "*****"; 12 | } 13 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter/src/main/resources/_tuya-default.properties: -------------------------------------------------------------------------------- 1 | connector.api.auto-refresh-token=true 2 | connector.api.auto-set-header=true 3 | connector.api.context-manager=com.tuya.connector.open.api.context.TuyaContextManager 4 | connector.api.token-manager=com.tuya.connector.open.api.token.TuyaTokenManager 5 | connector.api.header-processor=com.tuya.connector.open.api.header.TuyaHeaderProcessor -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/IssueParam.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | import java.util.Map; 8 | 9 | @Data 10 | public class IssueParam implements Serializable { 11 | @Serial 12 | private static final long serialVersionUID = -514297250121261947L; 13 | 14 | private Map properties; 15 | } 16 | -------------------------------------------------------------------------------- /tuya-common/src/main/java/com/tuya/connector/open/common/constant/EnvConstant.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.common.constant; 2 | 3 | /** 4 | * @Classname EnvConstant 5 | * @Description TODO 6 | * @Date 2021/4/2 7 | * @Author 哲也(张梓濠 zheye.zhang@tuya.com) 8 | */ 9 | public class EnvConstant { 10 | 11 | public static final String ENV_AK = "connector.ak"; 12 | 13 | public static final String ENV_SK = "connector.sk"; 14 | 15 | public static final String ENV_REGION = "connector.region"; 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tuya-api/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} %-5level [%thread] ${PID:- } %logger{39}.%method %line : %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tuya-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.tuya 8 | tuya-connector 9 | 1.5.4 10 | 11 | 12 | tuya-common 13 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/abilities/DeviceAbility.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.abilities; 2 | 3 | import com.tuya.connector.open.api.model.Device; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | *

TODO 9 | * 10 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 11 | * @since 2021/2/18 10:52 上午 12 | */ 13 | public interface DeviceAbility { 14 | 15 | Device getById(String deviceId); 16 | 17 | Boolean commands(String deviceId, Map commands); 18 | 19 | Object specification(String deviceId); 20 | } 21 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/EnableDynamicMessaging.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.TYPE) 12 | @Import(DynamicMessageManager.class) 13 | public @interface EnableDynamicMessaging { 14 | } 15 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/Firmware.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | 8 | @Data 9 | public class Firmware implements Serializable { 10 | @Serial 11 | private static final long serialVersionUID = 6845359510017172104L; 12 | 13 | private Integer type; 14 | private String typeDesc; 15 | private String currentVersion; 16 | private Long lastUpgradeTime; 17 | } 18 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ompiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea/ 26 | 27 | *.iml 28 | 29 | target/ 30 | 31 | .mvn/ 32 | mvn* 33 | *.flattened-pom.xml 34 | 35 | # 忽略 sample 模块的 application.properties 36 | /tuya-spring-boot-starter-sample/src/main/resources/application.properties -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/EnableMessaging.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import org.springframework.context.annotation.Import; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.TYPE) 12 | @Import({MessageScanRegister.class, MessageAutoConfiguration.class}) 13 | public @interface EnableMessaging { 14 | String[] msgPaths() default {}; 15 | } 16 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/token/TokenConnector.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.token; 2 | 3 | import com.tuya.connector.api.annotations.GET; 4 | import com.tuya.connector.api.annotations.Path; 5 | import com.tuya.connector.api.annotations.Query; 6 | 7 | /** 8 | * 9 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 10 | * @since 2021/2/4 5:53 下午 11 | */ 12 | public interface TokenConnector { 13 | 14 | @GET("/v1.0/token") 15 | TuyaToken getToken(@Query("grant_type") int grantType); 16 | 17 | @GET("/v1.0/token/{refresh_token}") 18 | TuyaToken refreshToken(@Path("refresh_token") String refreshToken); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/model/PageResultWithTotal.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * Description the page result with total records 10 | * 11 | * @author Medivh.chen@tuya.com 12 | * @date 2021/4/20 13 | */ 14 | @Data 15 | public class PageResultWithTotal implements Serializable { 16 | 17 | 18 | /** 19 | * returned data 20 | */ 21 | private List list; 22 | 23 | /** 24 | * whether has next page 25 | */ 26 | private Boolean hasMore; 27 | 28 | /** 29 | * the total number of records 30 | */ 31 | private int total; 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/maven-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Maven Deploy 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | - release 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Java Environment 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'adopt' 16 | java-version: '8' 17 | server-id: maven 18 | server-username: MAVEN_USERNAME 19 | server-password: MAVEN_CENTRAL_TOKEN 20 | - name: Maven Repository Publish 21 | run: mvn -e clean deploy -Dmaven.test.skip=true 22 | env: 23 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 24 | MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_PASSWORD }} -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/UnknownMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * Description: TODO 8 | * 9 | * @author Chyern 10 | * @since 2021/9/17 11 | */ 12 | public class UnknownMessage extends BaseTuyaMessage { 13 | 14 | private JSONObject messageBody; 15 | 16 | @Override 17 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 18 | super.defaultBuild(sourceMessage, messageBody); 19 | this.messageBody = messageBody; 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "unknown"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/connectors/device/IndustryDeviceConnector.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.connectors.device; 2 | 3 | import com.tuya.connector.api.annotations.Body; 4 | import com.tuya.connector.api.annotations.GET; 5 | import com.tuya.connector.api.annotations.POST; 6 | import com.tuya.connector.api.annotations.Path; 7 | import com.tuya.connector.open.api.abilities.DeviceAbility; 8 | import com.tuya.connector.open.api.model.Device; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public interface IndustryDeviceConnector { 14 | /** 15 | * 设备指令下发 16 | */ 17 | @POST("/v1.0/iot-03/devices/{deviceId}/commands") 18 | Boolean sendCommands(@Path("deviceId") String deviceId, @Body Object commands); 19 | } 20 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/TuyaSpringBootStarterSampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample; 2 | 3 | import com.tuya.connector.spring.annotations.ConnectorScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @ConnectorScan(basePackages = "com.tuya.open.spring.boot.sample.ability.api") 8 | //@EnableMessaging(msgPaths = {"com.tuya.open.spring.boot.sample.ability.messaging.msg"}) 9 | @SpringBootApplication 10 | public class TuyaSpringBootStarterSampleApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(TuyaSpringBootStarterSampleApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/api/DeviceConnector.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.api; 2 | 3 | import com.tuya.connector.api.annotations.Body; 4 | import com.tuya.connector.api.annotations.GET; 5 | import com.tuya.connector.api.annotations.POST; 6 | import com.tuya.connector.api.annotations.Path; 7 | import com.tuya.open.spring.boot.sample.ability.model.Device; 8 | 9 | import java.util.Map; 10 | 11 | public interface DeviceConnector { 12 | 13 | @GET("/v1.1/iot-03/devices/{device_id}") 14 | Device getById(@Path("device_id") String deviceId); 15 | 16 | @POST("/v1.0/iot-03/devices/{device_id}/commands") 17 | Boolean command(@Path("device_id") String deviceId, @Body Map commands); 18 | } 19 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/OnlineMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | *

TODO 8 | * 9 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 10 | * @since 2021/3/24 3:34 下午 11 | */ 12 | public class OnlineMessage extends BaseTuyaMessage { 13 | 14 | public static final String UID = "uid"; 15 | public static final String TIME = "time"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "online"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/OfflineMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | *

TODO 8 | * 9 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 10 | * @since 2021/3/24 3:34 下午 11 | */ 12 | public class OfflineMessage extends BaseTuyaMessage { 13 | 14 | public static final String UID = "uid"; 15 | public static final String TIME = "time"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "offline"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/RoomDeleteMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 房间删除 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 23:01 10 | **/ 11 | public class RoomDeleteMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String TIME = "time"; 15 | 16 | @Override 17 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 18 | super.defaultBuild(sourceMessage, messageBody); 19 | } 20 | 21 | @Override 22 | public String type() { 23 | return "roomDelete"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/model/PageResult.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | import java.util.List; 7 | 8 | /** 9 | * Description TODO 10 | * 11 | * @author Chyern 12 | * @date 2021/3/26 13 | */ 14 | @Data 15 | public class PageResult implements Serializable { 16 | 17 | private static final long serialVersionUID = -1379646557144992944L; 18 | 19 | /** 20 | * returned data 21 | */ 22 | private List list; 23 | 24 | /** 25 | * the next page is exist or not 26 | */ 27 | private Boolean hasNext; 28 | 29 | /** 30 | * the last asset id where in this page 31 | */ 32 | private String lastRowKey; 33 | 34 | /** 35 | * the size of page 36 | */ 37 | private String pageSize; 38 | } 39 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/DeleteMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 设备解绑(设备删除) 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 21:59 10 | **/ 11 | public class DeleteMessage extends BaseTuyaMessage { 12 | public static final String DEV_ID = "devId"; 13 | public static final String UID = "uid"; 14 | public static final String OWNER_ID = "ownerId"; 15 | 16 | @Override 17 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 18 | super.defaultBuild(sourceMessage, messageBody); 19 | } 20 | 21 | @Override 22 | public String type() { 23 | return "delete"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/NameUpdateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 设备改名称 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 21:46 10 | **/ 11 | public class NameUpdateMessage extends BaseTuyaMessage { 12 | 13 | public static final String DEV_ID = "devId"; 14 | public static final String UID = "uid"; 15 | public static final String NAME = "name"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "nameUpdate"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/UserDeleteMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 用户注销 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:31 10 | **/ 11 | public class UserDeleteMessage extends BaseTuyaMessage { 12 | 13 | public static final String UID = "uid"; 14 | public static final String SCHEMA = "schema"; 15 | public static final String TIME = "time"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "userDelete"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/UserUpdateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 用户更新 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:31 10 | **/ 11 | public class UserUpdateMessage extends BaseTuyaMessage { 12 | 13 | public static final String UID = "uid"; 14 | public static final String SCHEMA = "schema"; 15 | public static final String TIME = "time"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "userUpdate"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/UserRegisterMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 用户注册 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:30 10 | **/ 11 | public class UserRegisterMessage extends BaseTuyaMessage { 12 | 13 | public static final String UID = "uid"; 14 | public static final String SCHEMA = "schema"; 15 | public static final String TIME = "time"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "userRegister"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/DpNameUpdateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 修改设备功能点名称 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 21:46 10 | **/ 11 | public class DpNameUpdateMessage extends BaseTuyaMessage { 12 | 13 | public static final String DEV_ID = "devId"; 14 | public static final String DP_ID = "dpId"; 15 | public static final String NAME = "name"; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | } 21 | 22 | @Override 23 | public String type() { 24 | return "dpNameUpdate"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/model/Device.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.model; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | @Data 10 | @Builder 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | @FieldDefaults(level = AccessLevel.PRIVATE) 14 | public class Device { 15 | Long active_time; 16 | int biz_type; 17 | String category; 18 | Long create_time; 19 | String icon; 20 | String id; 21 | String ip; 22 | String local_key; 23 | String model; 24 | String name; 25 | Boolean online; 26 | String owner_id; 27 | String product_id; 28 | String product_name; 29 | List> status; 30 | Boolean sub; 31 | String time_zone; 32 | String uid; 33 | Long update_time; 34 | String uuid; 35 | } 36 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/RoomSortMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 房间排序 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 23:07 10 | **/ 11 | public class RoomSortMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String ROOMS = "rooms"; 15 | public static final String UID = "uid"; 16 | public static final String TIME = "time"; 17 | 18 | @Override 19 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 20 | super.defaultBuild(sourceMessage, messageBody); 21 | } 22 | 23 | @Override 24 | public String type() { 25 | return "roomSort"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/context/TuyaContext.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.context; 2 | 3 | import com.tuya.connector.api.config.ApiDataSource; 4 | import com.tuya.connector.api.context.Context; 5 | 6 | /** 7 | *

TODO 8 | * 9 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 10 | * @since 2021/2/6 11:22 上午 11 | */ 12 | public class TuyaContext implements Context { 13 | 14 | private ApiDataSource apiDataSource; 15 | private String lang; 16 | 17 | public void setApiDataSource(ApiDataSource apiDataSource) { 18 | this.apiDataSource = apiDataSource; 19 | } 20 | 21 | public void setLang(String lang) { 22 | this.lang = lang; 23 | } 24 | 25 | @Override 26 | public ApiDataSource getApiDataSource() { 27 | return apiDataSource; 28 | } 29 | 30 | @Override 31 | public String getLang() { 32 | return lang; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/HomeCreateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 家庭创建 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:41 10 | **/ 11 | public class HomeCreateMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String HOME_NAME = "homeName"; 15 | public static final String UID = "uid"; 16 | public static final String TIME = "time"; 17 | 18 | @Override 19 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 20 | super.defaultBuild(sourceMessage, messageBody); 21 | } 22 | 23 | @Override 24 | public String type() { 25 | return "homeCreate"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/RoomCreateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 房间创建 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:57 10 | **/ 11 | public class RoomCreateMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String ROOM_NAME = "roomName"; 15 | public static final String UID = "uid"; 16 | public static final String TIME = "time"; 17 | 18 | @Override 19 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 20 | super.defaultBuild(sourceMessage, messageBody); 21 | } 22 | 23 | @Override 24 | public String type() { 25 | return "roomCreate"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/service/DeviceService.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.service; 2 | 3 | import com.tuya.open.spring.boot.sample.ability.api.DeviceConnector; 4 | import com.tuya.open.spring.boot.sample.ability.model.Device; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @SuppressWarnings("all") 12 | @Service 13 | public class DeviceService { 14 | @Autowired 15 | DeviceConnector deviceConnector; 16 | 17 | public Device getById(String deviceId) { 18 | return deviceConnector.getById(deviceId); 19 | } 20 | 21 | public Boolean command(String deviceId, List> commands) { 22 | return deviceConnector.command(deviceId, Map.of("commands", commands)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/RoomNameUodateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 房间更名 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 23:05 10 | **/ 11 | public class RoomNameUodateMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String ROOM_NAME = "roomName"; 15 | public static final String UID = "uid"; 16 | public static final String TIME = "time"; 17 | 18 | @Override 19 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 20 | super.defaultBuild(sourceMessage, messageBody); 21 | } 22 | 23 | @Override 24 | public String type() { 25 | return "roomNameUpdate"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/BindUserMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 设备绑定 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:02 10 | **/ 11 | public class BindUserMessage extends BaseTuyaMessage { 12 | 13 | public static final String DEV_ID = "devId"; 14 | public static final String UID = "uid"; 15 | public static final String OWNER_ID = "ownerId"; 16 | public static final String UUID = "uuid"; 17 | public static final String TOKEN = "token"; 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | } 23 | 24 | @Override 25 | public String type() { 26 | return "bindUser"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/UpgradeStatusMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 设备升级状态 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:09 10 | **/ 11 | public class UpgradeStatusMessage extends BaseTuyaMessage { 12 | 13 | public static final String DEV_ID = "devId"; 14 | public static final String UPGRADE_STATUS = "upgradeStatus"; 15 | public static final String DESCRIPTION = "description"; 16 | public static final String MODULE_TYPE = "moduleType"; 17 | 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | } 23 | 24 | @Override 25 | public String type() { 26 | return "upgradeStatus"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/token/TuyaToken.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.token; 2 | 3 | import com.tuya.connector.api.token.Token; 4 | import lombok.*; 5 | import lombok.experimental.FieldDefaults; 6 | 7 | /** 8 | *

TODO 9 | * 10 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 11 | * @since 2021/2/5 4:23 下午 12 | */ 13 | @Data 14 | @Builder 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE) 18 | public class TuyaToken implements Token { 19 | 20 | String access_token; 21 | /** 22 | * time unit second 23 | * */ 24 | Long expire_time; 25 | String refresh_token; 26 | String uid; 27 | /** 28 | * time unit second 29 | * */ 30 | Long expire_at; 31 | 32 | @Override 33 | public String getAccessToken() { 34 | return access_token; 35 | } 36 | 37 | @Override 38 | public String getRefreshToken() { 39 | return refresh_token; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/ShareMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 设备分享 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:14 10 | **/ 11 | public class ShareMessage extends BaseTuyaMessage { 12 | 13 | public static final String RECEIVER = "receiver"; 14 | public static final String SHARE = "share"; 15 | public static final String GROUP_ID = "groupId"; 16 | public static final String ADD_DEV_IDS = "addDevIds"; 17 | public static final String REMOVE_DDEV_IDS = "removedDevIds"; 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | } 23 | 24 | @Override 25 | public String type() { 26 | return "share"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/HomeUpdateMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 家庭更新 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:46 10 | **/ 11 | public class HomeUpdateMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String HOME_NAME = "homeName"; 15 | public static final String UID = "uid"; 16 | public static final String TIME = "time"; 17 | public static final String LON = "lon"; 18 | public static final String LAT = "lat"; 19 | 20 | @Override 21 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 22 | super.defaultBuild(sourceMessage, messageBody); 23 | } 24 | 25 | @Override 26 | public String type() { 27 | return "homeUpdate"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/HomeDeleteMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 家庭删除 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-03-24 22:50 10 | **/ 11 | public class HomeDeleteMessage extends BaseTuyaMessage { 12 | 13 | public static final String HOME_ID = "homeId"; 14 | public static final String HOME_NAME = "homeName"; 15 | public static final String TIME = "time"; 16 | public static final String MEMBER_IDS = "memberIds"; 17 | public static final String DEVICES = "devices"; 18 | public static final String REASON = "reason"; 19 | 20 | @Override 21 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 22 | super.defaultBuild(sourceMessage, messageBody); 23 | } 24 | 25 | @Override 26 | public String type() { 27 | return "homeDelete"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/web/DeviceController.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.web; 2 | 3 | import com.tuya.open.spring.boot.sample.ability.model.Device; 4 | import com.tuya.open.spring.boot.sample.service.DeviceService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @RestController 12 | @RequestMapping("/devices") 13 | public class DeviceController { 14 | 15 | @Autowired 16 | DeviceService deviceService; 17 | 18 | @GetMapping("{device_id}") 19 | public Device getById(@PathVariable("device_id") String deviceId) { 20 | return deviceService.getById(deviceId); 21 | } 22 | 23 | @PostMapping("{device_id}/command") 24 | public Boolean command(@PathVariable("device_id") String deviceId, @RequestBody List> commands) { 25 | return deviceService.command(deviceId, commands); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/DeviceSpecification.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | @Data 12 | public class DeviceSpecification implements Serializable { 13 | @Serial 14 | private static final long serialVersionUID = -8870376569649350814L; 15 | 16 | private String category; 17 | private List functions; 18 | private List status; 19 | 20 | 21 | @Data 22 | @AllArgsConstructor 23 | @NoArgsConstructor 24 | public static class Item implements Serializable{ 25 | @Serial 26 | private static final long serialVersionUID = -7907633673276744529L; 27 | private String code; 28 | private String desc; 29 | private String name; 30 | private String type; 31 | private String values; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | tuya-connector 8 | com.tuya 9 | 1.5.4 10 | 11 | 12 | tuya-spring-boot-starter 13 | 14 | 15 | 16 | com.tuya 17 | connector-spring-boot-starter 18 | 19 | 20 | 21 | com.tuya 22 | tuya-api 23 | 24 | 25 | 26 | com.tuya 27 | tuya-messaging 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/Device.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.*; 4 | import lombok.experimental.FieldDefaults; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | @NoArgsConstructor 9 | @FieldDefaults(level = AccessLevel.PRIVATE) 10 | public class Device { 11 | String id; // 设备 ID 12 | String gatewayId; // 网关 ID, 非网关子设备时为空 13 | String nodeId; // 节点 ID, 非网关子设备时为空 14 | String uuid; // 设备 UUID 15 | String category; // 产品品类 16 | String categoryName; // 产品品类名称 17 | String name; // 设备名称 18 | String productId; // 产品 ID 19 | String productName; // 产品名称 20 | String localKey; // 密钥 21 | Boolean sub; // 是否为子设备 22 | String assetId; // 资产 ID 23 | String ownerId; // 家庭 ID 24 | String ip; // 设备 IP 25 | String lon; // 经度 26 | String lat; // 纬度 27 | String model; // 产品型号 28 | String timeZone; // 时区 29 | Long activeTime; // 激活时间 30 | Long updateTime; // 更新时间 31 | Long createTime; // 初次配网时间 32 | Boolean online; // 在线状态 33 | String icon; // 设备图标 34 | } 35 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/DeviceProperties.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.io.Serial; 9 | import java.io.Serializable; 10 | import java.util.List; 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Builder 16 | public class DeviceProperties implements Serializable { 17 | @Serial 18 | private static final long serialVersionUID = -8949241840263891680L; 19 | 20 | private List properties; 21 | 22 | 23 | @Data 24 | @AllArgsConstructor 25 | @NoArgsConstructor 26 | public static class Item implements Serializable { 27 | 28 | @Serial 29 | private static final long serialVersionUID = 4342920416129210078L; 30 | 31 | private String code; 32 | private String customName; 33 | private Integer dpId; 34 | private Long time; 35 | private String type; 36 | private Object value; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/messaging/msg/DeviceOnlineMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.messaging.msg; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | @Getter @Setter 10 | public class DeviceOnlineMessage extends BaseTuyaMessage { 11 | private String devId; 12 | private String productId; 13 | private String uid; 14 | private Long time; 15 | 16 | @Override 17 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 18 | super.defaultBuild(sourceMessage, messageBody); 19 | this.devId = messageBody.getJSONObject("bizData").getString("devId"); 20 | this.productId = messageBody.getJSONObject("bizData").getString("productId"); 21 | this.uid = messageBody.getJSONObject("bizData").getString("uid"); 22 | this.time = messageBody.getJSONObject("bizData").getLong("time"); 23 | } 24 | 25 | @Override 26 | public String type() { 27 | return "deviceOnline"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/messaging/msg/DeviceOfflineMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.messaging.msg; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | 9 | @Getter @Setter 10 | public class DeviceOfflineMessage extends BaseTuyaMessage { 11 | private String devId; 12 | private String productId; 13 | private String uid; 14 | private Long time; 15 | 16 | @Override 17 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 18 | super.defaultBuild(sourceMessage, messageBody); 19 | this.devId = messageBody.getJSONObject("bizData").getString("devId"); 20 | this.productId = messageBody.getJSONObject("bizData").getString("productId"); 21 | this.uid = messageBody.getJSONObject("bizData").getString("uid"); 22 | this.time = messageBody.getJSONObject("bizData").getLong("time"); 23 | } 24 | 25 | @Override 26 | public String type() { 27 | return "deviceOffline"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/AutomationExternalActionMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | 6 | /** 7 | * @description: 自动化外部动作 8 | * @author: jinyun.zhou@tuya.com 9 | * @create: 2021-04-01 15:50 10 | **/ 11 | public class AutomationExternalActionMessage extends BaseTuyaMessage { 12 | 13 | public static final String CODE = "code"; 14 | public static final String VALUE = "value"; 15 | public static final String OPERATOR = "operator"; 16 | 17 | private String automationId; 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | this.automationId = messageBody.getString("automationId"); 23 | } 24 | 25 | @Override 26 | public String type() { 27 | return "automationExternalAction"; 28 | } 29 | 30 | public String getAutomationId() { 31 | return automationId; 32 | } 33 | 34 | public void setAutomationId(String automationId) { 35 | this.automationId = automationId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/MessageScanRegister.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import com.tuya.connector.open.messaging.MessageRegister; 4 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 5 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 6 | import org.springframework.core.annotation.AnnotationAttributes; 7 | import org.springframework.core.type.AnnotationMetadata; 8 | 9 | import java.util.Arrays; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | public class MessageScanRegister implements ImportBeanDefinitionRegistrar { 14 | @Override 15 | public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { 16 | AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(EnableMessaging.class.getName())); 17 | Set pkgPaths = new HashSet<>(); 18 | if (attributes != null) { 19 | String[] paths = attributes.getStringArray("msgPaths"); 20 | pkgPaths.addAll(Arrays.asList(paths)); 21 | } 22 | MessageRegister.init(pkgPaths); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/messaging/msg/DeviceNameUpdate.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.messaging.msg; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.tuya.connector.open.messaging.SourceMessage; 5 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class DeviceNameUpdate extends BaseTuyaMessage { 10 | 11 | private String devId; 12 | private String productId; 13 | private String uid; 14 | private String name; 15 | private String uuid; 16 | 17 | @Override 18 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 19 | super.defaultBuild(sourceMessage, messageBody); 20 | this.devId = messageBody.getJSONObject("bizData").getString("devId"); 21 | this.productId = messageBody.getJSONObject("bizData").getString("productId"); 22 | this.uid = messageBody.getJSONObject("bizData").getString("uid"); 23 | this.name = messageBody.getJSONObject("bizData").getString("name"); 24 | this.uuid = messageBody.getJSONObject("bizData").getString("uuid"); 25 | } 26 | 27 | @Override 28 | public String type() { 29 | return "deviceNameUpdate"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter/src/main/java/com/tuya/connector/open/spring/boot/TuyaAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.spring.boot; 2 | 3 | 4 | import com.tuya.connector.open.api.config.TuyaRegionConfig; 5 | import com.tuya.connector.open.api.errorprocessor.TokenInvalidErrorProcessor; 6 | import com.tuya.connector.spring.annotations.ConnectorScan; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.PropertySource; 13 | 14 | /** 15 | *

TODO 16 | * 17 | * @author qiufeng.yu@tuya.com 18 | * @since 2021/1/25 5:51 下午 19 | */ 20 | 21 | @Slf4j 22 | @Configuration 23 | @ImportAutoConfiguration(TuyaRegionConfig.class) 24 | @PropertySource("classpath:/_tuya-default.properties") 25 | @ConnectorScan(basePackages = {"com.tuya.connector.open.ability"}) 26 | public class TuyaAutoConfiguration { 27 | 28 | @Bean 29 | @ConditionalOnMissingBean 30 | public TokenInvalidErrorProcessor tokenInvalidErrorProcessor() { 31 | return new TokenInvalidErrorProcessor(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/connectors/device/DeviceConnector.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.connectors.device; 2 | 3 | import com.tuya.connector.api.annotations.Body; 4 | import com.tuya.connector.api.annotations.GET; 5 | import com.tuya.connector.api.annotations.POST; 6 | import com.tuya.connector.api.annotations.Path; 7 | import com.tuya.connector.open.api.abilities.DeviceAbility; 8 | import com.tuya.connector.open.api.model.Device; 9 | 10 | import java.util.Map; 11 | 12 | /** 13 | *

TODO 14 | * 15 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 16 | * @since 2021/2/4 5:53 下午 17 | */ 18 | public interface DeviceConnector extends DeviceAbility { 19 | /** 20 | * 设备信息查询 21 | * @param deviceId 22 | * @return 23 | */ 24 | @Override 25 | @GET("/v1.1/iot-03/devices/{device_id}") 26 | Device getById(@Path("device_id") String deviceId); 27 | 28 | /** 29 | * 设备指令下发 30 | * @param deviceId 31 | * @param commands 32 | * @return 33 | */ 34 | @Override 35 | @POST("/v1.0/devices/{device_id}/commands") 36 | Boolean commands(@Path("device_id") String deviceId, @Body Map commands); 37 | 38 | @Override 39 | @GET("/v1.2/iot-03/devices/{device_id}/specification") 40 | Object specification(@Path("device_id") String deviceId); 41 | } 42 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/context/TuyaContextManager.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.context; 2 | 3 | import com.tuya.connector.api.config.Configuration; 4 | import com.tuya.connector.api.context.ContextManager; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | /** 8 | *

TODO 9 | * 10 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 11 | * @since 2021/2/6 11:22 上午 12 | */ 13 | @Slf4j 14 | public class TuyaContextManager implements ContextManager { 15 | private static final ThreadLocal ctx = new InheritableThreadLocal<>(); 16 | 17 | private final Configuration configuration; 18 | public TuyaContextManager(Configuration configuration) { 19 | this.configuration = configuration; 20 | } 21 | 22 | @Override 23 | public TuyaContext start() { 24 | TuyaContext tuyaCtx = new TuyaContext(); 25 | tuyaCtx.setApiDataSource(configuration.getApiDataSource()); 26 | tuyaCtx.setLang("zh");// TODO 27 | ctx.set(tuyaCtx); 28 | return tuyaCtx; 29 | } 30 | 31 | @Override 32 | public void clear() { 33 | ctx.remove(); 34 | } 35 | 36 | @Override 37 | public TuyaContext get() { 38 | return ctx.get(); 39 | } 40 | 41 | @Override 42 | public Configuration getConfiguration() { 43 | return configuration; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/errorprocessor/TokenInvalidErrorProcessor.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.errorprocessor; 2 | 3 | import com.tuya.connector.api.context.Context; 4 | import com.tuya.connector.api.error.ErrorInfo; 5 | import com.tuya.connector.api.error.ErrorProcessor; 6 | import com.tuya.connector.api.exceptions.ConnectorException; 7 | import com.tuya.connector.api.plugin.Invocation; 8 | import com.tuya.connector.api.token.TokenManager; 9 | import lombok.SneakyThrows; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | /** 13 | *

TODO 14 | * 15 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 16 | * @since 2021/2/5 11:07 上午 17 | */ 18 | @Slf4j 19 | public class TokenInvalidErrorProcessor implements ErrorProcessor { 20 | 21 | @Override 22 | @SneakyThrows 23 | public Object process(ErrorInfo errorInfo, Invocation invocation, Context context) { 24 | log.warn("token error processor : {}", errorInfo); 25 | if (context.getApiDataSource().isAutoRefreshToken()) { 26 | log.warn("token invalid error processor: refresh token and auto retry call request."); 27 | TokenManager tokenManager = context.getApiDataSource().getTokenManager(); 28 | tokenManager.refreshToken(); 29 | return invocation.proceed(); 30 | } 31 | throw new ConnectorException(errorInfo.toString()); 32 | } 33 | 34 | @Override 35 | public String getErrorCode() { 36 | return "1010"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/model/DeviceDetail.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serial; 6 | import java.io.Serializable; 7 | 8 | @Data 9 | public class DeviceDetail implements Serializable { 10 | 11 | @Serial 12 | private static final long serialVersionUID = -1389103173186462562L; 13 | 14 | private String id; // 设备 ID 15 | private Long activeTime; // 设备的激活时间(时间戳秒数) 16 | private String category; // 设备的产品品类 17 | private Long createTime; // 设备的初次配网时间(时间戳秒数) 18 | private Long updateTime; // 设备的更新时间(时间戳秒数) 19 | private String customName; // 设备的自定义名称 20 | private String icon; // 设备的图标 21 | private String ip; // 设备的 IP 地址 22 | private Boolean isOnline; // 设备的在线状态 23 | private Boolean online; // 设备的在线状态 24 | private String lat; // 设备的纬度 25 | private String localKey; // 设备的局域网加密后的唯一密钥 26 | private String lon; // 设备的经度 27 | private String name; // 设备的名称 28 | private String productId; // 设备的产品 ID 29 | private String productName; // 设备的产品名称 30 | private Boolean sub; // 是否为子设备 31 | private String timeZone; // 设备的时区 32 | private String uuid; // 设备的 UUID 33 | private String bindSpaceId; // 空间 Id 34 | private String gatewayId; // 网关 ID 35 | private String nodeId; // 设备的节点 ID 36 | private String assetId; // 设备的资产 ID 37 | private String model; // 设备的型号 38 | private String sn; // 设备的序列号 39 | 40 | } 41 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/config/CustomOkHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.config; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import lombok.extern.slf4j.Slf4j; 5 | import okhttp3.Dispatcher; 6 | import okhttp3.OkHttpClient; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.DependsOn; 12 | 13 | @Slf4j 14 | @Configuration 15 | public class CustomOkHttpClient implements ApplicationContextAware { 16 | private static ApplicationContext ctx; 17 | 18 | 19 | @PostConstruct 20 | @DependsOn("tuyaConfiguration") 21 | public void customSetOkHttpClient() { 22 | log.info("自定义OkHttpClient..."); 23 | com.tuya.connector.api.config.Configuration configuration = ctx.getBean(com.tuya.connector.api.config.Configuration.class); 24 | Dispatcher dispatcher = new Dispatcher(); 25 | dispatcher.setMaxRequestsPerHost(20); 26 | OkHttpClient myOkHttpClient = new OkHttpClient.Builder().dispatcher(dispatcher).build(); 27 | configuration.getApiDataSource().setSpecificClient(myOkHttpClient); 28 | } 29 | 30 | @Override 31 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 32 | ctx = applicationContext; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tuya-common/src/main/java/com/tuya/connector/open/common/constant/TuyaRegion.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.common.constant; 2 | 3 | /** 4 | * @Classname Region 5 | * @Description Region Enum 6 | * @Date 2021/4/2 7 | * @Author 哲也 8 | */ 9 | 10 | public enum TuyaRegion { 11 | 12 | /** 13 | * China 14 | */ 15 | CN("https://openapi.tuyacn.com", "pulsar+ssl://mqe.tuyacn.com:7285/"), 16 | /** 17 | * US WEST 18 | */ 19 | US("https://openapi.tuyaus.com", "pulsar+ssl://mqe.tuyaus.com:7285/"), 20 | /** 21 | * US EAST 22 | */ 23 | US_EAST("https://openapi-ueaz.tuyaus.com", "pulsar+ssl://mqe.tuyaus.com:7285/"), 24 | /** 25 | * European 26 | */ 27 | EU("https://openapi.tuyaeu.com", "pulsar+ssl://mqe.tuyaeu.com:7285/"), 28 | /** 29 | * Europe West 30 | */ 31 | EU_WEST("https://openapi-weaz.tuyaeu.com", "pulsar+ssl://mqe.tuyaeu.com:7285/"), 32 | /** 33 | * India 34 | */ 35 | IN("https://openapi.tuyain.com", "pulsar+ssl://mqe.tuyain.com:7285/"), 36 | 37 | /** 38 | * Singapore 39 | */ 40 | SG("https://openapi-sg.iotbing.com", "pulsar+ssl://mqe-sg.iotbing.com:7285/"); 41 | 42 | private final String apiUrl; 43 | 44 | private final String msgUrl; 45 | 46 | TuyaRegion(String apiUrl, String msgUrl) { 47 | this.apiUrl = apiUrl; 48 | this.msgUrl = msgUrl; 49 | } 50 | 51 | public String getApiUrl() { 52 | return apiUrl; 53 | } 54 | 55 | public String getMsgUrl() { 56 | return msgUrl; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/DeviceDpCommandMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.SourceMessage; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | /** 11 | * @description: 设备控制 12 | * @author: jinyun.zhou@tuya.com 13 | * @create: 2021-03-24 23:09 14 | **/ 15 | public class DeviceDpCommandMessage extends BaseTuyaMessage { 16 | 17 | private List command; 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | JSONArray object = messageBody.getJSONArray("command"); 23 | if (Objects.nonNull(object)) { 24 | this.command = object.toJavaList(Item.class); 25 | } 26 | } 27 | 28 | @Override 29 | public String type() { 30 | return "deviceDpCommand"; 31 | } 32 | 33 | public List getCommand() { 34 | return command; 35 | } 36 | 37 | public void setCommand(List command) { 38 | this.command = command; 39 | } 40 | 41 | public static class Item { 42 | private Long dpId; 43 | private Boolean value; 44 | 45 | public Long getDpId() { 46 | return dpId; 47 | } 48 | 49 | public void setDpId(Long dpId) { 50 | this.dpId = dpId; 51 | } 52 | 53 | public Boolean getValue() { 54 | return value; 55 | } 56 | 57 | public void setValue(Boolean value) { 58 | this.value = value; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/MessageProperties.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import com.tuya.connector.open.common.constant.EnvConstant; 4 | import com.tuya.connector.open.common.constant.TuyaRegion; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.boot.context.properties.ConfigurationProperties; 9 | import org.springframework.context.EnvironmentAware; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.util.StringUtils; 12 | 13 | /** 14 | *

TODO 15 | * 16 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 17 | * @since 2021/1/25 5:52 下午 18 | */ 19 | 20 | @Data 21 | @NoArgsConstructor 22 | @AllArgsConstructor 23 | @ConfigurationProperties(prefix = MessageProperties.MESSAGE_PREFIX) 24 | public class MessageProperties implements EnvironmentAware { 25 | protected static final String MESSAGE_PREFIX = "connector.message"; 26 | 27 | String url; 28 | 29 | String ak; 30 | 31 | String sk; 32 | 33 | /** 34 | * pulsar 消息订阅名称后缀,订阅名称完整格式为:clientId-{subNameSuffix} 35 | * 默认值为:sub 36 | */ 37 | String subNameSuffix = "sub"; 38 | 39 | @Override 40 | public void setEnvironment(Environment env) { 41 | if (StringUtils.isEmpty(ak)) { 42 | this.ak = env.getProperty(EnvConstant.ENV_AK, ""); 43 | } 44 | 45 | if (StringUtils.isEmpty(sk)) { 46 | this.sk = env.getProperty(EnvConstant.ENV_SK, ""); 47 | } 48 | 49 | if (StringUtils.isEmpty(url)) { 50 | String region = env.getProperty(EnvConstant.ENV_REGION); 51 | this.url = StringUtils.isEmpty(region) ? TuyaRegion.CN.getMsgUrl() : TuyaRegion.valueOf(region).getMsgUrl(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/web/SIController.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.web; 2 | 3 | import com.tuya.open.spring.boot.sample.service.SIService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | public class SIController { 10 | 11 | @Autowired 12 | SIService sIService; 13 | 14 | @RequestMapping("/a") 15 | public void isDeviceExist(String deviceId) { 16 | sIService.isDeviceExist(deviceId); 17 | } 18 | 19 | @RequestMapping("/b") 20 | public void getDevice(String deviceId) { 21 | sIService.getDevice(deviceId); 22 | } 23 | 24 | @RequestMapping("/c") 25 | public void getFirmware(String deviceId) { 26 | sIService.getFirmware(deviceId); 27 | } 28 | 29 | @RequestMapping("/d") 30 | public void getExtProperties(String deviceId) { 31 | sIService.getExtProperties(deviceId); 32 | } 33 | 34 | @RequestMapping("/e") 35 | public void getDeviceSpecification(String deviceId) { 36 | sIService.getDeviceSpecification(deviceId); 37 | } 38 | 39 | @RequestMapping("/f") 40 | public void getDeviceModel(String deviceId) { 41 | sIService.getDeviceModel(deviceId); 42 | } 43 | 44 | @RequestMapping("/g") 45 | public void getDeviceProperties(String deviceId) { 46 | sIService.getDeviceProperties(deviceId); 47 | } 48 | 49 | @RequestMapping("/h") 50 | public void getDeviceState(String deviceId) { 51 | sIService.getDeviceState(deviceId); 52 | } 53 | 54 | @RequestMapping("/i") 55 | public void issue(String deviceId) { 56 | sIService.issueDeviceProperties(null, null); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/config/TuyaRegionConfig.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.config; 2 | 3 | import com.tuya.connector.api.config.Configuration; 4 | import com.tuya.connector.api.exceptions.ConnectorException; 5 | import com.tuya.connector.open.common.constant.EnvConstant; 6 | import com.tuya.connector.open.common.constant.TuyaRegion; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 9 | import org.springframework.context.EnvironmentAware; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.util.StringUtils; 12 | 13 | /** 14 | * @Classname TuyaRegionConfig 15 | * @Description TODO 16 | * @Date 2021/4/2 17 | * @Author 哲也(张梓濠 zheye.zhang@tuya.com) 18 | */ 19 | @AutoConfigureAfter(Configuration.class) 20 | public class TuyaRegionConfig implements EnvironmentAware, InitializingBean { 21 | 22 | private Configuration configuration; 23 | 24 | private Environment environment; 25 | 26 | public TuyaRegionConfig(Configuration configuration) { 27 | this.configuration = configuration; 28 | } 29 | 30 | @Override 31 | public void setEnvironment(Environment env) { 32 | this.environment = env; 33 | } 34 | 35 | @Override 36 | public void afterPropertiesSet() { 37 | if (StringUtils.isEmpty(configuration.getApiDataSource().getBaseUrl())) { 38 | String evnRegion = environment.getProperty(EnvConstant.ENV_REGION, TuyaRegion.CN.name()); 39 | try { 40 | TuyaRegion tuyaRegion = TuyaRegion.valueOf(evnRegion); 41 | configuration.getApiDataSource().setBaseUrl(tuyaRegion.getApiUrl()); 42 | } catch (IllegalArgumentException e) { 43 | throw new ConnectorException("Connector region must in legal scope!"); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/TuyaMessageDataSource.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import com.tuya.connector.messaging.MessageDataSource; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.apache.pulsar.client.api.SubscriptionType; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | public class TuyaMessageDataSource extends MessageDataSource { 13 | 14 | @Getter @Setter 15 | private String subNameSuffix; 16 | 17 | @Getter @Setter 18 | private Set pkgPaths; 19 | 20 | // pulsar client loadConf 21 | private Map clientLoadConfMap = new HashMap<>(); 22 | 23 | // pulsar consumer loadConf 24 | private Map consumerLoadConfMap = new HashMap<>(); 25 | 26 | 27 | public TuyaMessageDataSource(String url, String ak, String sk) { 28 | super(url, ak, sk); 29 | } 30 | 31 | public TuyaMessageDataSource(String url, String ak, String sk, String subNameSuffix) { 32 | super(url, ak, sk); 33 | this.subNameSuffix = subNameSuffix; 34 | } 35 | 36 | public Map clientLoadConf() { 37 | // Whether the Pulsar client accepts untrusted TLS certificate from broker 38 | clientLoadConfMap.put("tlsAllowInsecureConnection", true); 39 | 40 | return clientLoadConfMap; 41 | } 42 | 43 | public Map consumerLoadConf() { 44 | // Multiple consumers can attach to the same subscription, yet only the first consumer is active, and others are 45 | // standby. When the active consumer is disconnected, messages will be dispatched to one of standby consumers, 46 | // and the standby consumer then becomes active consumer. 47 | consumerLoadConfMap.put("subscriptionType", SubscriptionType.Failover); 48 | 49 | return consumerLoadConfMap; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/DeviceSignalMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.SourceMessage; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | /** 11 | * @description: 设备信号量 12 | * @author: jinyun.zhou@tuya.com 13 | * @create: 2021-03-24 22:20 14 | **/ 15 | public class DeviceSignalMessage extends BaseTuyaMessage { 16 | 17 | private List reportData; 18 | 19 | @Override 20 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 21 | super.defaultBuild(sourceMessage, messageBody); 22 | JSONArray statusArray = messageBody.getJSONArray("reportData"); 23 | if (Objects.nonNull(statusArray)) { 24 | this.reportData = statusArray.toJavaList(Item.class); 25 | } 26 | } 27 | 28 | @Override 29 | public String type() { 30 | return "deviceSignal"; 31 | } 32 | 33 | public List getReportData() { 34 | return reportData; 35 | } 36 | 37 | public void setReportData(List reportData) { 38 | this.reportData = reportData; 39 | } 40 | 41 | public static class Item { 42 | private Long memory; 43 | private Integer rssi; 44 | private Long t; 45 | 46 | public Long getMemory() { 47 | return memory; 48 | } 49 | 50 | public void setMemory(Long memory) { 51 | this.memory = memory; 52 | } 53 | 54 | public Integer getRssi() { 55 | return rssi; 56 | } 57 | 58 | public void setRssi(Integer rssi) { 59 | this.rssi = rssi; 60 | } 61 | 62 | public Long getT() { 63 | return t; 64 | } 65 | 66 | public void setT(Long t) { 67 | this.t = t; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/messaging/TuyaMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.messaging; 2 | 3 | import com.tuya.connector.open.messaging.event.NameUpdateMessage; 4 | import com.tuya.connector.open.messaging.event.StatusReportMessage; 5 | import com.tuya.open.spring.boot.sample.ability.messaging.msg.DeviceNameUpdate; 6 | import com.tuya.open.spring.boot.sample.ability.messaging.msg.DeviceOfflineMessage; 7 | import com.tuya.open.spring.boot.sample.ability.messaging.msg.DeviceOnlineMessage; 8 | import com.tuya.open.spring.boot.sample.ability.messaging.msg.DevicePropertyMessage; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.context.event.EventListener; 11 | import org.springframework.stereotype.Component; 12 | 13 | /** 14 | *

TODO 15 | * 16 | * @author qiufeng.yu@tuya.com 17 | * @since 2021/4/1 10:11 下午 18 | */ 19 | @Slf4j 20 | @Component 21 | public class TuyaMessageListener { 22 | 23 | // @EventListener 24 | public void updateStatusEvent(StatusReportMessage message) { 25 | log.info("StatusReport event happened: {}", message); 26 | } 27 | 28 | // @EventListener 29 | public void nameUpdateMessage(NameUpdateMessage message) { 30 | log.info("NameUpdate event happened: {}", message); 31 | } 32 | 33 | @EventListener 34 | public void deviceNameUpdateMsg(DeviceNameUpdate message) { 35 | log.info("deviceNameUpdateMsg event happened: {}", message); 36 | } 37 | 38 | @EventListener 39 | public void deviceOfflineMsg(DeviceOfflineMessage msg) { 40 | log.warn("pulsar msg, DeviceOfflineMessage:{}", msg); 41 | } 42 | 43 | @EventListener 44 | public void deviceOnlineMsg(DeviceOnlineMessage msg) { 45 | log.warn("pulsar msg, DeviceOnlineMessage:{}", msg); 46 | } 47 | 48 | @EventListener 49 | public void devicePropertyMsg(DevicePropertyMessage msg) { 50 | log.warn("pulsar msg, DevicePropertyMessage:{}", msg); 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tuya-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.tuya 8 | tuya-connector 9 | 1.5.4 10 | 11 | 12 | tuya-api 13 | 14 | 15 | 16 | com.tuya 17 | tuya-common 18 | 19 | 20 | 21 | com.tuya 22 | connector-api 23 | 24 | 25 | 26 | ch.qos.logback 27 | logback-classic 28 | 29 | 30 | 31 | ch.qos.logback 32 | logback-core 33 | 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | 39 | 40 | 41 | org.junit.jupiter 42 | junit-jupiter 43 | test 44 | 45 | 46 | 47 | com.alibaba 48 | fastjson 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-configuration-processor 54 | true 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-autoconfigure 60 | true 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/StatusReportMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.SourceMessage; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | /** 11 | *

TODO 12 | * 13 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 14 | * @since 2021/3/24 3:34 下午 15 | */ 16 | public class StatusReportMessage extends BaseTuyaMessage { 17 | private String dataId; 18 | private List status; 19 | 20 | @Override 21 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 22 | super.defaultBuild(sourceMessage, messageBody); 23 | this.dataId = messageBody.getString("dataId"); 24 | JSONArray statusArray = messageBody.getJSONArray("status"); 25 | if (Objects.nonNull(statusArray)) { 26 | this.status = statusArray.toJavaList(Item.class); 27 | } 28 | } 29 | 30 | @Override 31 | public String type() { 32 | return "statusReport"; 33 | } 34 | 35 | public static class Item { 36 | private String code; 37 | private Long t; 38 | private Object value; 39 | 40 | public String getCode() { 41 | return code; 42 | } 43 | 44 | public void setCode(String code) { 45 | this.code = code; 46 | } 47 | 48 | public Long getT() { 49 | return t; 50 | } 51 | 52 | public void setT(Long t) { 53 | this.t = t; 54 | } 55 | 56 | public Object getValue() { 57 | return value; 58 | } 59 | 60 | public void setValue(Object value) { 61 | this.value = value; 62 | } 63 | } 64 | 65 | public String getDataId() { 66 | return dataId; 67 | } 68 | 69 | public void setDataId(String dataId) { 70 | this.dataId = dataId; 71 | } 72 | 73 | public List getStatus() { 74 | return status; 75 | } 76 | 77 | public void setStatus(List status) { 78 | this.status = status; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/api/ThingConnector.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.api; 2 | 3 | import com.tuya.connector.api.annotations.Body; 4 | import com.tuya.connector.api.annotations.GET; 5 | import com.tuya.connector.api.annotations.POST; 6 | import com.tuya.connector.api.annotations.Path; 7 | import com.tuya.connector.api.model.Result; 8 | import com.tuya.open.spring.boot.sample.ability.model.DeviceDetail; 9 | import com.tuya.open.spring.boot.sample.ability.model.DeviceProperties; 10 | import com.tuya.open.spring.boot.sample.ability.model.DeviceSpecification; 11 | import com.tuya.open.spring.boot.sample.ability.model.Firmware; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public interface ThingConnector { 17 | @GET("/v2.0/cloud/thing/{device_id}") 18 | Result getDeviceResult(@Path("device_id") String deviceId); 19 | 20 | @GET("/v2.0/cloud/thing/{device_id}") 21 | DeviceDetail getDevice(@Path("device_id") String deviceId); 22 | 23 | @GET("/v1.1/iot-03/devices/{device_id}") 24 | DeviceDetail getIndustryDevice(@Path("device_id") String deviceId); 25 | 26 | @GET("/v2.0/cloud/thing/{device_id}/firmware") 27 | List getFirmware(@Path("device_id") String deviceId); 28 | 29 | @GET("/v1.0/iot-03/devices/{device_id}/properties") 30 | List> getDeviceExtProperties(@Path("device_id") String deviceId); 31 | 32 | @GET("/v1.0/iot-03/devices/{device_id}/specification") 33 | DeviceSpecification getDeviceSpecification(@Path("device_id") String deviceId); 34 | 35 | @GET("/v2.0/cloud/thing/{device_id}/model") 36 | Map getDeviceModel(@Path("device_id") String deviceId); 37 | 38 | @GET("/v2.0/cloud/thing/{device_id}/shadow/properties") 39 | DeviceProperties getDeviceProperties(@Path("device_id") String deviceId); 40 | 41 | @GET("/v2.0/cloud/thing/{device_id}/state") 42 | Map getDeviceState(@Path("device_id") String deviceId); 43 | 44 | @POST("/v2.0/cloud/thing/{device_id}/shadow/properties/issue") 45 | Result issueDeviceProperties(@Path("device_id") String deviceId, @Body Map param); 46 | } 47 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/MessageAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import com.tuya.connector.open.messaging.TuyaMessageDispatcher; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.util.StringUtils; 10 | 11 | /** 12 | *

TODO 13 | * 14 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 15 | * @since 2021/3/25 8:32 下午 16 | */ 17 | 18 | @Slf4j 19 | @Configuration 20 | @EnableConfigurationProperties(MessageProperties.class) 21 | public class MessageAutoConfiguration { 22 | 23 | private final MessageProperties messageProperties; 24 | 25 | public MessageAutoConfiguration(MessageProperties messageProperties) { 26 | this.messageProperties = messageProperties; 27 | } 28 | 29 | @Bean 30 | public TuyaMessageDispatcher tuyaMessageDispatcher(TuyaMessageDataSource tuyaMessageDataSource) { 31 | if (StringUtils.isEmpty(tuyaMessageDataSource.getUrl())) { 32 | tuyaMessageDataSource.setUrl(messageProperties.getUrl()); 33 | } 34 | if (StringUtils.isEmpty(tuyaMessageDataSource.getAk())) { 35 | tuyaMessageDataSource.setUrl(messageProperties.getAk()); 36 | } 37 | if (StringUtils.isEmpty(tuyaMessageDataSource.getSk())) { 38 | tuyaMessageDataSource.setUrl(messageProperties.getSk()); 39 | } 40 | if (StringUtils.isEmpty(tuyaMessageDataSource.getSubNameSuffix())) { 41 | tuyaMessageDataSource.setUrl(messageProperties.getSubNameSuffix()); 42 | } 43 | return new TuyaMessageDispatcher(tuyaMessageDataSource); 44 | } 45 | 46 | @Bean 47 | @ConditionalOnMissingBean 48 | public TuyaMessageDataSource tuyaMessageDataSource() { 49 | return new TuyaMessageDataSource( 50 | messageProperties.getUrl(), 51 | messageProperties.getAk(), 52 | messageProperties.getSk(), 53 | messageProperties.getSubNameSuffix() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.1.2 9 | 10 | 11 | 12 | com.tuya 13 | tuya-spring-boot-starter-sample 14 | 1.0.0 15 | tuya-spring-boot-starter-sample 16 | Demo project for Spring Boot 17 | 18 | 19 | 17 20 | 1.5.4 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-web 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-test 36 | test 37 | 38 | 39 | 40 | com.tuya 41 | tuya-spring-boot-starter 42 | ${tuya-spring-boot-starter.version} 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-maven-plugin 51 | 52 | 53 | 54 | 55 | 56 | 57 | tuya-maven 58 | https://maven-other.tuya.com/repository/maven-public/ 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/ability/messaging/msg/DevicePropertyMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.ability.messaging.msg; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.SourceMessage; 6 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | import java.io.Serial; 11 | import java.io.Serializable; 12 | import java.util.List; 13 | 14 | @Getter @Setter 15 | public class DevicePropertyMessage extends BaseTuyaMessage { 16 | 17 | private String dataId; 18 | private String devId; 19 | private String productId; 20 | private List properties; 21 | 22 | @Override 23 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 24 | super.defaultBuild(sourceMessage, messageBody); 25 | this.dataId = messageBody.getJSONObject("bizData").getString("dataId"); 26 | this.devId = messageBody.getJSONObject("bizData").getString("devId"); 27 | this.productId = messageBody.getJSONObject("bizData").getString("productId"); 28 | JSONArray jsonArray = messageBody.getJSONObject("bizData").getJSONArray("properties"); 29 | if (jsonArray != null) { 30 | properties = jsonArray.stream().map( 31 | item -> { 32 | JSONObject jsonObject = (JSONObject) item; 33 | PropertyItem propertyItem = new PropertyItem(); 34 | propertyItem.setCode(jsonObject.getString("code")); 35 | propertyItem.setValue(jsonObject.get("value")); 36 | propertyItem.setDpId(jsonObject.getString("dpId")); 37 | propertyItem.setTime(jsonObject.getLong("time")); 38 | return propertyItem; 39 | } 40 | ).toList(); 41 | } 42 | } 43 | 44 | @Override 45 | public String type() { 46 | return "devicePropertyMessage"; 47 | } 48 | 49 | @Getter @Setter 50 | class PropertyItem implements Serializable { 51 | 52 | @Serial 53 | private static final long serialVersionUID = -5316969945618066530L; 54 | 55 | private String code; 56 | private Object value; 57 | private String dpId; 58 | private Long time; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/token/TuyaTokenTest.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.token; 2 | 3 | import com.tuya.connector.api.config.ApiDataSource; 4 | import com.tuya.connector.api.config.Configuration; 5 | import com.tuya.connector.api.config.Logging; 6 | import com.tuya.connector.api.core.ConnectorFactory; 7 | import com.tuya.connector.api.core.DefaultConnectorFactory; 8 | import com.tuya.connector.api.header.HeaderProcessor; 9 | import com.tuya.connector.open.api.SecurityInfo; 10 | import com.tuya.connector.open.api.context.TuyaContextManager; 11 | import com.tuya.connector.open.api.header.TuyaHeaderProcessor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.junit.jupiter.api.BeforeAll; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.ValueSource; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | 19 | 20 | /** 21 | *

TODO 22 | * 23 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 24 | * @since 2021/2/5 4:26 下午 25 | */ 26 | @Slf4j 27 | public class TuyaTokenTest { 28 | 29 | static TokenConnector connector; 30 | 31 | @BeforeAll 32 | static void init() { 33 | Configuration config = new Configuration(); 34 | ApiDataSource dataSource = ApiDataSource.DEFAULT_BUILDER.build(); 35 | dataSource.setBaseUrl("https://openapi.tuyacn.com"); 36 | dataSource.setAk(SecurityInfo.AK); 37 | dataSource.setSk(SecurityInfo.SK); 38 | dataSource.setLoggingStrategy(Logging.Strategy.BASIC); 39 | dataSource.setAutoSetHeader(true); 40 | HeaderProcessor headerProcessor = new TuyaHeaderProcessor(config); 41 | dataSource.setHeaderProcessor(headerProcessor); 42 | 43 | dataSource.setContextManager(new TuyaContextManager(config)); 44 | dataSource.setAutoRefreshToken(true); 45 | dataSource.setTokenManager(new TuyaTokenManager(config)); 46 | 47 | config.setApiDataSource(dataSource); 48 | config.init(); 49 | 50 | ConnectorFactory connectorFactory = new DefaultConnectorFactory(config); 51 | connector = connectorFactory.loadConnector(TokenConnector.class); 52 | } 53 | 54 | @ParameterizedTest 55 | @ValueSource(ints = 1) 56 | void getTokenTest(int grantType) { 57 | TuyaToken tuyaToken = connector.getToken(grantType); 58 | log.info("TOKEN: {}", tuyaToken.toString()); 59 | assertTrue(tuyaToken.getExpire_time() > 0 && tuyaToken.getUid().equals("*********")); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /tuya-common/src/main/java/com/tuya/connector/open/common/util/Sha256Util.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.common.util; 2 | 3 | import javax.crypto.Mac; 4 | import javax.crypto.SecretKey; 5 | import javax.crypto.spec.SecretKeySpec; 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.InvalidKeyException; 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | /** 12 | * Description: TODO 13 | * 14 | * @author Chyern 15 | * @since 2021/4/7 16 | */ 17 | public class Sha256Util { 18 | 19 | public static String encryption(String str) throws Exception{ 20 | return encryption(str.getBytes(StandardCharsets.UTF_8)); 21 | } 22 | 23 | public static String encryption(byte[] buf) throws Exception{ 24 | MessageDigest messageDigest; 25 | messageDigest = MessageDigest.getInstance("SHA-256"); 26 | messageDigest.update(buf); 27 | return byte2Hex(messageDigest.digest()); 28 | } 29 | 30 | private static String byte2Hex(byte[] bytes) { 31 | StringBuilder stringBuffer = new StringBuilder(); 32 | String temp; 33 | for (byte aByte : bytes) { 34 | temp = Integer.toHexString(aByte & 0xFF); 35 | if (temp.length() == 1) { 36 | stringBuffer.append("0"); 37 | } 38 | stringBuffer.append(temp); 39 | } 40 | return stringBuffer.toString(); 41 | } 42 | 43 | public static String sign(String content, String secret) { 44 | Mac sha256HMAC = null; 45 | try { 46 | sha256HMAC = Mac.getInstance("HmacSHA256"); 47 | } catch (NoSuchAlgorithmException e) { 48 | e.printStackTrace(); 49 | } 50 | SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); 51 | try { 52 | sha256HMAC.init(secretKey); 53 | } catch (InvalidKeyException e) { 54 | e.printStackTrace(); 55 | } 56 | byte[] digest = sha256HMAC.doFinal(content.getBytes(StandardCharsets.UTF_8)); 57 | return printHexBinary(digest).toUpperCase(); 58 | } 59 | 60 | private static final char[] hexCode = "0123456789ABCDEF".toCharArray(); 61 | 62 | /** 63 | * 字节数组转16进制字符串 64 | */ 65 | private static String printHexBinary(byte[] data) { 66 | StringBuilder r = new StringBuilder(data.length * 2); 67 | for (byte b : data) { 68 | r.append(hexCode[(b >> 4) & 0xF]); 69 | r.append(hexCode[(b & 0xF)]); 70 | } 71 | return r.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/test/java/com/tuya/open/spring/boot/sample/service/SIServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.service; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.tuya.open.spring.boot.sample.ability.model.DeviceProperties; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.MockitoAnnotations; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | @SpringBootTest 15 | class SIServiceTest { 16 | 17 | @InjectMocks 18 | SIService siService; 19 | 20 | String deviceId = "6c8561a54bf607698f2sgw"; 21 | 22 | @BeforeEach 23 | void setUp() { 24 | MockitoAnnotations.openMocks(this); 25 | } 26 | 27 | @Test 28 | void fastjson_test() { 29 | Map m =new HashMap<>(); 30 | Long[] longs = {17201088000000L}; 31 | m.put("fieldValues", longs); 32 | System.out.println(JSON.toJSONString(m)); 33 | } 34 | 35 | @Test 36 | void isDeviceExist_validDeviceId_returnsResult() { 37 | siService.isDeviceExist(deviceId); 38 | } 39 | 40 | @Test 41 | void getDevice_validDeviceId_returnsDeviceDetail() { 42 | siService.getDevice(deviceId); 43 | } 44 | 45 | @Test 46 | void getFirmware_validDeviceId_returnsFirmware() { 47 | siService.getFirmware(deviceId); 48 | } 49 | 50 | @Test 51 | void getExtProperties_validDeviceId_returnsProperties() { 52 | siService.getExtProperties(deviceId); 53 | } 54 | 55 | @Test 56 | void getDeviceSpecification_validDeviceId_returnsSpecification() { 57 | siService.getDeviceSpecification(deviceId); 58 | } 59 | 60 | @Test 61 | void getDeviceModel_validDeviceId_returnsModel() { 62 | siService.getDeviceModel(deviceId); 63 | } 64 | 65 | @Test 66 | void getDeviceProperties_validDeviceId_returnsProperties() { 67 | siService.getDeviceProperties(deviceId); 68 | } 69 | 70 | @Test 71 | void getDeviceState_validDeviceId_returnsState() { 72 | siService.getDeviceState(deviceId); 73 | } 74 | 75 | @Test 76 | void issueDeviceProperties_validDeviceIdAndProperties_issuesProperties() { 77 | DeviceProperties properties = new DeviceProperties(); 78 | siService.issueDeviceProperties(deviceId, properties); 79 | } 80 | 81 | @Test 82 | void getIndustryDevice_validDeviceId_returnsIndustryDevice() { 83 | siService.getIndustryDevice(deviceId); 84 | } 85 | } -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/SourceMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | *

TODO 7 | * 8 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 9 | * @since 2021/3/24 5:35 下午 10 | */ 11 | public class SourceMessage implements Serializable { 12 | 13 | private String data; 14 | private Integer protocol; 15 | private String pv; 16 | private String sign; 17 | private Long t; 18 | /** 智慧商业消息属性 */ 19 | private String encryptType; 20 | private String encryptPayload; 21 | private String v; 22 | /** 智慧商业消息属性 */ 23 | 24 | public String getData() { 25 | return data; 26 | } 27 | 28 | public void setData(String data) { 29 | this.data = data; 30 | } 31 | 32 | public Integer getProtocol() { 33 | return protocol; 34 | } 35 | 36 | public void setProtocol(Integer protocol) { 37 | this.protocol = protocol; 38 | } 39 | 40 | public String getPv() { 41 | return pv; 42 | } 43 | 44 | public void setPv(String pv) { 45 | this.pv = pv; 46 | } 47 | 48 | public String getSign() { 49 | return sign; 50 | } 51 | 52 | public void setSign(String sign) { 53 | this.sign = sign; 54 | } 55 | 56 | public Long getT() { 57 | return t; 58 | } 59 | 60 | public void setT(Long t) { 61 | this.t = t; 62 | } 63 | 64 | public String getEncryptType() { 65 | return encryptType; 66 | } 67 | 68 | public void setEncryptType(String encryptType) { 69 | this.encryptType = encryptType; 70 | } 71 | 72 | public String getEncryptPayload() { 73 | return encryptPayload; 74 | } 75 | 76 | public void setEncryptPayload(String encryptPayload) { 77 | this.encryptPayload = encryptPayload; 78 | } 79 | 80 | public String getV() { 81 | return v; 82 | } 83 | 84 | public void setV(String v) { 85 | this.v = v; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "MessageVO{" + 91 | "data='" + data + '\'' + 92 | ", protocol=" + protocol + 93 | ", pv='" + pv + '\'' + 94 | ", sign='" + sign + '\'' + 95 | ", t=" + t + 96 | ", encryptType='" + encryptType + '\'' + 97 | ", encryptPayload='" + encryptPayload + '\'' + 98 | ", v='" + v + '\'' + 99 | '}'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/MessageFactory.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 6 | import com.tuya.connector.open.messaging.event.UnknownMessage; 7 | import lombok.SneakyThrows; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.reflections.Reflections; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | import java.util.Set; 15 | 16 | /** 17 | *

TODO 18 | * 19 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 20 | * @since 2021/3/24 3:56 下午 21 | */ 22 | @Slf4j 23 | public class MessageFactory { 24 | 25 | private static Map> messageHandler = new HashMap<>(); 26 | 27 | static { 28 | Reflections reflections = new Reflections("com.tuya.connector.open.messaging.event"); 29 | Set> classSet = reflections.getSubTypesOf(BaseTuyaMessage.class); 30 | classSet.forEach(handler -> { 31 | try { 32 | BaseTuyaMessage baseTuyaMessage = handler.newInstance(); 33 | messageHandler.put(baseTuyaMessage.type(), handler); 34 | } catch (Exception ignore) { 35 | log.error("ignore {} handler.", handler.getSimpleName()); 36 | } 37 | }); 38 | } 39 | 40 | @SneakyThrows 41 | public static BaseTuyaMessage extract(SourceMessage sourceMessage, String sk) { 42 | String data; 43 | if (sourceMessage.getEncryptPayload() != null) { 44 | // problem left over by history 45 | data = sourceMessage.getEncryptPayload(); 46 | } else { 47 | data = sourceMessage.getData(); 48 | } 49 | String decryptData = AESBase64Utils.decrypt(data, sk.substring(8, 24)); 50 | JSONObject messageBody = JSON.parseObject(decryptData); 51 | String bizCode = null; 52 | if (Objects.nonNull(messageBody) && messageBody.size() > 0) { 53 | bizCode = messageBody.getString("bizCode"); 54 | } 55 | return generate(bizCode, sourceMessage, messageBody); 56 | } 57 | 58 | @SneakyThrows 59 | public static BaseTuyaMessage generate(String bizCode, SourceMessage sourceMessage, JSONObject messageBody) { 60 | Class msgHandler = messageHandler.getOrDefault(bizCode, UnknownMessage.class); 61 | BaseTuyaMessage tuyaMessage = msgHandler.newInstance(); 62 | tuyaMessage.defaultBuild(sourceMessage, messageBody); 63 | return tuyaMessage; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tuya-messaging/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.tuya 8 | tuya-connector 9 | 1.5.4 10 | 11 | 12 | tuya-messaging 13 | 14 | 15 | 16 | com.tuya 17 | tuya-common 18 | ${project.version} 19 | 20 | 21 | 22 | com.tuya 23 | connector-messaging 24 | 25 | 26 | 27 | ch.qos.logback 28 | logback-classic 29 | 30 | 31 | 32 | ch.qos.logback 33 | logback-core 34 | 35 | 36 | 37 | org.projectlombok 38 | lombok 39 | 40 | 41 | 42 | org.junit.jupiter 43 | junit-jupiter 44 | test 45 | 46 | 47 | 48 | com.alibaba 49 | fastjson 50 | 51 | 52 | 53 | org.apache.pulsar 54 | pulsar-client 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot 60 | provided 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-configuration-processor 66 | true 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-autoconfigure 72 | true 73 | 74 | 75 | 76 | javax.annotation 77 | javax.annotation-api 78 | 79 | 80 | 81 | org.reflections 82 | reflections 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/main/java/com/tuya/open/spring/boot/sample/service/SIService.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample.service; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.tuya.connector.api.model.Result; 5 | import com.tuya.open.spring.boot.sample.ability.api.ThingConnector; 6 | import com.tuya.open.spring.boot.sample.ability.model.DeviceDetail; 7 | import com.tuya.open.spring.boot.sample.ability.model.DeviceProperties; 8 | import com.tuya.open.spring.boot.sample.ability.model.DeviceSpecification; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | @Service 17 | public class SIService { 18 | @Autowired 19 | ThingConnector thingConnector; 20 | 21 | public void isDeviceExist(String deviceId) { 22 | Result result = thingConnector.getDeviceResult(deviceId); 23 | System.out.println(JSON.toJSONString(result)); 24 | } 25 | 26 | public void getDevice(String deviceId) { 27 | DeviceDetail device = thingConnector.getDevice(deviceId); 28 | System.out.println(JSON.toJSONString(device)); 29 | } 30 | 31 | public void getFirmware(String deviceId) { 32 | Object ret = thingConnector.getFirmware(deviceId); 33 | System.out.println(JSON.toJSONString(ret)); 34 | } 35 | 36 | public void getExtProperties(String deviceId) { 37 | List> ret = thingConnector.getDeviceExtProperties(deviceId); 38 | System.out.println(JSON.toJSONString(ret)); 39 | } 40 | 41 | public void getDeviceSpecification(String deviceId) { 42 | DeviceSpecification ret = thingConnector.getDeviceSpecification(deviceId); 43 | System.out.println(JSON.toJSONString(ret)); 44 | } 45 | 46 | public void getDeviceModel(String deviceId) { 47 | System.out.println(thingConnector.getDeviceModel(deviceId)); 48 | } 49 | 50 | public void getDeviceProperties(String deviceId) { 51 | DeviceProperties ret = thingConnector.getDeviceProperties(deviceId); 52 | System.out.println(JSON.toJSONString(ret)); 53 | } 54 | 55 | public void getDeviceState(String deviceId) { 56 | Map ret = thingConnector.getDeviceState(deviceId); 57 | System.out.println(JSON.toJSONString(ret)); 58 | } 59 | 60 | public void issueDeviceProperties(String deviceId, DeviceProperties properties) { 61 | Map param = new HashMap<>(); 62 | Map kv = new HashMap<>(); 63 | kv.put("switch_led", false); 64 | param.put("properties", kv); 65 | System.out.println(thingConnector.issueDeviceProperties(deviceId, param)); 66 | } 67 | 68 | public void getIndustryDevice(String deviceId) { 69 | System.out.println(JSON.toJSONString(thingConnector.getIndustryDevice(deviceId))); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tuya-messaging/src/test/java/com/tuya/connector/open/messaging/MessageTest.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | import com.tuya.connector.open.messaging.autoconfig.TuyaMessageDataSource; 4 | import com.tuya.connector.open.messaging.event.*; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.event.EventListener; 12 | 13 | /** 14 | *

TODO 15 | * 16 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 17 | * @since 2021/3/24 2:41 下午 18 | */ 19 | @Slf4j 20 | public class MessageTest { 21 | static AnnotationConfigApplicationContext ctx; 22 | 23 | @BeforeAll 24 | static void init() { 25 | ctx = new AnnotationConfigApplicationContext(); 26 | ctx.register(MessageConfig.class); 27 | ctx.refresh(); 28 | ctx.start(); 29 | } 30 | 31 | @Test 32 | void msgTest() { 33 | while (true) {} 34 | } 35 | 36 | 37 | @Configuration 38 | static class MessageConfig { 39 | @EventListener 40 | public void online(OnlineMessage onlineMessageEvent) { 41 | log.info("### online event happened, event: {}", onlineMessageEvent); 42 | } 43 | 44 | @EventListener 45 | public void offline(OfflineMessage offlineMessageEvent) { 46 | log.info("### offline event happened, event: {}", offlineMessageEvent); 47 | } 48 | 49 | @EventListener 50 | public void nameUpdate(NameUpdateMessage event) { 51 | log.info("### nameUpdate event happened, event: {}", event); 52 | 53 | } 54 | 55 | @EventListener 56 | public void delete(DeleteMessage event) { 57 | log.info("### delete event happened, event: {}", event); 58 | } 59 | 60 | @EventListener 61 | public void bindUser(BindUserMessage event) { 62 | log.info("### bindUser event happened, event: {}", event); 63 | } 64 | 65 | @EventListener 66 | public void statusReportMessage(StatusReportMessage event) { 67 | log.info("### statusReport event happened, event: {}", event); 68 | } 69 | 70 | @EventListener 71 | public void unknownMessage(UnknownMessage event) { 72 | log.info("### unknown event happened, event: {}", event); 73 | } 74 | 75 | @Bean 76 | public TuyaMessageDispatcher tuyaMessageDispatcher(TuyaMessageDataSource tuyaMessageDataSource) { 77 | return new TuyaMessageDispatcher(tuyaMessageDataSource); 78 | } 79 | 80 | @Bean 81 | public TuyaMessageDataSource tuyaMessageDataSource() { 82 | return new TuyaMessageDataSource("pulsar+ssl://mqe.tuyacn.com:7285/", SecurityInfo.AK, SecurityInfo.SK); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/SceneExecuteMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.SourceMessage; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | /** 11 | * @description: 场景执行事件 12 | * @author: jinyun.zhou@tuya.com 13 | * @create: 2021-04-01 15:26 14 | **/ 15 | public class SceneExecuteMessage extends BaseTuyaMessage { 16 | /** 17 | * private field 18 | */ 19 | private Long gid; 20 | private String uid; 21 | 22 | /** 23 | * bizData 24 | */ 25 | public static String NAME = "name"; 26 | public static String ID = "id"; 27 | private List actions; 28 | 29 | @Override 30 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 31 | super.defaultBuild(sourceMessage, messageBody); 32 | this.gid = messageBody.getLong("gid"); 33 | this.uid = messageBody.getString("uid"); 34 | 35 | JSONArray statusArray = messageBody.getJSONArray("actions"); 36 | if (Objects.nonNull(statusArray)) { 37 | this.actions = statusArray.toJavaList(Item.class); 38 | } 39 | } 40 | 41 | @Override 42 | public String type() { 43 | return "sceneExecute"; 44 | } 45 | 46 | public List getActions() { 47 | return actions; 48 | } 49 | 50 | public Long getGid() { 51 | return gid; 52 | } 53 | 54 | public void setGid(Long gid) { 55 | this.gid = gid; 56 | } 57 | 58 | public String getUid() { 59 | return uid; 60 | } 61 | 62 | public void setUid(String uid) { 63 | this.uid = uid; 64 | } 65 | 66 | public void setActions(List actions) { 67 | this.actions = actions; 68 | } 69 | 70 | public static class Item { 71 | private String entityId; 72 | private Integer execStatus; 73 | private Long executeTime; 74 | private String id; 75 | 76 | public String getEntityId() { 77 | return entityId; 78 | } 79 | 80 | public void setEntityId(String entityId) { 81 | this.entityId = entityId; 82 | } 83 | 84 | public Integer getExecStatus() { 85 | return execStatus; 86 | } 87 | 88 | public void setExecStatus(Integer execStatus) { 89 | this.execStatus = execStatus; 90 | } 91 | 92 | public Long getExecuteTime() { 93 | return executeTime; 94 | } 95 | 96 | public void setExecuteTime(Long executeTime) { 97 | this.executeTime = executeTime; 98 | } 99 | 100 | public String getId() { 101 | return id; 102 | } 103 | 104 | public void setId(String id) { 105 | this.id = id; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/event/BaseTuyaMessage.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.event; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.messaging.MessageEvent; 6 | import com.tuya.connector.open.messaging.SourceMessage; 7 | 8 | import java.io.Serializable; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | 12 | /** 13 | *

TODO 14 | * 15 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 16 | * @since 2021/3/24 3:33 下午 17 | */ 18 | public abstract class BaseTuyaMessage implements MessageEvent, Serializable { 19 | private static final long serialVersionUID = 8025704099210695269L; 20 | 21 | private SourceMessage sourceMessage; 22 | 23 | private String bizCode; 24 | private String devId; 25 | private String productKey; 26 | private Long ts; 27 | private String uuid; 28 | 29 | private Map bizData; 30 | 31 | public void defaultBuild(SourceMessage sourceMessage, JSONObject messageBody) { 32 | this.sourceMessage = sourceMessage; 33 | if (Objects.nonNull(messageBody) && messageBody.size() > 0) { 34 | this.bizCode = messageBody.getString("bizCode"); 35 | this.devId = messageBody.getString("devId"); 36 | this.productKey = messageBody.getString("productKey"); 37 | this.ts = messageBody.getLong("ts"); 38 | this.bizData = messageBody.getJSONObject("bizData"); 39 | } 40 | } 41 | 42 | public static long getSerialVersionUID() { 43 | return serialVersionUID; 44 | } 45 | 46 | public SourceMessage getSourceMessage() { 47 | return sourceMessage; 48 | } 49 | 50 | public void setSourceMessage(SourceMessage sourceMessage) { 51 | this.sourceMessage = sourceMessage; 52 | } 53 | 54 | public String getBizCode() { 55 | return bizCode; 56 | } 57 | 58 | public void setBizCode(String bizCode) { 59 | this.bizCode = bizCode; 60 | } 61 | 62 | public String getDevId() { 63 | return devId; 64 | } 65 | 66 | public void setDevId(String devId) { 67 | this.devId = devId; 68 | } 69 | 70 | public String getProductKey() { 71 | return productKey; 72 | } 73 | 74 | public void setProductKey(String productKey) { 75 | this.productKey = productKey; 76 | } 77 | 78 | public Long getTs() { 79 | return ts; 80 | } 81 | 82 | public void setTs(Long ts) { 83 | this.ts = ts; 84 | } 85 | 86 | public String getUuid() { 87 | return uuid; 88 | } 89 | 90 | public void setUuid(String uuid) { 91 | this.uuid = uuid; 92 | } 93 | 94 | public Map getBizData() { 95 | return bizData; 96 | } 97 | 98 | public void setBizData(Map bizData) { 99 | this.bizData = bizData; 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return JSON.toJSONString(this); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/AESBase64Utils.java: -------------------------------------------------------------------------------- 1 | 2 | package com.tuya.connector.open.messaging; 3 | 4 | import org.apache.pulsar.shade.org.apache.commons.codec.binary.Base64; 5 | import org.apache.pulsar.shade.org.apache.commons.codec.binary.StringUtils; 6 | 7 | import javax.crypto.Cipher; 8 | import javax.crypto.spec.SecretKeySpec; 9 | import java.security.Key; 10 | 11 | public class AESBase64Utils { 12 | 13 | private static final String AES = "AES"; 14 | 15 | // 加密算法 16 | private String ALGO; 17 | 18 | // 16位的加密密钥 19 | private byte[] keyValue; 20 | 21 | /** 22 | * 用来进行加密的操作 23 | * 24 | * @param data 25 | * @return 26 | * @throws Exception 27 | */ 28 | public String encrypt(String data) throws Exception { 29 | Key key = generateKey(); 30 | Cipher c = Cipher.getInstance(ALGO); 31 | c.init(Cipher.ENCRYPT_MODE, key); 32 | byte[] encVal = c.doFinal(StringUtils.getBytesUtf8(data)); 33 | String encryptedValue = Base64.encodeBase64String(encVal); 34 | return encryptedValue; 35 | } 36 | 37 | /** 38 | * 用来进行解密的操作 39 | * 40 | * @param encryptedData 41 | * @return 42 | * @throws Exception 43 | */ 44 | public String decrypt(String encryptedData) throws Exception { 45 | Key key = generateKey(); 46 | Cipher c = Cipher.getInstance(ALGO); 47 | c.init(Cipher.DECRYPT_MODE, key); 48 | byte[] decodedValue = Base64.decodeBase64(encryptedData); 49 | byte[] decValue = c.doFinal(decodedValue); 50 | String decryptedValue = StringUtils.newStringUtf8(decValue); 51 | return decryptedValue; 52 | } 53 | 54 | /** 55 | * 根据密钥和算法生成Key 56 | * 57 | * @return 58 | * @throws Exception 59 | */ 60 | private Key generateKey() throws Exception { 61 | Key key = new SecretKeySpec(keyValue, ALGO); 62 | return key; 63 | } 64 | 65 | public String getALGO() { 66 | return ALGO; 67 | } 68 | 69 | public void setALGO(String aLGO) { 70 | ALGO = aLGO; 71 | } 72 | 73 | public byte[] getKeyValue() { 74 | return keyValue; 75 | } 76 | 77 | public void setKeyValue(byte[] keyValue) { 78 | this.keyValue = keyValue; 79 | } 80 | 81 | public static String decrypt(String data, String secretKey) throws Exception { 82 | // 创建加解密 83 | AESBase64Utils aes = new AESBase64Utils(); 84 | // 设置加解密算法 85 | aes.setALGO(AES); 86 | // 设置加解密密钥 87 | aes.setKeyValue(secretKey.getBytes()); 88 | // 进行解密后的字符串 89 | return aes.decrypt(data); 90 | } 91 | 92 | public static String encrypt(String data, String secretKey) throws Exception { 93 | // 创建加解密 94 | AESBase64Utils aes = new AESBase64Utils(); 95 | // 设置加解密算法 96 | aes.setALGO(AES); 97 | // 设置加解密密钥 98 | aes.setKeyValue(secretKey.getBytes()); 99 | // 进行解密后的字符串 100 | return aes.encrypt(data); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/MessageRegister.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 6 | import com.tuya.connector.open.messaging.event.UnknownMessage; 7 | import lombok.SneakyThrows; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.reflections.Reflections; 10 | import org.springframework.util.CollectionUtils; 11 | 12 | import java.util.*; 13 | 14 | /** 15 | *

TODO 16 | * 17 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 18 | * @since 2021/3/24 3:56 下午 19 | */ 20 | @Slf4j 21 | public class MessageRegister { 22 | 23 | private final static String INNER_MSG_PATH = "com.tuya.connector.open.messaging.event"; 24 | private static boolean isInit = false; 25 | private static final Map> MSG = new HashMap<>(); 26 | 27 | public static void init(Set pkgPaths) { 28 | if (isInit) { 29 | return; 30 | } 31 | log.info("###TUYA_PULSAR_MSG => start initial message register, pkgPaths: {}", pkgPaths); 32 | Set paths = new HashSet<>(); 33 | paths.add(INNER_MSG_PATH); 34 | if (!CollectionUtils.isEmpty(pkgPaths)) { 35 | paths.addAll(pkgPaths); 36 | } 37 | Reflections reflections = new Reflections(paths); 38 | Set> classSet = reflections.getSubTypesOf(BaseTuyaMessage.class); 39 | classSet.forEach(handler -> { 40 | try { 41 | BaseTuyaMessage baseTuyaMessage = handler.newInstance(); 42 | MSG.put(baseTuyaMessage.type(), handler); 43 | } catch (Exception ignore) { 44 | log.error("ignore {} handler.", handler.getSimpleName()); 45 | } 46 | }); 47 | isInit = true; 48 | } 49 | 50 | @SneakyThrows 51 | public static BaseTuyaMessage extract(SourceMessage sourceMessage, String sk) { 52 | String data; 53 | if (sourceMessage.getEncryptPayload() != null) { 54 | // problem left over by history 55 | data = sourceMessage.getEncryptPayload(); 56 | } else { 57 | data = sourceMessage.getData(); 58 | } 59 | String decryptData = AESBase64Utils.decrypt(data, sk.substring(8, 24)); 60 | JSONObject messageBody = JSON.parseObject(decryptData); 61 | String bizCode = null; 62 | if (Objects.nonNull(messageBody) && messageBody.size() > 0) { 63 | bizCode = messageBody.getString("bizCode"); 64 | } 65 | return generate(bizCode, sourceMessage, messageBody); 66 | } 67 | 68 | @SneakyThrows 69 | public static BaseTuyaMessage generate(String bizCode, SourceMessage sourceMessage, JSONObject messageBody) { 70 | Class msgHandler = MSG.getOrDefault(bizCode, UnknownMessage.class); 71 | BaseTuyaMessage tuyaMessage = msgHandler.newInstance(); 72 | tuyaMessage.defaultBuild(sourceMessage, messageBody); 73 | return tuyaMessage; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/autoconfig/DynamicMessageManager.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging.autoconfig; 2 | 3 | import com.tuya.connector.open.common.constant.EnvConstant; 4 | import com.tuya.connector.open.common.constant.TuyaRegion; 5 | import com.tuya.connector.open.messaging.TuyaMessageDispatcher; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.BeansException; 8 | import org.springframework.context.ApplicationContext; 9 | import org.springframework.context.ApplicationContextAware; 10 | import org.springframework.context.EnvironmentAware; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.core.env.Environment; 13 | import org.springframework.util.StringUtils; 14 | 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | /** 20 | *

TODO 21 | * 22 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 23 | * @since 2021/3/25 8:32 下午 24 | */ 25 | 26 | @Slf4j 27 | @Configuration 28 | public class DynamicMessageManager implements EnvironmentAware, ApplicationContextAware { 29 | 30 | static Environment env; 31 | static ApplicationContext ctx; 32 | static Map messageMap = new ConcurrentHashMap<>(); 33 | 34 | /** 35 | * add new message dispatcher 36 | * @param ak 37 | * @param sk 38 | * @param url 39 | * @return 40 | */ 41 | public static TuyaMessageDispatcher addMessageDispatcher(String ak, String sk, String url) { 42 | if (Objects.isNull(messageMap.get(ak))) { 43 | log.warn("message dispatcher has exists,url:{}",url); 44 | } 45 | if (StringUtils.isEmpty(url)) { 46 | String region = env.getProperty(EnvConstant.ENV_REGION); 47 | url = StringUtils.isEmpty(region) ? TuyaRegion.CN.getMsgUrl() : TuyaRegion.valueOf(region).getMsgUrl(); 48 | } 49 | TuyaMessageDataSource tuyaMessageDataSource = new TuyaMessageDataSource(url, ak, sk); 50 | TuyaMessageDispatcher tuyaMessageDispatcher = new TuyaMessageDispatcher(tuyaMessageDataSource); 51 | tuyaMessageDispatcher.setApplicationContext(ctx); 52 | tuyaMessageDispatcher.dispatch(); 53 | messageMap.put(ak, tuyaMessageDispatcher); 54 | return tuyaMessageDispatcher; 55 | } 56 | 57 | public static Boolean stopMessageDispatcher(String ak, String sk, String url) { 58 | TuyaMessageDispatcher tuyaMessageDispatcher = messageMap.get(ak); 59 | if (Objects.isNull(tuyaMessageDispatcher)) { 60 | log.warn("message dispatcher not exists,url:{}", url); 61 | return true; 62 | } 63 | boolean result = tuyaMessageDispatcher.stop(); 64 | log.info("stop message dispatcher {},url:{}", result, url); 65 | if (result) { 66 | messageMap.remove(ak); 67 | } 68 | return result; 69 | } 70 | 71 | 72 | @Override 73 | public void setEnvironment(Environment environment) { 74 | env = environment; 75 | } 76 | 77 | @Override 78 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 79 | ctx = applicationContext; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/errorprocessor/TokenValidErrorProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.errorprocessor; 2 | 3 | import com.tuya.connector.api.config.ApiDataSource; 4 | import com.tuya.connector.api.config.Configuration; 5 | import com.tuya.connector.api.config.Logging; 6 | import com.tuya.connector.api.config.Timeout; 7 | import com.tuya.connector.api.core.ConnectorFactory; 8 | import com.tuya.connector.api.core.DefaultConnectorFactory; 9 | import com.tuya.connector.api.header.HeaderProcessor; 10 | import com.tuya.connector.open.api.SecurityInfo; 11 | import com.tuya.connector.open.api.connectors.device.DeviceConnector; 12 | import com.tuya.connector.open.api.context.TuyaContextManager; 13 | import com.tuya.connector.open.api.header.TuyaHeaderProcessor; 14 | import com.tuya.connector.open.api.model.Device; 15 | import com.tuya.connector.open.api.token.TuyaToken; 16 | import com.tuya.connector.open.api.token.TuyaTokenManager; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.BeforeAll; 20 | import org.junit.jupiter.api.Test; 21 | 22 | /** 23 | *

TODO 24 | * 25 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 26 | * @since 2021/2/6 1:41 下午 27 | */ 28 | @Slf4j 29 | public class TokenValidErrorProcessorTest { 30 | static DeviceConnector deviceConnector; 31 | String deviceId = "*********"; 32 | 33 | @BeforeAll 34 | static void init() { 35 | Configuration config = new Configuration(); 36 | ApiDataSource dataSource = ApiDataSource.DEFAULT_BUILDER.build(); 37 | dataSource.setTimeout(Timeout.builder().callTimeout(60).connectTimeout(60).readTimeout(60).writeTimeout(60).build()); 38 | dataSource.setBaseUrl("https://openapi.tuyacn.com"); 39 | dataSource.setAk(SecurityInfo.AK); 40 | dataSource.setSk(SecurityInfo.SK); 41 | dataSource.setLoggingStrategy(Logging.Strategy.BASIC); 42 | dataSource.setAutoSetHeader(true); 43 | HeaderProcessor headerProcessor = new TuyaHeaderProcessor(config); 44 | dataSource.setHeaderProcessor(headerProcessor); 45 | 46 | dataSource.setContextManager(new TuyaContextManager(config)); 47 | 48 | dataSource.setAutoRefreshToken(true); 49 | TuyaTokenManager tuyaTokenManager = new TuyaTokenManager(config); 50 | tuyaTokenManager.setCachedToken( 51 | TuyaToken.builder() 52 | .access_token("*********") 53 | .refresh_token("*********") 54 | .build() 55 | ); 56 | dataSource.setTokenManager(tuyaTokenManager); 57 | 58 | dataSource.getErrorProcessorRegister().register(new TokenInvalidErrorProcessor()); 59 | dataSource.setAutoRefreshToken(true); 60 | 61 | config.setApiDataSource(dataSource); 62 | config.init(); 63 | 64 | ConnectorFactory connectorFactory = new DefaultConnectorFactory(config); 65 | deviceConnector = connectorFactory.loadConnector(DeviceConnector.class); 66 | } 67 | 68 | /** 69 | * 可以手动debug在RetrofitDelegate.execute()方法内设置 response code 为token expired(1010) 70 | */ 71 | @Test 72 | void autoRefreshToken() { 73 | Device device = deviceConnector.getById(deviceId); 74 | log.error("DEVICE: {}", device); 75 | Assertions.assertEquals(device.getUuid(), deviceId); 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/token/TuyaTokenManager.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.token; 2 | 3 | import com.tuya.connector.api.config.Configuration; 4 | import com.tuya.connector.api.core.DefaultConnectorFactory; 5 | import com.tuya.connector.api.token.TokenManager; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.Future; 15 | 16 | /** 17 | *

TODO 18 | * 19 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 20 | * @since 2021/2/6 11:31 上午 21 | */ 22 | @Slf4j 23 | public class TuyaTokenManager implements TokenManager { 24 | 25 | private final static int TOKEN_GRANT_TYPE = 1; 26 | private final static ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); 27 | private final TokenConnector connector; 28 | private final Map cachedTokenMap = new ConcurrentHashMap<>(); 29 | private final Configuration configuration; 30 | 31 | public TuyaTokenManager(Configuration configuration) { 32 | this.configuration = configuration; 33 | connector = new DefaultConnectorFactory(configuration).loadConnector(TokenConnector.class); 34 | } 35 | 36 | @Override 37 | public TuyaToken getCachedToken() { 38 | String currentAk = configuration.getApiDataSource().getAk(); 39 | if (Objects.isNull(cachedTokenMap.get(currentAk))) { 40 | cachedTokenMap.put(currentAk, getToken()); 41 | } 42 | return cachedTokenMap.get(currentAk); 43 | } 44 | 45 | public void setCachedToken(TuyaToken cachedToken) { 46 | cachedTokenMap.put(configuration.getApiDataSource().getAk(), cachedToken); 47 | } 48 | 49 | @Override 50 | @SneakyThrows 51 | public TuyaToken getToken() { 52 | TuyaToken token = connector.getToken(TOKEN_GRANT_TYPE); 53 | if (Objects.isNull(token)) { 54 | log.error("Get token required not null."); 55 | } 56 | setCachedToken(token); 57 | log.info("Get token success, token: {}", token); 58 | return token; 59 | } 60 | 61 | @Override 62 | @SneakyThrows 63 | public TuyaToken refreshToken() { 64 | String ak = configuration.getApiDataSource().getAk(); 65 | String sk = configuration.getApiDataSource().getSk(); 66 | Future future = EXECUTOR.submit(() -> { 67 | try { 68 | configuration.getApiDataSource().setAk(ak); 69 | configuration.getApiDataSource().setSk(sk); 70 | return connector.getToken(TOKEN_GRANT_TYPE); 71 | } catch (Exception e) { 72 | log.error("refresh token error", e); 73 | return null; 74 | } finally { 75 | configuration.getApiDataSource().clear(); 76 | } 77 | }); 78 | TuyaToken refreshedToken = future.get(); 79 | if (Objects.isNull(refreshedToken)) { 80 | log.error("refreshed token required not null."); 81 | } 82 | cachedTokenMap.put(configuration.getApiDataSource().getAk(), refreshedToken); 83 | return refreshedToken; 84 | } 85 | 86 | @Override 87 | public Configuration getConfiguration() { 88 | return configuration; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tuya-spring-boot-starter-sample/src/test/java/com/tuya/open/spring/boot/sample/TuyaSpringBootStarterSampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.tuya.open.spring.boot.sample; 2 | 3 | import com.alibaba.ttl.threadpool.TtlExecutors; 4 | import com.tuya.connector.api.config.Configuration; 5 | import com.tuya.connector.open.messaging.autoconfig.MessageProperties; 6 | import com.tuya.connector.spring.boot.autoconfigure.ConnectorProperties; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.core.env.Environment; 13 | 14 | import java.util.concurrent.*; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | 19 | @Slf4j 20 | @SpringBootTest(classes = {TuyaSpringBootStarterSampleApplication.class}) 21 | class TuyaSpringBootStarterSampleApplicationTests { 22 | 23 | @Autowired 24 | private ApplicationContext applicationContext; 25 | 26 | @Autowired 27 | private Configuration configuration; 28 | 29 | @Autowired 30 | private Environment environment; 31 | 32 | @Autowired 33 | private MessageProperties messageProperties; 34 | 35 | @Test 36 | void contextLoads() { 37 | ConnectorProperties bean = applicationContext.getBean(ConnectorProperties.class); 38 | Boolean auto = environment.getRequiredProperty("connector.api.auto-set-header", Boolean.class); 39 | System.out.println(configuration.getApiDataSource().getBaseUrl()); 40 | System.out.println(messageProperties.getUrl()); 41 | assertNotNull(bean.getApi().getContextManager()); 42 | assertTrue(auto); 43 | } 44 | 45 | public ExecutorService executor = Executors.newFixedThreadPool(1); 46 | 47 | @Test 48 | public void threadTest() throws InterruptedException { 49 | executor = TtlExecutors.getTtlExecutorService(executor); 50 | 51 | CountDownLatch countDownLatch = new CountDownLatch(2); 52 | Thread t1 = new Thread(() ->{ 53 | configuration.getApiDataSource().setAk("1"); 54 | configuration.getApiDataSource().setSk("1"); 55 | log.info("t1:{} ak:{}, sk:{}", Thread.currentThread(), configuration.getApiDataSource().getAk(), configuration.getApiDataSource().getSk()); 56 | try { 57 | Thread.sleep(6000); 58 | } catch (InterruptedException e) { 59 | e.printStackTrace(); 60 | } 61 | executor.execute(()-> 62 | log.info("t1 child:{} ak:{}, sk:{}", Thread.currentThread(), configuration.getApiDataSource().getAk(), configuration.getApiDataSource().getSk())); 63 | countDownLatch.countDown(); 64 | }); 65 | 66 | Thread t2 = new Thread(() ->{ 67 | try { 68 | Thread.sleep(2000); 69 | } catch (InterruptedException e) { 70 | e.printStackTrace(); 71 | } 72 | configuration.getApiDataSource().setAk("2"); 73 | configuration.getApiDataSource().setSk("2"); 74 | log.info("t2:{} ak:{}, sk{}",Thread.currentThread(), configuration.getApiDataSource().getAk(), configuration.getApiDataSource().getSk()); 75 | executor.execute(() -> 76 | log.info("t2 child:{} ak:{}, sk:{}", Thread.currentThread(), configuration.getApiDataSource().getAk(), configuration.getApiDataSource().getSk())); 77 | countDownLatch.countDown(); 78 | }); 79 | 80 | t1.start(); 81 | t2.start(); 82 | countDownLatch.await(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /tuya-api/src/test/java/com/tuya/connector/open/api/connectors/device/ConnectorTest.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.connectors.device; 2 | 3 | import com.tuya.connector.api.config.ApiDataSource; 4 | import com.tuya.connector.api.config.Configuration; 5 | import com.tuya.connector.api.config.Logging; 6 | import com.tuya.connector.api.core.ConnectorFactory; 7 | import com.tuya.connector.api.core.DefaultConnectorFactory; 8 | import com.tuya.connector.api.header.HeaderProcessor; 9 | import com.tuya.connector.open.api.SecurityInfo; 10 | import com.tuya.connector.open.api.context.TuyaContextManager; 11 | import com.tuya.connector.open.api.header.TuyaHeaderProcessor; 12 | import com.tuya.connector.open.api.model.Device; 13 | import com.tuya.connector.open.api.token.TuyaTokenManager; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.junit.jupiter.api.Assertions; 16 | import org.junit.jupiter.api.BeforeAll; 17 | import org.junit.jupiter.api.Test; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | *

TODO 24 | * 25 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 26 | * @since 2021/2/6 12:43 下午 27 | */ 28 | @Slf4j 29 | public class ConnectorTest { 30 | 31 | static DeviceConnector deviceConnector; 32 | static IndustryDeviceConnector industryDeviceConnector; 33 | String deviceId = "*********"; 34 | 35 | static Configuration config; 36 | 37 | @BeforeAll 38 | static void init() { 39 | config = new Configuration(); 40 | ApiDataSource dataSource = ApiDataSource.DEFAULT_BUILDER.build(); 41 | dataSource.setBaseUrl("https://openapi.tuyacn.com"); 42 | 43 | dataSource.setAk(SecurityInfo.AK); 44 | dataSource.setSk(SecurityInfo.SK); 45 | dataSource.setLoggingStrategy(Logging.Strategy.BASIC); 46 | dataSource.setAutoSetHeader(true); 47 | HeaderProcessor headerProcessor = new TuyaHeaderProcessor(config); 48 | dataSource.setHeaderProcessor(headerProcessor); 49 | 50 | dataSource.setContextManager(new TuyaContextManager(config)); 51 | 52 | dataSource.setAutoRefreshToken(true); 53 | dataSource.setTokenManager(new TuyaTokenManager(config)); 54 | 55 | config.setApiDataSource(dataSource); 56 | config.init(); 57 | 58 | ConnectorFactory connectorFactory = new DefaultConnectorFactory(config); 59 | deviceConnector = connectorFactory.loadConnector(DeviceConnector.class); 60 | industryDeviceConnector = connectorFactory.loadConnector(IndustryDeviceConnector.class); 61 | } 62 | 63 | @Test 64 | void getById() { 65 | Device device = deviceConnector.getById(deviceId); 66 | log.error("DEVICE: {}", device); 67 | Assertions.assertEquals(device.getId(), deviceId); 68 | } 69 | 70 | @Test 71 | void testSendCommand(){ 72 | CommandWrapper cmdWrapper = new CommandWrapper(); 73 | cmdWrapper.commands = new ArrayList<>(); 74 | cmdWrapper.commands.add(new Command()); 75 | cmdWrapper.commands.get(0).code = "basic_indicator"; 76 | cmdWrapper.commands.get(0).value = true; 77 | industryDeviceConnector.sendCommands(deviceId,cmdWrapper); 78 | } 79 | 80 | 81 | /** 82 | * 中文:zh 83 | * 英文:en 84 | */ 85 | @Test 86 | void testLanguage() { 87 | config.getApiDataSource().setLang("zh-CN"); 88 | Object ret1 = deviceConnector.specification(deviceId); 89 | log.info("中文语言请求结果: {}", ret1); 90 | 91 | config.getApiDataSource().setLang("en-US"); 92 | Object ret2 = deviceConnector.specification(deviceId); 93 | log.info("English language result: {}", ret2); 94 | } 95 | 96 | static class Command{ 97 | String code; 98 | Object value; 99 | } 100 | static class CommandWrapper{ 101 | List commands; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | [中文版](README_zh.md) 2 | 3 | [![license: Apache 2](https://img.shields.io/badge/license-Apache%202-green)](https://github.com/tuya/tuya-connector/blob/master/LICENSE 'License') 4 | ![Version: 1.0.0](https://img.shields.io/badge/version-1.0.0-blue) 5 | 6 | 7 | `tuya-connector`可以使得开发者在涂鸦云云对接(OpenAPI或者消息订阅)项目过程中,就如同本地开发一样,无需关注跟云端的连接和处理过程,从而帮助开发者更加聚焦在自身的业务逻辑上。 8 | #### [演示视频](https://www.bilibili.com/video/BV1E5411g7H1?share_source=copy_web) 9 | 10 | 11 | ## 快速开始 12 | ### SpringBoot集成 13 | 14 | ```xml 15 | 16 | com.tuya 17 | tuya-spring-boot-starter 18 | #{latest.version} 19 | 20 | 21 | 22 | 23 | tuya-maven 24 | https://maven-other.tuya.com/repository/maven-public/ 25 | 26 | ``` 27 | 28 | #### 配置 29 | ```properties 30 | # 涂鸦IoT平台云开发应用 ClientId & SecretKey 31 | connector.ak=*** 32 | connector.sk=*** 33 | ``` 34 | 区域配置 35 | ```properties 36 | # 区域配置(默认使用中国区) 37 | # 具体配置值参考:com.tuya.connector.open.common.constant.TuyaRegion) 38 | connector.region=CN 39 | ``` 40 | 41 | #### 使用 42 | ##### **调用OpenAPI** 43 | 44 | 1. 创建`Connector`接口(OpenAPI的映射类) 45 | ```java 46 | public interface DeviceConnector { 47 | /** 48 | * query device info by device_id 49 | * @param deviceId 50 | * @return 51 | */ 52 | @GET("/v1.0/devices/{device_id}") 53 | Device getById(@Path("device_id") String deviceId); 54 | } 55 | ``` 56 | 57 | 2. Spring应用启动类添加`@ConnectorScan`扫描路径,如果需要消息订阅,可以通过`@EnableMessaging`开启。 58 | > 注意:由于 SDK 底层依赖了反射机制,JDK9 开始提供了模块化机制,因此启动时需要添加 `--add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED` 参数,否则会报错。 59 | ```java 60 | @ConnectorScan(basePackages = "com.xxx.connectors") 61 | @EnableMessaging 62 | @SpringBootApplication 63 | public class DemoApplication { 64 | public static void main(String[] args) { 65 | SpringApplication.run(DemoApplication.class, args); 66 | } 67 | } 68 | ``` 69 | 70 | 3. 直接注入`Connector`即可调用 71 | ```java 72 | @Service 73 | public class DeviceService { 74 | @Autowired 75 | private DeviceConnector device; 76 | 77 | public Device getById(String deviceId) { 78 | return device.getById(deviceId); 79 | } 80 | } 81 | ``` 82 | 83 | ##### **订阅消息事件** 84 | ```java 85 | /** 86 | * device status data report event 87 | */ 88 | @EventListener 89 | public void statusReportMessage(StatusReportMessage event) { 90 | log.info("### StatusReport event happened, eventInfo: {}", event); 91 | } 92 | ``` 93 | 94 | ## 原理:基于 [connector](https://github.com/tuya/connector) 框架的涂鸦云平台扩展实现 95 | ### OpenAPI扩展点实现 96 | 97 | - ErrorProcessor 98 | 99 | 当请求OpenAPI后的响应结果返回错误码时,可以通过自定义`ErrorProcessor`的实现类来针对不同的错误码进行相应的处理,比如Token失效时,自动刷新token后重新请求;`TokenInvalidErrorProcessor`是内置的`ErrorProcessor`。 100 | > **扩展的ErrorProcessor需要注入到Spring容器才能生效。** 101 | 102 | 103 | - ContextManager 104 | 105 | `connector`框架支持扩展上下文管理器,每次API请求时都会提前准备好上下文信息,`TuyaContextManager`提供了数据源连接、当前token以及多语言等信息的管理,框架支持的token自动刷新机制依赖上下文管理器。
106 | 107 | - TokenManager 108 | 109 | `TuyaTokenManager`实现了`connector`框架的`TokenManager`接口,作为框架默认的token管理机制,token信息会缓存在本地内存。 110 | > **如果开发者需要自行管理token,可以扩展TokenManager并注入到Spring容器。** 111 | 112 | 113 | - HeaderProcessor 114 | 115 | `TuyaHeaderProcessor`实现了涂鸦云OpenAPI请求时的header处理逻辑,包括需要添加的属性值以及签名。
116 | 117 | 118 | ### 消息扩展点实现 119 | 120 | - MessageDispatcher 121 | 122 | `TuyaMessageDispatcher`实现了`connector`框架`MessageDispatcher`消息分发接口,支持顺序订阅云端消息、数据解密、构建精确的具体消息类型并通过Spring事件机制分发出去。
123 | 124 | - MessageEvent 125 | 126 | 开发者需要针对需要订阅的事件添加相应的`ApplicationListener`即可,框架内置了涂鸦所有云端消息事件类型,订阅的消息数据包括原始加密消息数据以及解密后的结构化的消息数据。 127 | 128 | | **消息事件** | **BizCode** | **描述** | 129 | | --- | --- | --- | 130 | | StatusReportMessage | statusReport | 数据上报 | 131 | | OnlineMessage | online | 设备上线 | 132 | | OfflineMessage | offline | 设备离线 | 133 | | NameUpdateMessage | nameUpdate | 修改设备名称 | 134 | | DpNameUpdateMessage | dpNameUpdate | 修改设备功能点名称 | 135 | | DeleteMessage | delete | 删除设备 | 136 | | BindUserMessage | bindUser | 设备绑定用户 | 137 | | UpgradeStatusMessage | upgradeStatus | 设备升级状态 | 138 | | AutomationExternalActionMessage | automationExternalAction | 自动化外部动作 | 139 | | SceneExecuteMessage | sceneExecute | 场景执行 | 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | [中文版](README_zh.md) 2 | 3 | [![License: Apache 2](https://img.shields.io/badge/license-Apache%202-green)](https://github.com/tuya/tuya-connector/blob/master/LICENSE 'License') 4 | ![Version: 1.0.0](https://img.shields.io/badge/version-1.0.0-blue) 5 | 6 | `tuya-connector` helps you efficiently create cloud development projects regarding the OpenAPI or message subscription capabilities. You can put all the focus on business logic without taking care of server-side programming nor relational databases. 7 | 8 | 9 | #### [demo vedio](https://www.youtube.com/watch?v=pEGg-n43UhI) 10 | 11 | ## Quick start 12 | ### Integrate Spring Boot 13 | 14 | ```xml 15 | 16 | com.tuya 17 | tuya-spring-boot-starter 18 | #{latest.version} 19 | 20 | 21 | 22 | 23 | tuya-maven 24 | https://maven-other.tuya.com/repository/maven-public/ 25 | 26 | ``` 27 | 28 | #### Configuration 29 | ```properties 30 | # ClientId & SecretKey generated on the Tuya Cloud Development Platform 31 | connector.ak=*** 32 | connector.sk=*** 33 | ``` 34 | region configuration 35 | ```properties 36 | # region configuration(default region is China if without configuration) 37 | # more details, please check: com.tuya.connector.open.common.constant.TuyaRegion) 38 | connector.region=CN 39 | ``` 40 | #### Usage 41 | ##### **Call OpenAPI operations** 42 | 43 | 1. Create the `Connector` interface, which is the mapping class of OpenAPI. 44 | ```java 45 | public interface DeviceConnector { 46 | /** 47 | * query device info by device_id 48 | * @param deviceId 49 | * @return 50 | */ 51 | @GET("/v1.0/devices/{device_id}") 52 | Device getById(@Path("device_id") String deviceId); 53 | } 54 | ``` 55 | 56 | 2. Set `@ConnectorScan` for the class of the Spring Boot application. You can set `@EnableMessaging` to enable the message subscription capability. 57 | > Note: Since the connector SDK relies on the reflection mechanism, and starting from JDK 9, a modularization mechanism has been introduced. Therefore, when starting, you need to add the parameters `--add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED` to avoid potential errors. 58 | ```java 59 | @ConnectorScan(basePackages = "com.xxx.connectors") 60 | @EnableMessaging 61 | @SpringBootApplication 62 | public class DemoApplication { 63 | public static void main(String[] args) { 64 | SpringApplication.run(DemoApplication.class, args); 65 | } 66 | } 67 | ``` 68 | 69 | 3. The `Connector` interface will be scanned and injected into the Spring container. 70 | ```java 71 | @Service 72 | public class DeviceService { 73 | @Autowired 74 | private DeviceConnector device; 75 | 76 | public Device getById(String deviceId) { 77 | return device.getById(deviceId); 78 | } 79 | } 80 | ``` 81 | 82 | ##### **Subscribe to message events** 83 | ```java 84 | /** 85 | * device status data report event 86 | */ 87 | @EventListener 88 | public void statusReportMessage(StatusReportMessage event) { 89 | log.info("### StatusReport event happened, eventInfo: {}", event); 90 | } 91 | ``` 92 | 93 | ## How it works: implement extensions based on the [connector](https://github.com/tuya/connector) framework. 94 | ### Extension points of OpenAPI 95 | 96 | - ErrorProcessor 97 | 98 | You can define the implementation class of `ErrorProcessor` to handle different error responses. For example, if a token expires, it can be automatically refreshed. The API operation will be tried again with the refreshed token. `TokenInvalidErrorProcessor` is the built-in implementation class of `ErrorProcessor`. 99 | > **The extended `ErrorProcessor` must be injected into the Spring container to take effect.** 100 | 101 | 102 | - ContextManager 103 | 104 | The `connector` framework supports `TuyaContextManager` on which the automatic token refreshing depends. `TuyaContextManager` can prepare the context before API operations, and manage information including data source connection, tokens, and multilingual text. 105 | 106 | - TokenManager 107 | 108 | `TuyaTokenManager` is the default token management mechanism and implements the `TokenManager` interface in the `connector` framework. The token information is cached on the premises. 109 | > **To manage the token on the premises, you can extend `TokenManager` and inject it into the Spring container.** 110 | 111 | 112 | - HeaderProcessor 113 | 114 | `TuyaHeaderProcessor` implements the processing logic of the header for OpenAPI operations, including the required attribute values and signatures.
115 | 116 | 117 | ### Extension points of messages 118 | 119 | - MessageDispatcher 120 | 121 | `TuyaMessageDispatcher` implements `MessageDispatcher` interface for message dispatching in the `connector` framework. The dispatcher features message ordering and data decryption. It allows you to create specific message types and publish messages based on Spring's event mechanism.
122 | 123 | - MessageEvent 124 | 125 | You can add `ApplicationListener` to listen for required events. The `connector` framework includes all the Tuya's message event types. The message data contains ciphertext messages and plaintext messages. 126 | 127 | | **Message event** | **BizCode** | **Description** | 128 | | --- | --- | --- | 129 | | StatusReportMessage | statusReport | Report data to the cloud. | 130 | | OnlineMessage | online | A device is online. | 131 | | OfflineMessage | offline | A device is offline. | 132 | | NameUpdateMessage | nameUpdate | Modify the device name. | 133 | | DpNameUpdateMessage | dpNameUpdate | Modify the name of a data point. | 134 | | DeleteMessage | delete | Remove a device. | 135 | | BindUserMessage | bindUser | Bind the device to a user account. | 136 | | UpgradeStatusMessage | upgradeStatus | The update status. | 137 | | AutomationExternalActionMessage | automationExternalAction | Automate an external action. | 138 | | SceneExecuteMessage | sceneExecute | Execute a scene. | 139 | -------------------------------------------------------------------------------- /tuya-api/src/main/java/com/tuya/connector/open/api/header/TuyaHeaderProcessor.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.api.header; 2 | 3 | 4 | import com.tuya.connector.api.config.ApiDataSource; 5 | import com.tuya.connector.api.config.Configuration; 6 | import com.tuya.connector.api.header.HeaderProcessor; 7 | import com.tuya.connector.api.model.HttpRequest; 8 | import com.tuya.connector.api.token.TokenManager; 9 | import com.tuya.connector.open.api.token.TuyaToken; 10 | import com.tuya.connector.open.common.util.Sha256Util; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.util.StringUtils; 14 | 15 | import java.net.URL; 16 | import java.net.URLDecoder; 17 | import java.util.*; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * 22 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 23 | * @since 2021/2/3 7:05 下午 24 | */ 25 | @Slf4j 26 | public class TuyaHeaderProcessor implements HeaderProcessor { 27 | private static final String EMPTY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; 28 | private static final String SING_HEADER_NAME = "Signature-Headers"; 29 | private static final String NONCE_HEADER_NAME = "nonce"; 30 | private final Configuration configuration; 31 | 32 | public TuyaHeaderProcessor(Configuration configuration) { 33 | this.configuration = configuration; 34 | } 35 | 36 | @Override 37 | public Map value(HttpRequest request) { 38 | ApiDataSource dataSource = configuration.getApiDataSource(); 39 | TokenManager tokenManager = dataSource.getTokenManager(); 40 | String ak = dataSource.getAk(); 41 | String accessToken = null; 42 | boolean withToken = isWithToken(request.getUrl()); 43 | if (withToken) { 44 | TuyaToken token = (TuyaToken) tokenManager.getCachedToken(); 45 | accessToken = token.getAccessToken(); 46 | } 47 | 48 | Map flattenHeaders = flattenHeaders(request.getHeaders()); 49 | Map map = new HashMap<>(); 50 | String t = flattenHeaders.get("t"); 51 | if (!StringUtils.hasText(t)) { 52 | t = System.currentTimeMillis() + ""; 53 | } 54 | map.put("client_id", ak); 55 | map.put("t", t); 56 | map.put("sign_method", "HMAC-SHA256"); 57 | String lang = configuration.getApiDataSource().getLang(); 58 | if (StringUtils.hasText(lang)) { 59 | map.put("lang", lang); 60 | } 61 | String signHeaderName = flattenHeaders.getOrDefault(SING_HEADER_NAME, ""); 62 | map.put(SING_HEADER_NAME, signHeaderName); 63 | String nonce = flattenHeaders.getOrDefault(NONCE_HEADER_NAME, ""); 64 | map.put(NONCE_HEADER_NAME, nonce); 65 | String str; 66 | if (withToken) { 67 | str = ak + accessToken + t + nonce + stringToSign(request, flattenHeaders); 68 | } else { 69 | str = ak + t + nonce + stringToSign(request, flattenHeaders); 70 | } 71 | map.put("sign", sign(str)); 72 | if (withToken) { 73 | map.put("access_token", accessToken); 74 | } 75 | return map; 76 | } 77 | 78 | private Map flattenHeaders(Map> headers) { 79 | Map newHeaders = new HashMap<>(); 80 | headers.forEach((name, values) -> { 81 | if (values == null || values.isEmpty()) { 82 | newHeaders.put(name, ""); 83 | } else { 84 | newHeaders.put(name, values.get(0)); 85 | } 86 | }); 87 | return newHeaders; 88 | } 89 | 90 | @SneakyThrows 91 | private String stringToSign(HttpRequest request, Map headers) { 92 | List lines = new ArrayList<>(16); 93 | lines.add(request.getHttpMethod().toUpperCase()); 94 | String bodyHash = EMPTY_HASH; 95 | if (request.getBody() != null && request.getBody().length > 0) { 96 | bodyHash = Sha256Util.encryption(request.getBody()); 97 | } 98 | String signHeaders = headers.get(SING_HEADER_NAME); 99 | String headerLine = ""; 100 | if (signHeaders != null) { 101 | String[] sighHeaderNames = signHeaders.split("\\s*:\\s*"); 102 | headerLine = Arrays.stream(sighHeaderNames).map(it -> it.trim()) 103 | .filter(it -> it.length() > 0) 104 | .map(it->it+":"+headers.get(it)) 105 | .collect(Collectors.joining("\n")); 106 | } 107 | lines.add(bodyHash); 108 | lines.add(headerLine); 109 | URL url = request.getUrl(); 110 | String paramSortedPath = getPathAndSortParam(request); 111 | lines.add(paramSortedPath); 112 | return String.join("\n", lines); 113 | } 114 | 115 | @SneakyThrows 116 | private String getPathAndSortParam(HttpRequest request) { 117 | String originalPath = request.originalPath(); 118 | URL url = request.getUrl(); 119 | String query = url.getQuery(); 120 | if(!StringUtils.hasText(query)){ 121 | return originalPath; 122 | } 123 | Map kvMap = new TreeMap<>(); 124 | String[] kvs = query.split("\\&"); 125 | for(String kv: kvs){ 126 | String[] kvArr = kv.split("="); 127 | if(kvArr.length>1){ 128 | kvMap.put(kvArr[0], decode(kvArr[1])); 129 | }else{ 130 | kvMap.put(kvArr[0],""); 131 | } 132 | } 133 | return originalPath + "?" + kvMap.entrySet().stream().map(it->it.getKey()+"="+ encode(it.getValue())) 134 | .collect(Collectors.joining("&")); 135 | } 136 | 137 | @SneakyThrows 138 | private String decode(String data) { 139 | return URLDecoder.decode(data,"utf-8"); 140 | } 141 | 142 | @SneakyThrows 143 | private String encode(String data){ 144 | return data; 145 | //return URLEncoder.encode(data,"utf-8"); 146 | } 147 | 148 | @Override 149 | public String sign(String content) { 150 | ApiDataSource dataSource = configuration.getApiDataSource(); 151 | String sk = dataSource.getSk(); 152 | return Sha256Util.sign(content,sk); 153 | } 154 | 155 | @Override 156 | public Configuration getConfiguration() { 157 | return configuration; 158 | } 159 | 160 | private boolean isWithToken(URL url) { 161 | String path = url.getPath(); 162 | log.info("URL PATH: {}", path); 163 | 164 | return !path.contains("/v1.0/token") && !path.contains("/v1.0/authorize_token"); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | com.tuya 7 | tuya-connector 8 | pom 9 | 1.5.4 10 | 11 | tuya-api 12 | tuya-messaging 13 | tuya-common 14 | tuya-spring-boot-starter 15 | 16 | 17 | 18 | 11 19 | 11 20 | 1.5.0 21 | 5.7.0 22 | 1.18.26 23 | 1.2.3 24 | 1.2.83 25 | 2.1.1.RELEASE 26 | 3.0.5 27 | 1.3.2 28 | 0.9.10 29 | 30 | 31 | 32 | 33 | 34 | com.tuya 35 | connector-api 36 | ${connector.version} 37 | 38 | 39 | 40 | com.tuya 41 | connector-messaging 42 | ${connector.version} 43 | 44 | 45 | 46 | com.tuya 47 | connector-spring-boot-starter 48 | ${connector.version} 49 | 50 | 51 | 52 | com.tuya 53 | tuya-api 54 | ${project.version} 55 | 56 | 57 | 58 | com.tuya 59 | tuya-messaging 60 | ${project.version} 61 | 62 | 63 | 64 | com.tuya 65 | tuya-common 66 | ${project.version} 67 | 68 | 69 | 70 | com.tuya 71 | tuya-spring-boot-starter 72 | ${project.version} 73 | 74 | 75 | 76 | com.tuya 77 | tuya-ability 78 | ${project.version} 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-dependencies 84 | ${spring-boot.version} 85 | pom 86 | import 87 | 88 | 89 | 90 | ch.qos.logback 91 | logback-classic 92 | ${logback.version} 93 | true 94 | 95 | 96 | 97 | ch.qos.logback 98 | logback-core 99 | ${logback.version} 100 | true 101 | 102 | 103 | 104 | org.projectlombok 105 | lombok 106 | ${lombok.version} 107 | true 108 | 109 | 110 | 111 | org.junit.jupiter 112 | junit-jupiter 113 | ${junit-jupiter.version} 114 | test 115 | 116 | 117 | 118 | com.alibaba 119 | fastjson 120 | ${fastjson.version} 121 | true 122 | 123 | 124 | 125 | org.apache.pulsar 126 | pulsar-client 127 | ${pulsar-client.version} 128 | 129 | 130 | 131 | org.reflections 132 | reflections 133 | ${reflections.version} 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-source-plugin 143 | 3.2.1 144 | 145 | 146 | attach-sources 147 | 148 | jar 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-surefire-plugin 156 | 157 | true 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | tuya-maven 166 | tuya maven release 167 | https://maven-other.tuya.com/repository/maven-releases/ 168 | 169 | 170 | tuya-maven 171 | tuya maven snapshot 172 | https://maven-other.tuya.com/repository/maven-snapshots/ 173 | 174 | 175 | 176 | 177 | 178 | apache-maven 179 | https://repo.maven.apache.org/maven2/ 180 | 181 | true 182 | 183 | 184 | false 185 | 186 | 187 | 188 | tuya-maven 189 | https://maven-other.tuya.com/repository/maven-public/ 190 | 191 | true 192 | 193 | 194 | true 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /tuya-messaging/src/main/java/com/tuya/connector/open/messaging/TuyaMessageDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.tuya.connector.open.messaging; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.tuya.connector.messaging.MessageDataSource; 5 | import com.tuya.connector.messaging.MessageDispatcher; 6 | import com.tuya.connector.open.messaging.autoconfig.TuyaMessageDataSource; 7 | import com.tuya.connector.open.messaging.event.BaseTuyaMessage; 8 | import lombok.SneakyThrows; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.pulsar.client.api.*; 11 | import org.apache.pulsar.shade.org.apache.commons.codec.digest.DigestUtils; 12 | import org.springframework.beans.BeansException; 13 | import org.springframework.context.ApplicationContext; 14 | import org.springframework.context.ApplicationContextAware; 15 | 16 | import javax.annotation.PostConstruct; 17 | import java.io.IOException; 18 | import java.util.Map; 19 | import java.util.Objects; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | 23 | /** 24 | *

TODO 25 | * 26 | * @author 丘枫(余秋风 qiufeng.yu@tuya.com) 27 | * @since 2021/2/3 11:57 上午 28 | */ 29 | @Slf4j 30 | @SuppressWarnings("rawtypes") 31 | public class TuyaMessageDispatcher implements MessageDispatcher, ApplicationContextAware { 32 | private boolean switchFlag = true; 33 | 34 | static ApplicationContext ctx; 35 | 36 | private final TuyaMessageDataSource tuyaMessageDataSource; 37 | private final static ExecutorService EXECUTORS = Executors.newSingleThreadExecutor(); 38 | 39 | public TuyaMessageDispatcher(TuyaMessageDataSource tuyaMessageDataSource) { 40 | this.tuyaMessageDataSource = tuyaMessageDataSource; 41 | } 42 | 43 | @Override 44 | @SneakyThrows 45 | @PostConstruct 46 | public void dispatch() { 47 | log.debug("###TUYA_PULSAR_MSG => start initial pulsar consumer"); 48 | final String ak = tuyaMessageDataSource.getAk(); 49 | final String sk = tuyaMessageDataSource.getSk(); 50 | String url = tuyaMessageDataSource.getUrl(); 51 | String subNameSuffix = tuyaMessageDataSource.getSubNameSuffix(); 52 | 53 | PulsarClient client = PulsarClient.builder() 54 | .loadConf(tuyaMessageDataSource.clientLoadConf()) 55 | .serviceUrl(url) 56 | .authentication(new Authentication() { 57 | private static final long serialVersionUID = -826735355004851795L; 58 | 59 | @Override 60 | public String getAuthMethodName() { 61 | return "auth1"; 62 | } 63 | 64 | @Override 65 | public AuthenticationDataProvider getAuthData() throws PulsarClientException { 66 | return new AuthenticationDataProvider() { 67 | private static final long serialVersionUID = 3721578352443318652L; 68 | 69 | @Override 70 | public boolean hasDataFromCommand() { 71 | return true; 72 | } 73 | 74 | @Override 75 | public String getCommandData() { 76 | return String.format("{\"username\":\"%s\",\"password\":\"%s\"}", 77 | ak, DigestUtils.md5Hex(ak + DigestUtils.md5Hex(sk)).substring(8, 24)); 78 | } 79 | }; 80 | } 81 | 82 | @Override 83 | public void configure(Map authParams) {} 84 | 85 | @Override 86 | public void start() throws PulsarClientException {} 87 | 88 | @Override 89 | public void close() throws IOException {} 90 | }) 91 | .build(); 92 | String topic = String.format("%s/out/event", ak); 93 | String subscriptionName = String.format("%s-%s", ak, subNameSuffix); 94 | Consumer consumer = client.newConsumer() 95 | .loadConf(tuyaMessageDataSource.consumerLoadConf()) 96 | .topic(topic) 97 | .subscriptionName(subscriptionName) 98 | .subscribe(); 99 | log.debug("###TUYA_PULSAR_MSG => pulsar consumer initial success"); 100 | EXECUTORS.execute(worker(consumer, sk)); 101 | } 102 | 103 | @Override 104 | public boolean stop() { 105 | switchFlag = false; 106 | EXECUTORS.shutdownNow(); 107 | return true; 108 | } 109 | 110 | @Override 111 | public MessageDataSource getMsgDataSource() { 112 | return tuyaMessageDataSource; 113 | } 114 | 115 | @SuppressWarnings("NullableProblems") 116 | @Override 117 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 118 | ctx = applicationContext; 119 | } 120 | 121 | @SuppressWarnings("all") 122 | private Runnable worker(Consumer consumer, String sk) { 123 | Thread worker = new Thread( 124 | () -> { 125 | boolean consumerFlag = true; 126 | while (switchFlag) { 127 | log.debug("###TUYA_PULSAR_MSG => start receive next message"); 128 | Message message = null; 129 | BaseTuyaMessage baseTuyaMessage = null; 130 | MessageId msgId = null; 131 | String tid = null; 132 | long publishTime = -1L; 133 | try { 134 | message = consumer.receive(); 135 | if (Objects.isNull(message)) { 136 | continue; 137 | } 138 | msgId = message.getMessageId(); 139 | tid = message.getProperty("tid"); 140 | publishTime = message.getPublishTime(); 141 | log.debug("###TUYA_PULSAR_MSG => message received, msgId={}, publishTime={}, tid={}", msgId, publishTime, tid); 142 | String payload = new String(message.getData()); 143 | SourceMessage sourceMessage = JSON.parseObject(payload, SourceMessage.class); 144 | BaseTuyaMessage msg = MessageRegister.extract(sourceMessage, sk); 145 | log.debug("###TUYA_PULSAR_MSG => start process message, messageId={}, publishTime={}, tid={}, payload={}", 146 | msgId, publishTime, tid, payload); 147 | ctx.publishEvent(msg); 148 | log.debug("###TUYA_PULSAR_MSG => finish process message, messageId={}, publishTime={}, tid={}", 149 | msgId, publishTime, tid); 150 | consumerFlag = true; 151 | } catch (Exception e) { 152 | consumerFlag = false; 153 | log.debug(String.format("###TUYA_PULSAR_MSG => Consume tuya message error, msgId: %s, tid: %s ", msgId, tid), e); 154 | } finally { 155 | if (Objects.nonNull(message)) { 156 | try { 157 | log.debug("###TUYA_PULSAR_MSG => start message ack, messageId={}, publishTime={}, tid={}", msgId, publishTime, tid); 158 | if (consumerFlag) { 159 | consumer.acknowledge(message); 160 | } else { 161 | consumer.negativeAcknowledge(message); 162 | } 163 | log.debug("###TUYA_PULSAR_MSG => message ack success, ackFlag={}, messageId={}, publishTime={}, tid={}", consumerFlag, msgId, publishTime, tid); 164 | } catch (PulsarClientException e) { 165 | log.error(String.format("###TUYA_PULSAR_MSG => Ack tuya message error, msgId: %s, tid: %s ", msgId, tid), e); 166 | } 167 | } 168 | } 169 | } 170 | } 171 | ); 172 | worker.setDaemon(true); 173 | return worker; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------